Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 8 additions & 19 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ Out-of-date docs mislead worse than missing docs. Keep entries short (one page o

## File layout

One markdown file per topic. Group flavors under `flavors/`, engine capabilities under `engine/`, CLI surface flat. Suggested layout:
One markdown file per topic. Group flavors under `flavors/`, engine capabilities under `engine/`, CLI surface flat.

```
docs/
├── README.md # this file
├── cli.md # subcommands + flags (DONE)
├── engine/
│ ├── flavor-hooks.md # Symlinks, NextSteps, CommonTemplates (DONE)
│ ├── releases.md # tag-driven release flow (DONE)
│ ├── templates.md # .tmpl content substitution (TODO)
│ ├── path-templating.md # {{.ProjectName}} in file paths (TODO)
│ ├── common-overlay.md # common/ fallback layer (TODO)
│ └── done-gate.md # what check.sh runs downstream (TODO)
│ ├── templates.md # .tmpl content substitution (DONE)
│ ├── path-templating.md # {{.ProjectName}} in file paths (DONE)
│ ├── common-overlay.md # common/ fallback layer (DONE)
│ ├── done-gate.md # what check.sh runs downstream (DONE)
│ └── releases.md # tag-driven release flow (DONE)
└── flavors/
├── fullstack.md # (DONE)
├── go-cli.md # (DONE — worked --agents-only example)
Expand All @@ -37,18 +37,7 @@ docs/
└── project-management.md # (DONE — worked skill examples)
```

Every flavor and subcommand is documented. The four engine docs marked `TODO` are the remaining gap; the backlog below tracks them.

## Backlog

Features that exist in the code but don't yet have a doc entry. The next agent to touch one of these is on the hook to write its doc. Every flavor is documented; the remaining gap is four engine-capability docs.

### Engine

- [ ] `engine/templates.md` — `.tmpl` opt-in for content substitution. Source: [scaffold.go:306-318](../internal/scaffold/scaffold.go#L306-L318) (content render) and the `.tmpl` strip in [walkLayer, scaffold.go:163](../internal/scaffold/scaffold.go#L163).
- [ ] `engine/path-templating.md` — `{{.ProjectName}}` in file paths; the `.tmpl` workaround for `cmd/{{.ProjectName}}/` directories. Source: [renderPath, scaffold.go:320](../internal/scaffold/scaffold.go#L320).
- [ ] `engine/common-overlay.md` — `internal/flavors/common/` as a fallback layer; flavor-first conflict resolution. Source: [Overlay, scaffold.go:102](../internal/scaffold/scaffold.go#L102) and [walkLayer, scaffold.go:163](../internal/scaffold/scaffold.go#L163).
- [ ] `engine/done-gate.md` — what `check.sh` runs in scaffolded projects and how `maybe_step` skips missing recipes. Source: [internal/flavors/common/templates/.agent/scripts/check.sh](../internal/flavors/common/templates/.agent/scripts/check.sh).
Every subcommand, flag, flavor, and engine capability is documented — the backlog is empty. When you add a new feature, add its doc in the same change and link it from the layout above.

## Style

Expand All @@ -61,5 +50,5 @@ Features that exist in the code but don't yet have a doc entry. The next agent t

Two project-scoped Claude Code skills automate the workflows around this directory:

- `/feature-doc` — bootstraps a new doc entry against a backlog item or refreshes an existing one. Routes the doc to the right subdirectory and removes the matching TODO above. See [`.claude/skills/feature-doc/SKILL.md`](../.claude/skills/feature-doc/SKILL.md).
- `/feature-doc` — bootstraps a new doc entry or refreshes an existing one. Routes the doc to the right subdirectory and updates the layout index above. See [`.claude/skills/feature-doc/SKILL.md`](../.claude/skills/feature-doc/SKILL.md).
- `/add-flavor` — walks the seven-step flavor-authoring checklist, including the `docs/flavors/<name>.md` entry. See [`.claude/skills/add-flavor/SKILL.md`](../.claude/skills/add-flavor/SKILL.md).
42 changes: 42 additions & 0 deletions docs/engine/common-overlay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Common overlay

Code flavors don't carry their own copy of the shared scaffold files (`check.sh`, `gen-codemap.sh`, `review.sh`, the `.pre-commit-config.yaml`, and so on). Those live once in `internal/flavors/common/templates/` and are layered under every code flavor at scaffold time. A flavor ships only what is *unique* to it; the engine fills in the rest from common.

This keeps shared behavior in one place. To change something for every code flavor, edit the file in `common/`. To change it for a single flavor, ship that flavor's own copy at the same relative path — the flavor's copy wins.

## How the layering works

`writeTemplates` ([scaffold.go:138-161](../../internal/scaffold/scaffold.go#L138-L161)) builds an ordered list of layers and walks them in sequence:

1. The flavor's own `Templates` (at `TemplateRoot`).
2. The flavor's `CommonTemplates` (at `CommonRoot`), **only if** the flavor sets it.

A single `claimed` set — `map[string]bool` — is shared across both walks. The first layer to produce a given destination path claims it; any later layer that would write the same path is skipped ([walkLayer:221-224](../../internal/scaffold/scaffold.go#L221-L224)). Because the flavor layer is walked first, **the flavor overrides common** on any path collision, with no special-casing.

```
flavor layer → claims cmd/main.go, its own Justfile.tmpl, README.agent.md.tmpl
common layer → fills in .agent/scripts/check.sh, gen-codemap.sh, review.sh, ...
skips anything the flavor already claimed
```

## Opting in and out

The overlay is driven by two `Flavor` fields (see [flavor-hooks.md](./flavor-hooks.md#commontemplates)):

- **Code flavors** set `CommonTemplates: common.Templates()` ([common/flavor.go:13](../../internal/flavors/common/flavor.go#L13)), so they share the `.agent/scripts/` tooling and other common files.
- **Doc-collab flavors** (`claude-cowork`, `project-management`) leave `CommonTemplates` nil. They get no overlay — a document folder has no use for `check.sh` or `gen-codemap.sh`. When the field is nil the second layer is simply not appended ([scaffold.go:146](../../internal/scaffold/scaffold.go#L146)), and the walk covers the flavor's templates alone.

## Don't copy common files into a flavor

If a shared file needs to change for everyone, change it in `common/` — don't copy it into a flavor "to be safe." A flavor should contain only the paths where it genuinely diverges from common. An unnecessary copy silently pins that flavor to a stale version of a file the rest of the tree keeps evolving.

## `Overlay`: a single-layer writer for subcommands

`Overlay` ([scaffold.go:102-113](../../internal/scaffold/scaffold.go#L102-L113)) is a public entry point that walks **one** template layer onto an already-scaffolded target, using the same write/skip/dry-run semantics but without symlinks, `git init`, or a next-steps message. `add-tracker` uses it to merge a tracker's `integrations/<tracker>/` files into an existing `project-management` workspace. It is the incremental counterpart to the full `Run` scaffold.

## Source

- Layer assembly and the shared `claimed` set: [scaffold.go:138-161](../../internal/scaffold/scaffold.go#L138-L161) (`writeTemplates`).
- Per-layer walk and the skip-if-claimed rule: [scaffold.go:163-235](../../internal/scaffold/scaffold.go#L163-L235) (`walkLayer`), collision skip at [221-224](../../internal/scaffold/scaffold.go#L221-L224).
- Single-layer overlay for subcommands: [scaffold.go:102-113](../../internal/scaffold/scaffold.go#L102-L113) (`Overlay`).
- The common layer itself: [internal/flavors/common/flavor.go](../../internal/flavors/common/flavor.go) (`Templates`).
73 changes: 73 additions & 0 deletions docs/engine/done-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# The done-gate (`check.sh`)

Every scaffolded code project ships `.agent/scripts/check.sh` — the agent's "am I done" gate. The contract, encoded in the generated `AGENTS.md`, is: **don't declare a task complete until `./.agent/scripts/check.sh` passes.** Every step must succeed; a failing step means the work is not done, and the agent's job is to fix the failure, not bypass it.

The script is shipped from the common layer ([internal/flavors/common/templates/.agent/scripts/check.sh](../../internal/flavors/common/templates/.agent/scripts/check.sh)), so every code flavor gets the same gate. What actually runs is driven by the flavor's `Justfile`, which is how one script serves flavors as different as `go-cli` and `iac`.

## What it runs

The script runs `set -euo pipefail`, resolves the repo root with `git rev-parse`, then runs, in order:

1. **codemap** — regenerates `.agent/CODEBASE.md` via `gen-codemap.sh`, if that script is present and executable. Always run so the committed map reflects current state.
2. **generate-clients** — OpenAPI client generation (the `fullstack` flavor).
3. **fmt**
4. **lint**
5. **typecheck**
6. **test**
7. **playwright** — end-to-end tests (the `fullstack` flavor).

## Two kinds of step

The behavior that lets one script fit every flavor is the split between hard and optional steps:

| Helper | Behavior |
|--------|----------|
| `step` | Runs the command. On a non-zero exit it prints `✗ <name> failed` and **exits 1** — the gate fails. Used for the codemap regeneration. |
| `maybe_step` | Looks the recipe up in `just --list`. If the flavor's `Justfile` defines it, it runs as a hard `step`. If not, it prints `⊘ skipping '<recipe>' (no Just recipe defined)` and moves on. |

So the gate adapts to the project:

- `go-cli` / `go-backend` define `fmt`, `lint`, `typecheck`, `test` — those run; `generate-clients` and `playwright` are skipped.
- `fullstack` additionally defines `generate-clients` and `playwright` — those run too.
- `iac` defines `fmt` / `lint` / `typecheck` / `test`, each guarded internally with `command -v` so they no-op when the toolchain isn't installed.
- A brand-new empty scaffold skips every `maybe_step` and still passes — the project is installable before any application code exists.

The distinction is important: `maybe_step` skips only when the **recipe is absent**. A recipe that exists and *fails* is a hard failure that stops the gate.

## Example

```
$ ./.agent/scripts/check.sh
Running done-gate checks for my-tool

→ codemap
✓ codemap passed
⊘ skipping 'generate-clients' (no Just recipe defined)

→ fmt
✓ fmt passed

→ lint
✓ lint passed

→ typecheck
✓ typecheck passed

→ test
✓ test passed
⊘ skipping 'playwright' (no Just recipe defined)

✓ all checks passed
```

## The Justfile is the source of truth for steps

`check.sh` never hard-codes tools; it only knows recipe names. The actual commands live in the flavor's `Justfile`, and each flavor doc lists its recipes — see [go-cli](../flavors/go-cli.md#justfile-recipes), [go-backend](../flavors/go-backend.md#justfile-recipes), [fullstack](../flavors/fullstack.md#justfile-recipes), and [iac](../flavors/iac.md#justfile-recipes). To add a gate step for a flavor, add the recipe to its `Justfile`; `check.sh` picks it up automatically if it is one of the names above.

> This repository's *own* `check.sh` (used to develop `agent-init`) is a superset of the shipped one — it adds a soft `vulncheck` step and the scaffold smoke test. Downstream projects get the common gate described here.

## Source

- The shipped gate: [internal/flavors/common/templates/.agent/scripts/check.sh](../../internal/flavors/common/templates/.agent/scripts/check.sh) (`step` and `maybe_step`).
- Codemap regeneration it invokes: [common/templates/.agent/scripts/gen-codemap.sh](../../internal/flavors/common/templates/.agent/scripts/gen-codemap.sh).
- Recipes come from each flavor's `Justfile.tmpl` under `internal/flavors/<flavor>/templates/`.
2 changes: 1 addition & 1 deletion docs/engine/flavor-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ CommonRoot string

Optional fallback layer. If set, the scaffold engine walks `CommonTemplates` after the flavor's own `Templates`, claiming any relative path the flavor didn't already produce. Code flavors all set `CommonTemplates: common.Templates()` so they share `.agent/scripts/check.sh`, `gen-codemap.sh`, and `review.sh`. Non-code flavors leave it nil — `claude-cowork` doesn't want `check.sh` or `gen-codemap.sh` in a document folder.

See [common-overlay.md](./common-overlay.md) (TODO) for the layering semantics and conflict resolution.
See [common-overlay.md](./common-overlay.md) for the layering semantics and conflict resolution.

## Adding a non-code flavor

Expand Down
50 changes: 50 additions & 0 deletions docs/engine/path-templating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Path templating

The scaffold engine runs every file's **destination path** through Go's `text/template`, regardless of whether the file's content is templated. This lets a directory or file name reflect the project name — most importantly `cmd/{{.ProjectName}}/main.go`, which renders to `cmd/my-tool/main.go` when scaffolding into `./my-tool`.

Path templating uses the same one-field data model as [content templating](./templates.md): `{{.ProjectName}}` is the target directory's base name.

## How it works

`renderPath` ([scaffold.go:320-333](../../internal/scaffold/scaffold.go#L320-L333)) renders a relative path string:

- If the path contains no `{{`, it is returned unchanged (a fast path that skips the template engine for the common case).
- Otherwise the path is parsed and executed against the scaffold data, and the result is the on-disk destination.

It is called on every walked file after the `.tmpl` suffix is stripped ([walkLayer:209](../../internal/scaffold/scaffold.go#L209)), and also on the flavor's `FreshOnlyPaths` ([scaffold.go:246](../../internal/scaffold/scaffold.go#L246)) and `.agents-only` variant base names ([scaffold.go:181](../../internal/scaffold/scaffold.go#L181)) so those comparisons match the rendered layout.

## Example

The `go-cli` flavor ships its entry point as:

```
internal/flavors/gocli/templates/cmd/{{.ProjectName}}/main.go.tmpl
```

Scaffolded into `./my-tool`, the path renders and the `.tmpl` suffix drops, producing:

```
cmd/my-tool/main.go
```

Here **both** kinds of templating apply: the path is rendered (`{{.ProjectName}}` → `my-tool`) and, because the source ends in `.tmpl`, the content is rendered too.

## The `.tmpl` gotcha for path-templated directories

A file that lives under a `{{.ProjectName}}` directory should carry a `.tmpl` extension **even if its content needs no substitution**. The reason is Go tooling, not the scaffold engine: a real `cmd/{{.ProjectName}}/main.go` in this repository's template tree makes `go build ./...` fail, because the Go toolchain tries to read the literal `{` as a package path.

Adding `.tmpl` fixes it two ways at once:

- Go tooling ignores non-`.go` files, so `cmd/{{.ProjectName}}/main.go.tmpl` no longer breaks the build of `agent-init` itself.
- `text/template` parses the file (a no-op when there is nothing to substitute) and the suffix is stripped on write.

So path-templated Go sources are always shipped as `.tmpl`. This is why `go-cli`'s entry point is `main.go.tmpl` rather than `main.go`.

## Relationship to content templating

Path templating always runs; [content templating](./templates.md) only runs for `.tmpl` files. The two are resolved separately during the walk — the path is rendered, then the content is rendered if the suffix calls for it.

## Source

- Path render + the no-`{{` fast path: [scaffold.go:320-333](../../internal/scaffold/scaffold.go#L320-L333) (`renderPath`).
- Where it is applied during the walk: [scaffold.go:209](../../internal/scaffold/scaffold.go#L209), and for `FreshOnlyPaths` / variant matching at [scaffold.go:246](../../internal/scaffold/scaffold.go#L246) and [scaffold.go:181](../../internal/scaffold/scaffold.go#L181).
58 changes: 58 additions & 0 deletions docs/engine/templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Content templating (`.tmpl`)

The scaffold engine substitutes template variables into file **content** only for files whose name ends in `.tmpl`. Every other file is copied byte-for-byte. Renaming a file to `.tmpl` is the explicit opt-in to content substitution; verbatim copy is the safe default.

This opt-in exists because many config files legitimately contain `{{ }}` — Helm charts, Ansible playbooks, GitHub Actions expressions. If the engine templated everything, those files would break or need every brace escaped. Keeping content templating behind the `.tmpl` suffix means those files ship unchanged.

## How it works

`render` ([scaffold.go:305-318](../../internal/scaffold/scaffold.go#L305-L318)) is the whole mechanism:

- If the source path does **not** end in `.tmpl`, the bytes are returned as-is.
- If it does, the content is parsed and executed with Go's [`text/template`](https://pkg.go.dev/text/template) against the scaffold's data.

The `.tmpl` suffix is stripped from the destination path ([walkLayer:201](../../internal/scaffold/scaffold.go#L201)), so `Justfile.tmpl` is written as `Justfile` and `README.agent.md.tmpl` as `README.agent.md`.

## The data model

Templates receive a single field, `templateData` ([scaffold.go:58-60](../../internal/scaffold/scaffold.go#L58-L60)):

| Field | Value |
|-------|-------|
| `{{.ProjectName}}` | The target directory's base name (`filepath.Base(target)`, [scaffold.go:75](../../internal/scaffold/scaffold.go#L75)). Scaffolding into `~/repos/my-tool` yields `my-tool`. |

That is the entire surface. There are no conditionals, ranges, or helper functions used in the shipped templates today; a `.tmpl` file is almost always a plain file with `{{.ProjectName}}` in a comment header or an example identifier.

## Example

`internal/flavors/gocli/templates/README.agent.md.tmpl`:

```markdown
# {{.ProjectName}} — agent notes
```

Scaffolded into `./my-tool`, this renders to `README.agent.md`:

```markdown
# my-tool — agent notes
```

## Escaping a literal `{{` inside a `.tmpl` file

If a `.tmpl` file needs a literal `{{ }}` in its output — for example prose in the `iac` flavor's `AGENTS.md.tmpl` that talks about Ansible's Jinja syntax — escape it with a template action that emits the braces:

```gotemplate
The one literal reference is written as {{"{{"}} var {{"}}"}}.
```

When a whole file is full of native `{{ }}` (an Ansible `site.yml`, a Helm template), don't fight the escaping — leave the file **without** a `.tmpl` extension so it is copied verbatim. The `iac` flavor does exactly this for its playbooks and inventory; see [docs/flavors/iac.md](../flavors/iac.md#template-files-and-the-jinja-gotcha).

## Relationship to path templating

Content templating (this page) and [path templating](./path-templating.md) are independent. A file's **path** is always run through the template engine; its **content** is only rendered when the name ends in `.tmpl`. A file can have a templated path and verbatim content, or the reverse.

## Source

- Content render + the `.tmpl` gate: [scaffold.go:305-318](../../internal/scaffold/scaffold.go#L305-L318) (`render`).
- Suffix strip on the destination path: [scaffold.go:201](../../internal/scaffold/scaffold.go#L201).
- The data type: [scaffold.go:58-60](../../internal/scaffold/scaffold.go#L58-L60) (`templateData`).
Loading