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>
This commit is contained in:
naudachu
2026-08-12 16:17:24 +05:00
parent f18a633185
commit 8b1b11001a
445 changed files with 231172 additions and 1339 deletions
@@ -0,0 +1,273 @@
---
name: kettle-project
description: Generated flag reference for the project-level `kettle` commands — `kettle init`, `kettle auth`, `kettle config`, `kettle gen`, `kettle mirror`. Load it to look up the exact flags and defaults of one of those, or when a command answers "no project" / "no login" and you need `kettle config` to say what this directory resolved to. The rules around initializing are /kettle:init and the credential workflow is /kettle:auth; this file is the flag table both of them point at.
---
# kettle project — the project itself
`kettle` resolves everything from one marker. `<project>/.kettle/` is created by
`kettle init` and never inferred: `.git` is in every clone, so a tool that
guessed a root from one would write issues into whatever tree it happened to be
standing in. With no marker anywhere the command stops and names the directories
it searched — that is an answer, not a fallback.
Two configuration files, and the split is the point. `<project>/.kettle/config.yaml`
holds the tracker repository and the **name** of a login; the name is worth
nothing on its own, which is what makes it safe inside a working tree.
`~/.config/kettle/logins.yaml` (0600, one per machine, `$KETTLE_CONFIG_HOME` or
`$XDG_CONFIG_HOME` move it) holds the tokens. `KETTLE_LOGIN`, `KETTLE_REPO`,
`KETTLE_URL` and `KETTLE_TOKEN` each override the file they shadow.
**No `kettle` on PATH?** `command not found: kettle` is the whole story — no
script and no other CLI substitutes for it, and it means the binary that wrote
this file has since moved or been removed, because nothing else could have put it
here. Stop and tell the operator to reinstall it:
`go install git.noodles.cam/claude-skills/marketplace/cli/cmd/kettle@latest`, or
`cd cli && make install` in a checkout of that repository (go.mod requires
**go 1.26**).
**This file is written by the binary and is replaced whole on the next
`kettle gen scaffold`.** Every word of it — the prose as much as the flag table
below — is embedded in the `kettle` that wrote it, which is why the two cannot
disagree about a flag. Editing it here changes nothing durable; the fix for a
wrong sentence is a newer `kettle`, and the fix for a stale one is
`kettle gen scaffold`. `kettle config` says which version wrote this tree.
<!-- kettle:gen -->
**Generated from the kettle command registry by `kettle gen scaffold`.** Everything between the two markers is replaced on the next run — the prose around it is embedded in the binary and replaced with it.
## `kettle auth list | add | remove <name>`
manage the tokens this machine holds
Credentials live in one file per machine, outside every working tree, mode
0600. A project pins a login by NAME; the name is worth nothing on its own,
which is what makes it safe to keep in a file inside the repository.
The token is read from standard input unless --token is given, because an
argument is in the shell history the moment it is typed:
kettle auth add --name noodles --url https://git.example.com < token.txt
pass show gitea/token | kettle auth add --name noodles --url https://git.example.com
`list` never prints a token. There is no flag to make it.
--scopes RECORDS WHAT THE TOKEN WAS MINTED WITH, and records is all it does:
nothing is checked against it and nothing is refused because of it. It is worth
writing down because the instance will not answer the question — Gitea's own
token listing needs a password, not a token, so a token cannot be asked what it
may do. Gitea spells them <read|write>:<category>; issues need `write:issue`,
and everything `kettle api` reaches outside issues — releases, pull requests,
branches, tags, actions — is `repository`. A token minted for issues alone
answers 403 there, and the 403 names no scope.
| flag | default | what it does |
| --- | --- | --- |
| `--name` | — | login name (add) |
| `--scopes` | — | what the token was minted with, comma separated, e.g. write:issue,write:repository; documentation only (add) |
| `--token` | — | token, if you would rather not use stdin (add) |
| `--url` | — | instance URL, e.g. https://git.example.com (add) |
| `--user` | — | account this token belongs to; documentation only (add) |
```bash
kettle auth list # what this machine holds
pass show gitea | kettle auth add --name noodles --url https://git.example.com # add one, token on stdin
kettle auth add --name noodles --url https://git.example.com --scopes write:issue,write:repository < t.txt # and write down what it can do
kettle auth remove noodles # forget it
```
## `kettle config`
show what this project resolved to
Every path and every setting, with the overrides already applied, so a run that
went somewhere unexpected can be explained without guessing.
The token is never printed — only whether one was found.
This is the command to reach for when the store looks empty, when a push says
401, or when two directories disagree about which project they are in.
```bash
kettle config # resolved paths and settings
```
## `kettle gen scaffold`
write this project's .claude/ commands, skills and subagent
A skill tells an agent how to invoke this binary, and a command is how an
operator invokes one by hand. Both are written from here, whole, because both
travel INSIDE the binary: the prose is embedded next to the code it describes
and the flag tables are rendered from the command registry the binary is built
from, so neither can be a version behind the other.
That is the whole reason these documents are not a plugin any more. A plugin
ships on its own cadence, and nothing on an operator's machine ever checked that
the one they installed described the binary they installed — so a renamed flag
could still arrive with documentation recommending the old one, which is exactly
the failure the generated block was invented to prevent, one hop further
downstream.
EVERY FILE IS WRITTEN WHOLE, and that is a deliberate reversal. The old
generator owned a region and left every byte outside it alone, because the prose
around the block was somebody's hand-written file. It is not any more: it is
embedded, so there is no hand-written half left to protect, and preserving local
edits would mean freezing a project's documentation at whatever version first
initialized it. The markers stay in the output so a reader can see which half
came from the registry.
WHAT THIS MEANS FOR A LOCAL EDIT: it does not survive. Run --check before an
upgrade if you have made one; the fix for a sentence that is wrong is a newer
kettle, not a patch that the next run silently discards.
The output is deterministic to the byte — no timestamps, no map iteration — so
regenerating something that has not changed produces no diff. --check is that
property made useful: it writes nothing and exits 1 when any file on disk
differs from what would be written, which is what a pre-commit hook or a CI step
calls. It wins over --dry-run when both are given.
| flag | default | what it does |
| --- | --- | --- |
| `--check` | `false` | write nothing, exit 1 if anything is out of date |
| `--dry-run` | `false` | print what would change; write nothing |
| `--out` | — | directory to write into (default: <project>/.claude) |
```bash
kettle gen scaffold # write .claude/ under this project
kettle gen scaffold --out ~/code/x/.claude # write it somewhere else
kettle gen scaffold --dry-run # print what would change; write nothing
kettle gen scaffold --check # exit 1 if the documents are out of date
```
## `kettle init`
make this directory a project that tracks issues
Creates `.kettle/` — the marker every other command resolves the store from,
and `.kettle/config.yaml`, which says which tracker repository these issues
belong to and which login to reach it under — and writes `.claude/`: the slash
commands an operator invokes, the skills a model loads, and the runner subagent.
The marker is deliberately something an operator makes, not something inferred
from the tree: `.git` is in every clone, so anything that inferred a root from
one would write issues into whatever it happened to be installed in.
--login pins a name, never a credential. The tokens live in one file per
machine, outside every working tree, managed with `kettle auth`.
All of it is idempotent: it creates .kettle/issues and .kettle/payload, migrates
an older store in if it finds one (either layout the tea plugin used, oldest
first), writes the config without disturbing settings it was not given, writes
the .claude/ tree, and adds .kettle/ to .gitignore. Each migration is a move,
not a copy — two stores is the state the marker exists to prevent — and it
refuses to pick a winner when both sides hold a file of the same name.
IT REFUSES TO RUN IN A LINKED WORKTREE, and names the main checkout instead. A
worktree is the same project on another branch and reaches the store by a hop
out to the main checkout; a marker here would give one project two stores, and
the directory holding the second one disappears with the branch.
--interactive walks a person through the whole thing — the login, the token with
the echo turned off, the repository, the .claude/ tree and the AGENTS.md mirror
hook. IT REQUIRES A TERMINAL and refuses a standard input that is not one, which
is deliberate: every question it asks has a flag beside it, so nothing that is
not a person ever needs to answer a prompt.
| flag | default | what it does |
| --- | --- | --- |
| `--at` | — | directory to initialize (default: the working directory) |
| `--dry-run` | `false` | report what would happen; change nothing |
| `--force-settings` | `false` | let the hook be merged into an existing settings.json, reformatting it |
| `--interactive` | `false` | ask, one question at a time; requires a terminal |
| `--login` | — | name of a login in the machine-wide file (see `kettle auth`) |
| `--mirror-hook` | `false` | register `kettle mirror --hook` on PreToolUse(Bash) |
| `--no-scaffold` | `false` | do not write the .claude/ commands, skills and subagent |
| `--repo` | — | tracker repository, as owner/name |
| `--scaffold-out` | — | where the .claude/ tree goes (default: <project>/.claude) |
```bash
kettle init # initialize the current directory
kettle init --interactive # be walked through it, at a terminal
kettle init --login noodles --repo claude-skills/marketplace # and point it at a tracker
kettle init --mirror-hook # register the AGENTS.md mirror on PreToolUse(Bash)
kettle init --at ~/code/x # initialize somewhere else
kettle init --dry-run # say what it would do, touch nothing
```
## `kettle mirror [<dir>]`
keep CLAUDE.md a symlink to AGENTS.md in every directory below here
Two agent harnesses read two different filenames for the same document. A
repository that keeps both as real files keeps TWO DOCUMENTS, and they drift —
silently, until somebody reads the stale one and believes it. This walks a tree
and leaves one arrangement behind everywhere:
AGENTS.md is the real file; CLAUDE.md is a symlink pointing at it.
The link is relative, so a tree that is moved, copied or cloned keeps working.
AGENTS.md is the real one because the convention is not one vendor's: a
repository that names its documents after a single tool has picked a side it did
not need to pick.
NOTHING HERE DELETES CONTENT. Six of the seven states it can find are repaired
losslessly — a missing link is created, a reversed layout is swapped round, a
duplicate whose bytes match its original is replaced by the link. The seventh,
two real files whose contents DIFFER, is reported and left exactly as it was:
one of them is somebody's writing and no rule here knows which.
It walks the directory given, or the working directory. node_modules, vendor,
venv, __pycache__ and every dot-directory are skipped, because somebody else's
tree is somebody else's business.
--hook is the PreToolUse form: it reads the hook payload on standard input,
writes any report back as additionalContext, and ALWAYS EXITS 0 — including when
it fails. A tool that broke somebody's Bash call because its documentation
helper crashed would be worse than no tool. --check is the opposite end: it
writes nothing and exits 1 when the tree is not canonical, which is what a
pre-commit hook or a make target calls.
`kettle init --interactive` offers to register the --hook form in
.claude/settings.json. It is offered rather than assumed: this is one
repository's documentation convention, and a project that does not keep AGENTS.md
files wants nothing to do with it.
| flag | default | what it does |
| --- | --- | --- |
| `--check` | `false` | write nothing, exit 1 if the tree is not canonical |
| `--hook` | `false` | PreToolUse form: payload on stdin, report as additionalContext, always exit 0 |
| `--quiet` | `false` | repair without printing what was repaired |
```bash
kettle mirror # repair the working directory and everything below it
kettle mirror ~/code/x # repair somewhere else
kettle mirror --check # exit 1 if anything is out of place; write nothing
kettle mirror --hook # the PreToolUse form; reads a payload, always exits 0
```
## `kettle version`
print the version this binary was built as
A binary that cannot say what it is, is a support problem: an operator with an
old copy on PATH and a skill written against a newer one has no way to see the
mismatch, and neither does anybody reading their transcript.
The version is stamped at link time. A build from source says "dev" and means
it — that is not a placeholder to be edited, it is the answer for a binary that
came out of somebody's working tree rather than off a tag.
The commit is reported when the build recorded one, which `go build` does from
git and a build from an unpacked tarball cannot. A tree with uncommitted
changes in it says so beside the commit.
| flag | default | what it does |
| --- | --- | --- |
| `--short` | `false` | print the version alone, with nothing around it |
```bash
kettle version # the version, the toolchain and the commit
kettle version --short # just the version, for a script
```
<!-- /kettle:gen -->