# AGENTS.md — internal/scaffold **The documents kettle writes into a project**: the slash commands an operator invokes, the skills a model loads, and the runner subagent. Embedded with `//go:embed`, handed out as bytes. **Imports the standard library and nothing else** — and, unusually, is forbidden `os` as well. These documents travel *inside* the binary; one `os.ReadFile` and they are back to being files on a machine that may not have them, which is the entire failure this package exists to make impossible. Two tests hold it, see [`internal/AGENTS.md`](../AGENTS.md). | file | what is in it | |---|---| | `scaffold.go` | `Marker`, `File`, `Files`, `Groups`, `PathFor`, `Dirs`, and the `generated` map | | `assets/` | every document, exactly as it is written out | | `scaffold_test.go` | frontmatter, names, the region declaration, determinism | | `layering_test.go` | the two tests that keep this package at the bottom | ## Why these are not a plugin They were, and the plugin shipped on its own release cadence. **Nothing on an operator's machine ever checked that the plugin they had installed described the binary they had installed** — so a renamed flag could ship with documentation recommending the old one, which is the exact failure the generated block was invented to prevent, one hop further downstream. Prose that travels inside the binary cannot be a version behind it. That is the whole argument, and everything else here follows from it: the generator writes files whole rather than splicing a region, `kettle init` writes the tree, and `kettle config` reports which build wrote the one on disk. What was lost is worth naming. The marketplace was discovery — `/plugin` is a shop window and a README URL is not — and a plugin's `/kettle:…` prefix came free where a project's does not. The second was recoverable and is recovered below; the first was not, and the trade was made anyway, because a document that describes the wrong binary is worse than one nobody found. ## The layout, and where the namespace went ``` commands/kettle/*.md → /kettle:init, /kettle:auth, /kettle:issue, … skills/kettle-*/SKILL.md → loaded by description, never by name skills/kettle-issue/references/format.md agents/kettle-runner.md ``` A project's skills have **no namespace**: `.claude/skills/` is flat, and a skill called `issue` would collide with every other tool's. So the prefix is spelled into the directory name — `kettle-issue`, not `issue` — and the `name:` in the frontmatter matches it, because the harness resolves one from the other. **Commands do have a namespace, and it is free.** A subdirectory under `.claude/commands/` becomes the prefix, so `commands/kettle/init.md` is `/kettle:init` — the same spelling the plugin had, with no plugin. Every `/kettle:…` in the embedded prose is still true because of this, which is why four of the six command files exist at all: `issue`, `sync`, `api` and `project` are three lines each, pointing at the skill that holds the substance. They are not duplication; they are the reason 1,600 lines of cross-references did not have to be rewritten. **`init` and `auth` are commands with no skill.** That is not an omission. They were skills carrying `disable-model-invocation: true` — a frontmatter flag saying "an operator invokes this, not a model". A command *is* operator-invoked, so the flag disappeared into the shape of the thing. `init` in particular must not be routable: which directory is the project is the one question this binary exists to have a person answer. ## The generated region Four documents carry `` markers around a flag table rendered from the command registry. Which four is declared in the `generated` map, and declared rather than derived from the directory name — the group ⇄ skill correspondence is a decision, and one that has never been one-to-one: `project` is a single skill covering five commands, and `init` and `auth` are commands inside it with no skill of their own. `scaffold_test.go` asserts the map and the markers agree in both directions, and [`internal/cmd`](../cmd/AGENTS.md) asserts every group in the registry is named here. A group added without a document would have its commands silently written nowhere — the generator walks the documents, not the registry. **This package does not render.** It hands out embedded bytes and says which of them carry a region; the splice is `internal/cmd`'s, because the registry is. The split is what keeps the import arrow pointing one way. ## Bootstrapping, which is circular and is meant to be `assets/` holds the block already spliced in — the files here are what a project gets, byte for byte. So: ```bash make check # gen scaffold --check, against assets/ dist/kettle gen scaffold --out internal/scaffold/assets # the fix ``` A flag change makes the render differ from the embedded copy, `--check` exits 1, regenerating updates `assets/`, and the binary has to be **rebuilt** to pick the new bytes up. That loop is the same one the plugin's SKILL.md files were in; what changed is that its output now ships with the binary rather than beside it. ## Editing a document Edit it here, under `assets/`, then `make check`. Never edit the copy in a project: it is replaced whole on the next `kettle gen scaffold`, and the run says so before it does it. The one line no generator may write is `description:` — it is what decides whether a model loads a skill at all, and it is prose tuned against real failures. `scaffold_test.go` fails on an empty one; nothing checks that it is *good*, and nothing can. ## Keeping this file true - **Scope:** `scaffold.go`, everything under `assets/`, and the two seams — the `generated` map, and the commands ⇄ skills split. - **Update it when** a document is added or removed, a group gains or loses its file, the output layout changes, or the argument for embedding rather than shipping a plugin stops being the argument. - **Do not** restate what any individual document says. Each one is its own procedure and says it in full.