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>
6.3 KiB
AGENTS.md — the repository root
One repository holding the kettle binary and a Claude Code plugin
marketplace with one plugin left in it. Three things live here and nothing else
does:
cli/ the kettle binary — Go, no cobra, 9 packages
plugins/ one directory per plugin
.claude-plugin/marketplace.json the catalog: one entry per plugin
Where to go from here, and each of these directories documents itself:
| directory | what it is |
|---|---|
cli/ |
one Go module, two binaries: kettle, which owns this project's connection to its tracker — the format, the store, the credentials, the transport, through kettle api every Gitea entity that has no command of its own, and the skills and commands it writes into a project's .claude/ — and cmd/release, which publishes this repository's own releases. make check is the gate; there is no CI here |
plugins/ |
what a plugin is here, and what the catalog entry has to match |
plugins/tdl/ |
the Three Dots Labs Go rule set — no binary, no state, just rules and templates |
The one architectural fact worth carrying
A binary holds what can be enforced; prose holds what can only be stated. Anything mechanical belongs in Go where a test can hold it down; anything that is a judgement an operator makes belongs in a SKILL.md.
There used to be a second half to that sentence — and a plugin holds the prose —
and it is gone, because it was wrong in a way that took a year to show. kettle
shipped as a plugin wrapping a binary, on two release cadences, and nothing on
an operator's machine ever checked that the plugin they installed described the
binary they installed. The generated flag block existed precisely so a renamed
flag could not ship with documentation recommending the old one, and it shipped
one version behind the registry it came from — the same bug, one hop downstream.
So the prose moved into the binary. cli/internal/scaffold embeds it,
kettle init and kettle gen scaffold write it into a project's own .claude/,
and the two cannot disagree because there is only one artefact. What a project
gets:
.claude/commands/kettle/*.md /kettle:init, /kettle:auth, /kettle:issue, …
.claude/skills/kettle-*/ loaded by description, never by name
.claude/agents/kettle-runner.md the batch runner, on a cheap model
The same rule made kettle api (one door for every request, rather than
requiring tea and its second set of invisible logins) and kettle mirror
(below). Each time, something mechanical was sitting where no test could reach
it, and each time it came into Go.
The AGENTS.md convention
Every directory with a story documents itself, in that directory. This file
is a map, not a manual — it says what lives where and sends you down. A reader
who opens cli/internal/gitea/ gets the transport's rules from
cli/internal/gitea/AGENTS.md and does not have to load the whole repository's
design to change one request.
Three rules make that work:
AGENTS.mdis the real file;CLAUDE.mdbeside it is a symlink to it.kettle mirrorenforces that and repairs any directory that drifted — it renames, re-points and swaps, and it never deletes content. Two real files with different content is the one case it refuses to resolve and reports instead.CLAUDE.mdis gitignored, because it is generated.- A directory's file describes that directory only. What a parent or a child
owns gets a link, never a second copy — the copy is what goes stale. If a
sentence is true of the whole binary it belongs in
cli/AGENTS.md; if it is true of one package it belongs in that package's file. - Every file ends with its own maintenance contract — the Keeping this file true section. It names the files the document covers and what kind of change obliges an edit.
What keeps them true
Nothing automatic, and that is a choice. Keeping these files honest is the job of whoever changes the code they describe, which is what the Keeping this file true section at the bottom of each one is for.
A hook that nagged after every write was written and then removed: it would have
fired for every user of the kettle plugin, on every edit in every repository
they touched, to enforce a documentation convention that is this repository's and
nobody else's. That argument survived the plugin. kettle mirror --hook is
what stayed, and it is narrower on both axes: it repairs the filesystem layout
rather than asking anybody for anything, and it is opt-in per project —
kettle init --mirror-hook, or one question in kettle init --interactive.
It cannot fail a tool call; it exits 0 on every path, including its own bugs,
because documentation maintenance is not permitted to break a build.
This repository registers it in .claude/settings.json, which is the one thing
under .claude/ that is not gitignored here — a fresh clone should get the
convention without anyone remembering to wire it up.
Development
cd cli && make check # fmt, vet, test, go mod verify, build, docs — the gate
cd cli && make help # install, dist, release
There is no CI on the instance this lives on, so make check is the only thing
between a mistake and the tracker, and it is on whoever is committing to run it.
Its last step is the repository's one mechanical documentation invariant, and it
is circular on purpose: the documents an operator will be given are embedded in
the binary with the registry's flag tables already spliced in, so a flag change
makes them stale and gen scaffold --check exits 1. Regenerate, then rebuild —
the binary has to pick the new bytes up.
Everything else in this tree — including every AGENTS.md — is prose, held true
by kettle mirror and by whoever is editing.
Keeping this file true
- Scope: the repository layout, the binary/prose split, and the AGENTS.md convention itself. Every deeper subject belongs to a deeper file.
- Update it when a top-level directory appears or goes, a plugin is added or removed, the marketplace catalog changes shape, or the mirror hook's behaviour changes.
- Do not put a command reference, a package's rules, or a skill's procedure here. Link to the file that owns it.