Files
marketplace/cli/internal/scaffold/AGENTS.md
T
naudachu 8b1b11001a feat: drop the kettle plugin; the binary writes its own skills
The plugin and the binary shipped on two release cadences and nothing on an
operator's machine ever checked that the one they installed described the other.
The generated flag block existed precisely so a renamed flag could not ship with
documentation recommending the old one — and then shipped one version behind the
registry it came from, which is the same bug one hop downstream.

So the prose moved into the binary. `internal/scaffold` embeds every document;
`kettle init` and `kettle gen scaffold` write them into a project's own
`.claude/`. The two cannot disagree because there is one artefact.

The namespace survived the move. A project's skills are flat, so the prefix is
spelled into the directory name (`kettle-issue`); a project's *commands* take
their namespace from a subdirectory, so `commands/kettle/init.md` is still
`/kettle:init`. Four of the six command files are thin pointers at a skill, and
that is what kept ~1,600 lines of `/kettle:…` cross-references true without a
rewrite. `init` and `auth` lost `disable-model-invocation: true` — being a
command is that property — and `auth` now restricts `allowed-tools` so a model
cannot reach `kettle auth add` at all.

`gen scaffold` writes files whole rather than splicing a region. The old
refusal protected somebody's hand-written prose around the block; that prose is
embedded now, so there is none to protect, and preserving local edits would
freeze a project's documentation at whatever version first initialized it.
`--check` warns before an upgrade discards one.

The plugin's `agents-sync.sh` — 141 lines of Python behind a filename that said
`.sh` — became `internal/mirror` and `kettle mirror`. Same seven branches, same
refusal to merge two real files that differ, now with a table test per branch
and a check that a repair converges in one pass. `--hook` is the PreToolUse
form and exits 0 on every path including a panic. It is opt-in per project,
which is strictly narrower than the plugin hook that was on for everybody who
installed it.

`kettle init --interactive` walks a person through the login, the token (read
with the echo off, so it lands in no history and no file), the repository, the
`.claude/` tree and the mirror hook. It refuses a stdin that is not a terminal
and names the flags instead: every question it asks has one, and it performs
nothing itself, so an interactive run and a flag run are one code path.

Two rules that used to be prose are now the binary's: init refuses a linked
worktree and names the main checkout, and writing into an existing
`.claude/settings.json` is refused with the snippet printed rather than
reformatting a file the operator commits.

The scaffold version stamp went to its own `.kettle/scaffold.yaml` rather than
into `config.yaml`, because unknown keys there are a hard error and that file
may be committed and read by whatever build each machine has.

golang.org/x/term becomes a direct dependency; it was already in the tree
indirectly, so no module was added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 16:17:24 +05:00

6.0 KiB

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.

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 <!-- kettle:gen --> 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 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:

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.