Files
marketplace/README.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

16 KiB

claude-skills — the kettle binary, and a plugin marketplace

One repository holding two things: kettle, a Go binary that keeps a project's issues as local markdown and syncs them with Gitea, and a small Claude Code plugin marketplace with one plugin in it.

Installing kettle

kettle is one binary and it installs itself into your project. There is no plugin to add — cli/go.mod requires Go 1.26, and the first build downloads eight modules, verified against go.sum:

go install git.noodles.cam/claude-skills/marketplace/cli/cmd/kettle@latest

# or from a clone of this repository, version stamped:
cd cli && make install                          # ~/.local/bin/kettle
cd cli && make install BINDIR=$(go env GOPATH)/bin

Put the target directory on your PATH and check with kettle version. Then, in a project you want to track issues in:

kettle init --interactive

That walks you through the login, the token (typed with the echo off, so it lands in no history and no file), the tracker repository, and the .claude/ tree — which is where the slash commands and skills come from:

.claude/commands/kettle/*.md      /kettle:init /kettle:auth /kettle:issue
                                  /kettle:sync /kettle:api /kettle:project
.claude/skills/kettle-*/          loaded by description, never by name
.claude/agents/kettle-runner.md   the batch runner, on a cheap model

Every flag table in those documents is rendered from the binary's own command registry, and the prose around it is embedded in the same binary. They cannot describe a version of kettle other than the one that wrote them — which is the whole reason this stopped being a plugin. kettle config tells you when the tree on disk was written by an older build; kettle gen scaffold refreshes it.

Prefer flags to prompts? Everything --interactive asks has one:

kettle init --login noodles --repo owner/name --mirror-hook
kettle init --no-scaffold                       # marker and config only

The marketplace

/plugin marketplace add https://git.noodles.cam/claude-skills/marketplace.git
/plugin install tdl@claude-skills
Plugin Commands What it does
tdl /tdl:audit Three Dots Labs Go conventions as an enforceable rule set — audits a Go project against nine categories of CQRS/DDD/Clean-Architecture rules by severity, or scaffolds services, handlers, entities, repositories and Watermill adapters from templates that already follow them

The kettle CLI

A single Go binary that keeps a project's issues as flat markdown files and moves them to and from Gitea when you say so — and, with kettle api, reaches every other Gitea entity under the same login, so nothing here needs a second CLI. Everything outside the sync and api groups works with the network off. Two direct dependencies, vendored, no cobra, seven internal packages with one direction of imports — see cli/AGENTS.md.

What it does

project — the project itself

command what it does
kettle init makes this directory a project: creates the .kettle/ marker every other command resolves the store from, migrates an older store in, gitignores it
kettle auth manages the tokens this machine holds — one file, mode 0600, outside every working tree
kettle config prints every path and setting a run resolved to, with overrides applied. Never prints a token
kettle gen scaffold writes the .claude/ commands, skills and subagent, prose and flag tables both, from inside this binary
kettle mirror keeps CLAUDE.md a symlink to AGENTS.md in every directory below here. --hook is the PreToolUse form and never fails a call
kettle version what this binary was built as — dev from a working tree, the tag when a release build stamped it in

issue — units of work, offline, no tracker involved

command what it does
kettle new creates a local issue from its type template. origin: local is a complete state, not a draft
kettle check validates issues against the canonical format. Exit 1 on errors, so a hook or a CI step can call it
kettle ac lists and ticks an issue's checkboxes — one byte of the file changes, so the diff is the state that changed
kettle tree draws the dependency graph read off depends:
kettle index rebuilds INDEX.md from what is on disk, progress counted from the bodies
kettle evict removes closed issues that also live in a tracker. A local one is never evicted, in any state

sync — moving issues between the store and the tracker

command what it does
kettle pull fetches issues by address (42, #42, owner/repo#42, a URL) or by filter, with their blockers, comments included
kettle push sends local issues up, blockers first, and deletes the local copy once the tracker confirms it
kettle remote lists what exists in the tracker, one line each. Writes nothing
kettle comment posts or edits a comment, body from a file or inline, then refetches the whole thread
kettle close closes or reopens issues in the tracker, and on disk with them. State only
kettle labels creates the canonical type/* and severity/* labels in a repository, exclusive flag and all
kettle sync-evict refreshes state from the tracker first, then evicts what is closed

api — everything else Gitea has, reached directly

command what it does
kettle api one request to an endpoint this binary has no command for — pull requests, releases, tags, milestones, branches, actions, webhooks — under the login the project already pins. One invocation is one request; it does not paginate, does not reformat the answer, and -X DELETE needs --yes

kettle help prints the tree; kettle help <command> prints one command in full — flags, defaults and worked examples. Flags may be typed after positional arguments.

Usage

Once per project

kettle auth add --name noodles --url https://git.example.com < token.txt
kettle init --login noodles --repo owner/name
kettle config                      # what did that actually resolve to?

The token is read from standard input because an argument is in the shell history the moment it is typed. What lands in the repository is the login's name; the tokens stay in ~/.config/kettle/logins.yaml.

Working offline

kettle new --type task --title "Wire sqlc into the appclick repo layer" \
           --label tech/sql --label comp/appclick
kettle new --type bug  --title "Fix the index rebuild on an empty store" \
           --depends wire-sqlc-appclick --milestone v0.2

kettle check                       # every issue in the store
kettle check --strict              # warnings count as errors
kettle ac wire-sqlc-appclick       # numbered checkboxes with their state
kettle ac wire-sqlc-appclick --check 3
kettle tree --depth 2              # every root, two levels down
kettle index

No login, no network and no tracker for any of that. The store is flat markdown with one metadata field per line, so plain grep works without a parser:

grep -l  'labels:.*type/bug'        .kettle/issues/*.md
grep -ln 'depends:.*migrate-schema' .kettle/issues/*.md   # who depends on it

The tracker

kettle labels --dry-run            # print the plan; not one writing request
kettle labels                      # create whatever is missing

kettle push                        # everything the tracker lacks, blockers first
kettle push --update wire-sqlc-appclick
kettle push --dry-run              # validate and print the plan; no network

kettle remote --state all --label type/bug --limit 50
kettle pull 42                     # the issue and everything blocking it, any state
kettle pull 42 --no-deps           # just that one — one request
kettle pull owner/repo#42           # an issue in another repository
kettle pull --milestone v0.2 --limit 20

kettle comment wire-sqlc-appclick --file notes.md
kettle close 42 43
kettle close --reopen 42
kettle sync-evict --dry-run

The rule worth knowing before you push

The store holds what has not left this machine. A successful push deletes the local file — on --update too, one rule with no exception — and only after the tracker confirms the write. Get it back with kettle pull <n>: it lands under the same slug, with the same depends:, even on a machine that has never seen it, because the slug travelled up in the body as a marker and was recorded in a local number → slug ledger.

A never-pushed origin: local issue is the only copy of that work, and nothing deletes it — in any state, not even when it is named on the command line.

Configuration

file holds notes
<project>/.kettle/config.yaml login: (a name) and repo: written by kettle init; safe inside a repository because a name is worth nothing on its own
~/.config/kettle/logins.yaml the tokens one file per machine, mode 0600, outside every working tree

KETTLE_LOGIN, KETTLE_REPO, KETTLE_URL, KETTLE_TOKEN and KETTLE_CONFIG_HOME each override the file they shadow — for CI, and for anyone who would rather have no token on disk at all. An unknown key in either file is an error, not a silent drop: an older binary would otherwise delete the setting it did not recognize the next time it wrote the file.


Layout

.claude-plugin/
  marketplace.json           the catalog — one entry per plugin, source is a
                             path into plugins/
cli/                         one Go module, two binaries
  Makefile                   the gate and the release: make check | dist | release
  cmd/kettle/                what people install
  cmd/release/               what publishes this repository's own releases
  internal/                  seven packages, one direction of imports
plugins/
  kettle/
    .claude-plugin/plugin.json
    agents/ hooks/ skills/
    README.md  AGENTS.md
  tdl/
    .claude-plugin/plugin.json
    skills/audit/{references,templates}

A plugin's root is its directory under plugins/, so ${CLAUDE_PLUGIN_ROOT} resolves inside it and every path a plugin uses stays relative to itself. Adding a plugin means adding a directory here plus one entry in marketplace.json — nothing else in the repo needs to know about it.

cli/ is deliberately not inside a plugin: a binary is installed on a machine, while a plugin is a directory Claude Code loads, and collapsing the two is what put an earlier version's issue store inside a versioned plugin cache.

Development

There is no CI. The instance this lives on has no Actions runner and none is planned, so nothing runs on a push, nothing checks a branch, and nothing will tell you afterwards. make check is the whole gate, it takes about ten seconds, and it is on whoever is committing to run it:

cd cli && make check

Five steps in one command, non-zero the moment any of them fails:

step what it holds down
gofmt -l cmd internal nothing in the tree is unformatted. It reports and never rewrites — a formatting change made silently by a build is a change nobody reviewed
go vet ./... the compiler's own second opinion
go test -count=1 ./... the whole suite, with the test cache defeated: a gate that can pass because it passed an hour ago on different code is not a gate
go mod verify + go build -mod=vendor ./... every module matches its hash in go.sum, and the committed vendor/ is complete and is what compiles. A vendor/ that has drifted from go.mod breaks nobody until somebody builds with a cold module cache
kettle gen scaffold --check the documents embedded in the binary still agree with the registry it is built from. Exit 1 the moment they do not, and dist/kettle gen scaffold --out internal/scaffold/assets is the fix

That last one is circular on purpose. What a project gets is embedded in the binary with the registry's flag tables already spliced in, so the embedded copy has to hold what the current registry would produce. Change a flag and it goes stale; regenerate, then rebuild, because the binary has to pick the new bytes up.

Where the design notes are

Every directory with a story carries its own AGENTS.md, starting at the repository root and recursing into cli/, each of its packages, and each plugin. A CLAUDE.md beside one is a generated symlink to it, and kettle mirror keeps that arrangement honest — six of the seven states it can find are repaired losslessly, and the seventh (two real files whose contents differ) is reported and left alone, because one of them is somebody's writing. Register it with kettle init --mirror-hook if you want it run before every Bash call. Read the file next to the code you are changing rather than the whole tree.

make help lists the rest — build, install [BINDIR=…], dist, clean. make install puts a version-stamped binary in ~/.local/bin (override with BINDIR=$(go env GOPATH)/bin) and says so if that directory is not on your PATH, which is the whole of what a skill means by command not found: kettle.

Design notes live next to the code they describe: every directory with a story carries its own AGENTS.md, starting at the repository root and recursing into cli/, each of its packages, and each plugin. A CLAUDE.md beside one is a generated symlink to it. Two hooks in the kettle plugin keep the arrangement honest — one repairs the layout before every Bash call, the other asks for the nearest AGENTS.md to be corrected after a file is written.

Cutting a release

Releases are cut by hand, from a developer's machine, and published by this repository's own code. cli/cmd/release is a second binary in the same module that talks to Gitea's API through the same SDK kettle uses: no third-party action, and nothing between a tag and what people download that is not in this tree. It is deliberately not a kettle subcommand — kettle is a tool for issues, and its command tree is what generates the documents it writes into a project.

cd cli
make check                          # nothing else is going to run this for you
git tag -a v0.2.0 -m v0.2.0         # on the commit you mean to ship
git push origin v0.2.0
export KETTLE_URL=https://git.noodles.cam KETTLE_TOKEN=# or run it from an
                                    # initialized project and it reads the
                                    # login pinned there
make release TAG=v0.2.0 NOTES=../notes-v0.2.0.md TITLE="kettle v0.2.0"
step what it verifies
make check the five things above. A release is the worst place to find out
git tag / git push the tag exists, and it exists on the server. A release naming a tag nobody else can fetch is a download page pointing at nothing
make release refuses a dirty working tree — what shipped would not be what is committed, and nobody could rebuild it; refuses a TAG that is not what git describe reports — which is only true when the tag exists and HEAD is standing on it, so it also catches "I tagged, then committed one more thing"; and refuses a tag that is not on the remote
the dist it runs first kettle cross-compiled for darwin and linux on amd64 and arm64, CGO_ENABLED=0, each stamped with the tag via -ldflags, plus a SHA256SUMS over all four
the publish creates the release or reuses the one already there, uploads each artifact, replaces an asset of the same name rather than doubling it, and prints the release URL with every asset under it

Re-running it is safe, and that is the point. An upload that died half way through is fixed by running the same command again: the tag's release is reused, the assets that made it are replaced by the freshly built ones, and corrected notes actually land. You get one release and one copy of each asset either way.

kettle version on a downloaded binary reports the tag it was built from; a binary somebody built out of a working tree says dev and means it.