diff --git a/docs/README.md b/docs/README.md index c5fc634..7b2c186 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,7 +15,7 @@ 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/ @@ -23,11 +23,11 @@ docs/ ├── 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) @@ -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 @@ -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/.md` entry. See [`.claude/skills/add-flavor/SKILL.md`](../.claude/skills/add-flavor/SKILL.md). diff --git a/docs/engine/common-overlay.md b/docs/engine/common-overlay.md new file mode 100644 index 0000000..14dfe68 --- /dev/null +++ b/docs/engine/common-overlay.md @@ -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//` 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`). diff --git a/docs/engine/done-gate.md b/docs/engine/done-gate.md new file mode 100644 index 0000000..d5911a0 --- /dev/null +++ b/docs/engine/done-gate.md @@ -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 `✗ 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 '' (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//templates/`. diff --git a/docs/engine/flavor-hooks.md b/docs/engine/flavor-hooks.md index d442b5f..ff800cc 100644 --- a/docs/engine/flavor-hooks.md +++ b/docs/engine/flavor-hooks.md @@ -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 diff --git a/docs/engine/path-templating.md b/docs/engine/path-templating.md new file mode 100644 index 0000000..fe9bd30 --- /dev/null +++ b/docs/engine/path-templating.md @@ -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). diff --git a/docs/engine/templates.md b/docs/engine/templates.md new file mode 100644 index 0000000..a9d1501 --- /dev/null +++ b/docs/engine/templates.md @@ -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`).