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:
@@ -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 -->
|
||||
Reference in New Issue
Block a user