# How the generator works

> The path from a folder in registry/ to a repo on your disk: loader, resolver, mergers, compiler, generator, and the file in packages/core that owns each step.

There is no template repo behind this. A selection goes through five stages,
each one a file in `packages/core/src`, and a whole repo comes out the other
end. The same selection gives the same bytes every time.

The stages, in order:

```
registry/  ->  loader  ->  resolver  ->  mergers  ->  compiler  ->  generator  ->  your repo
              registry.ts  resolver.ts  merge.ts    compile.ts    generate.ts
```

Each section names the real file and function, so you can read the source
alongside this page.

## 1. The registry is the product

`registry/` holds every plugin. A plugin is a folder: a stack, a battery or a
design. The generator has no idea what your battery is called. It reads the
folder, sorts it, and merges whatever it finds.

```
registry/<stacks|batteries|designs>/<id>/
  manifest.yaml            required
  template/                copied into the generated repo
  agent/
    rules/*.md             path-scoped conventions
    skills/*.md            slash commands
    agents/*.md            subagents
    hooks/*.md             hook definition plus its inline script
    solutions/*.md         long-form docs, also published at /cookbook
  design.yaml              designs only: the token set
  DESIGN.md                designs only
```

Today that is 1 stack, 26 batteries and 31 designs, plus 3 presets.

`loadRegistry(root)` in `packages/core/src/registry.ts` walks that tree and
turns it into the typed `Registry` in `types.ts`. It parses the manifests and
reads markdown frontmatter through `parseFrontmatter` from `markdown.ts`. It
sorts arrays by id and normalises text (BOM stripped, CRLF folded). It is
tolerant on purpose. A
broken manifest is kept, not dropped, so `validateRegistry` in `schema.ts` can
report every problem in one pass instead of dying on the first one.

`toIndex(registry)` makes the `RegistryIndex`: manifests, per-plugin counts,
presets and the tested pairs, with no template bodies and no absolute paths.
That is what the browser gets, which is how the builder re-resolves your picks
on every click with no round trip.

## 2. The resolver decides what is legal

`packages/core/src/resolver.ts` takes a raw `Selection` and returns a
`ResolvedPlan`. It is the only stage that says no. It also runs in the browser,
so it has no filesystem, no clock and no randomness.

`resolve(selection, index)` runs these passes:

- **Auto-add from `requires`.** Better Auth needs an ORM, a database and email,
  so picking it adds Drizzle, Neon and Resend. Each addition comes with an
  `auto-added` explanation and a sentence saying why, which the builder shows
  inline. The pass runs until nothing changes, and a cycle is reported as
  `requires-cycle` instead of looping.
- **Stack compatibility**, from each manifest's `compatibleStacks`.
- **Declared conflicts.** A `conflicts` entry names a plugin. Supabase Auth
  conflicts with Neon, so you get `conflict`, not a repo with two databases.
- **One choice per category**, reported as `category-conflict`.
- **The admin panel needs an auth provider**: `admin-needs-auth`.
- **The tested set.** Covered below.
- **Recommendations.** `recommends` never adds anything. It emits a `warn`.

The plan carries the final selection, the plugin ids in a fixed order (stack,
then batteries by category then id, then design), every explanation, the six
counts behind the counters, and `ok`. When `ok` is false, `generate` refuses to
run rather than write a repo that can't boot.

`isSelectable(batteryId, selection, index)` is the same machinery pointed
sideways. It resolves your selection with and without a battery and reports the
first blocking reason that battery adds. That is where the greyed-out options
and their reasons come from. Nothing in the UI decides what is legal on its own.

## 3. The mergers assemble the files

`packages/core/src/merge.ts` holds the ways two plugins can add to the same
output.

**Token substitution.** `templateVars(selection)` builds the token table and
`substitute(text, vars)` applies it to every template file, every
`contributes.scripts` value and every markdown body.

| Token | bun | pnpm | npm |
|---|---|---|---|
| `{{pm}}` | `bun` | `pnpm` | `npm` |
| `{{pmx}}` | `bunx` | `pnpm exec` | `npx` |
| `{{pmDlx}}` | `bunx` | `pnpm dlx` | `npx` |
| `{{pmRun}}` | `bun run` | `pnpm run` | `npm run` |
| `{{pmAdd}}` | `bun add` | `pnpm add` | `npm install` |
| `{{tsRun}}` | `bun` | `tsx` | `tsx` |
| `{{tsServer}}` | `bun --conditions=react-server` | `tsx --conditions=react-server` | `tsx --conditions=react-server` |

`{{pmx}}` runs a binary the repo already installed (prisma, drizzle-kit, tsx).
`{{pmDlx}}` fetches a one-off tool the repo doesn't depend on (neonctl, a CLI
pinned `@latest`). They differ under pnpm on purpose: `pnpm dlx prisma` ignores
`node_modules` and downloads the latest release.

Plus `{{projectName}}`, `{{design}}`, `{{mode}}` and the link tokens. A plugin
that writes `npm run dev` or `npx prisma` in a doc fails the template lint,
because that string is wrong for two thirds of users. The docs you are reading
use the same tokens: the tab strip above each command is `substitute` running
three times.

**JSON deep merge.** `deepMergeJson(base, patch)` owns `package.json`,
`tsconfig.json`, `.mcp.json` and `.claude/settings.json`. Keys are sorted on the
way out, and arrays are concatenated with primitives deduped, so two plugins
adding dependencies give one stable file.

**Markdown sections.** `appendMarkdownSection(base, heading, body)` builds
`CLAUDE.md` a section at a time.

**Slot fill.** `findSlots` and `fillSlots` handle injection. Everything else is
a plain file copy with collision checks: two plugins writing the same path
raises `CollisionError`, not a last-writer race.

### Slots

A stack template can leave a named hole:

```tsx
{/* @slot providers */}
```

`// @slot providers` in TypeScript and `<!-- @slot providers -->` in markdown
work the same way. A battery fills one by name from its manifest:

```yaml
contributes:
  slots:
    providers: "template/slots/provider.tsx"
```

Two rules keep this predictable. A slot a battery fills but nothing declares
fails `bun run validate` (`slot-missing`). It is never a silent skip. And an
unfilled slot line is deleted with its blank line, so a stack template reads
cleanly with zero batteries.

## 4. The compiler writes each agent target

The agent layer is written once in a neutral format (the frontmatter shapes in
`docs/architecture/CONTRACT.md`). `compileAgentLayer` in
`packages/core/src/compile.ts` writes it out for each target you picked. Claude
Code is always one.

| Target | Output |
|---|---|
| Claude Code | `CLAUDE.md`, `.claude/rules/<id>.md` with `paths:`, `.claude/agents/`, `.claude/skills/<name>/SKILL.md`, `.claude/hooks/*`, `.claude/settings.json`, `.mcp.json` |
| Codex | Root `AGENTS.md` plus a nested `AGENTS.md` per rule path prefix, skills in `.agents/skills/<name>/SKILL.md` |
| Cursor | `.cursor/rules/<id>.mdc` with `globs:`, plus one `skill-<name>.mdc` per skill that loads on request |

Path scoping is what gets translated. Claude Code reads a `paths:` list in the
rule's frontmatter, and a rule without one loads every session. Cursor reads
`globs:`. Codex has no globs. So the compiler takes the literal prefix of each
rule's paths and writes a nested `AGENTS.md` in that folder. Each one is sized
so the whole chain Codex reads fits its 32 KiB limit.

Hooks are wired in `.claude/settings.json` only. Each command runs its script
from `$CLAUDE_PROJECT_DIR` with your package manager (`bun`, or `tsx` from
`node_modules` for pnpm and npm), so a guard still fires after the agent runs
`cd`. See [the agentic layer](/docs/the-agentic-layer) for what Codex and Cursor
do with them.

## 5. The generator writes the repo

`generate(plan, registry)` in `packages/core/src/generate.ts` puts it together.
Order matters. Templates land first so slot markers exist to be filled. The
merged files go on top. The content hash is taken last, over everything except
`agentic.config.json`, which then carries it.

Along the way it builds the files no single plugin owns:

- `package.json` with the base scripts (`dev`, `build`, `start`, `typecheck`,
  `lint`, `lint:fix`, `format`, `test`, `test:e2e`, `verify`, `verify:hooks`)
  plus each battery's own.
- `.env.example` from every battery's declared env vars.
- `docs/onboard.md` from their onboarding steps, sorted by `order`.
- `CLAUDE.md`, a short index that points at `docs/onboard.md` first.
- `docs/solutions/` seeded with each plugin's solution docs.
- `docs/plans/` with a README and a plan template.
- `scripts/verify.ts` and `scripts/verify-hooks.ts`, filled in for your picks.

Three small modules finish the job:

- `hash.ts` (`hashFiles`): sha256 over the sorted path and content pairs.
- `write.ts` (`writeRepo`): writes to a folder, for the CLI and the matrix.
- `zip.ts` (`zipRepo`): builds the zip for the web download.

## The tested set

The resolver refuses any battery pair that is not in `registry/tested.yaml`, a
sorted list of pairs. A set that exactly matches a preset skips the pair check,
because the matrix runs every preset whole. Everything else is checked pair by
pair, and a missing pair is an `untested` error, not a warning.

So the promise is per pair. Pick three batteries and each of the three pairs has
passed the matrix. That exact trio may never have run together.

Only `bun run matrix` writes that file. For each combination it generates the
repo outside this monorepo, installs it, typechecks it, lints it and runs its
unit tests. With `--build` it also runs `next build` with no env set, boots the
app and loads every static route. `--update-tested` and `--add-tested` need
`--build`, and they write back only the pairs that passed. Edit the file by hand
and you claim a check that never happened. The resolver believes it completely,
and someone gets a repo that doesn't boot.

Two lanes go further than booting:

- `--db` gives each combo a fresh database on local Postgres. It runs the
  repo's own migrations and seeds (each seed twice, to prove it is safe to
  rerun), builds with that env and loads every route against the live
  database.
- `--browser` then runs the repo's own Playwright specs against that server:
  sign-up and sign-in, the app shell, `/pricing` and checkout with no provider
  keys, and the admin pages. A spec that has no environment to run in skips
  with a reason, and the lane prints every reason.

With either lane the matrix adds 8 lane combos, which between them cover every
auth, ORM, database and payments battery. No Docker: Neon runs through the
repo's own `db:proxy` and Supabase Auth through its GoTrue and PostgREST
binaries.


```sh
bun run matrix --db --browser --preset indie-saas
```


If the pair you want is greyed out, it isn't certified yet. From a clone of the
generator (it runs on Bun), run the matrix for that battery's pairs and commit
the result:


```sh
bun run matrix --battery <id> --pairs --build --add-tested
```


Leave out `--pairs` and no pair runs, so nothing new gets certified.

## Determinism, and why it replaces sync

The output has no timestamps, no random ids and no absolute paths. Keys are
sorted before they are written, and files are sorted by path.
`agentic.config.json` in the generated repo records the full selection, the
plugin ids, the counts and the sha256 of the output.

That file is the V1 answer to "how do I get updates". There is no sync. Months
later you regenerate from the same `agentic.config.json` on a newer registry and
diff it against your repo. You see exactly what changed and take what you want.
`bun run snapshot` in this repo generates every preset twice in one process and
fails if the hashes differ, so the property that makes this work is itself
tested.

## Read it yourself

| File | What it owns |
|---|---|
| `packages/core/src/types.ts` | Every shared type. Never redefined elsewhere. |
| `packages/core/src/registry.ts` | `loadRegistry`, `toIndex` |
| `packages/core/src/schema.ts` | `validateManifest`, `validateRegistry` |
| `packages/core/src/resolver.ts` | `resolve`, `isSelectable`, `defaultSelection`, `selectionFromPreset` |
| `packages/core/src/merge.ts` | `substitute`, `deepMergeJson`, `appendMarkdownSection`, `fillSlots` |
| `packages/core/src/compile.ts` | `compileAgentLayer`, `hookCommand` |
| `packages/core/src/generate.ts` | `generate` |
| `packages/core/src/markdown.ts` | `parseFrontmatter`, `serializeFrontmatter`, `extractScriptBlock` |
| `packages/core/src/hash.ts` · `write.ts` · `zip.ts` | `hashFiles`, `writeRepo`, `zipRepo` |

Next: [the agentic layer](/docs/the-agentic-layer), which is what all of this
exists to install.


---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
