# kettle — Claude Code plugin for issues as local markdown Issues are units of work first and tracker rows second. `kettle` keeps them as flat markdown files in your project, works on them entirely offline, and moves them to and from Gitea when you decide to — never before. The plugin is a thin wrapper. The work is done by the **`kettle` binary**, which lives in this same repository under [`cli/`](../../cli); the skills carry the rules and procedures a binary cannot state, and their command reference is generated from the binary's own command registry, so the docs cannot drift from the tool. ## What it ships | Piece | What it does | |---|---| | `/kettle:init` skill | Makes a directory a project: creates the `.kettle/` marker every command resolves the store from. Once per project, and only you can run it | | `/kettle:auth` skill | The credential workflow — what this machine holds, and which login this project runs under | | `/kettle:project` skill | Generated flag reference for `init`, `auth`, `config`, `gen` | | `/kettle:issue` skill | Issues as units of work — create, read, grep, validate, tick, evict, walk the dependency graph. Entirely offline | | `/kettle:sync` skill | Moves issues between the local store and Gitea — pull, push, comment, close, evict | | `/kettle:api` skill | Everything else Gitea has, through `kettle api` and the same login: pull requests, releases, tags, milestones, branches, actions, webhooks, notifications | | `kettle-runner` agent | Subagent on Haiku that runs batches of commands and reports back a receipt — the mechanical half, off your main context | ## Prerequisites - **Claude Code** — CLI, desktop app, or IDE extension. - **The `kettle` binary, on your `PATH`.** It is not installed for you, and nothing here works without it — see below. - **Python 3** — the `agents-sync` hook is a Python script despite its `.sh` name; `python3` must be on `$PATH`. Nothing else here needs it. There is **no second CLI to install** — `kettle api` reaches every Gitea entity this plugin has no command for, under the login the project already pins. ### Installing the binary Building it needs **Go 1.26** — `cli/go.mod` says so because the Gitea SDK requires it. `vendor/` is committed, so a build compiles exactly what is in this repository's history. ```bash # from a clone of this repository cd cli && go build -o ~/.local/bin/kettle ./cmd/kettle # or, without cloning go install git.noodles.cam/claude-skills/marketplace/cli/cmd/kettle@latest ``` Make sure the target directory is on your `PATH` (`go install` uses `$(go env GOPATH)/bin`). Check it with: ```bash kettle help ``` If a skill ever answers `command not found: kettle`, that is the whole diagnosis: the binary is missing. Install it and run the command again — the skills say so rather than falling back to something that half-works. ## Installation This is a Claude Code plugin — install it through the plugin marketplace, not by hand-editing `settings.json`. 1. Register the marketplace this plugin ships in: ``` /plugin marketplace add https://git.noodles.cam/claude-skills/marketplace.git ``` Already have a local clone? Point at the directory instead: ``` /plugin marketplace add /path/to/marketplace ``` 2. Install the plugin: ``` /plugin install kettle@claude-skills ``` The skills load immediately. Use `/plugin` to enable, disable, or update it later. > The marketplace registration is written to `extraKnownMarketplaces` and the > plugin to `enabledPlugins` in your settings automatically — you don't edit > those by hand. There is **no** top-level `"plugins"` settings key; if you've > added one from older instructions, remove it. ## First use Run `/kettle:init` once per project. It creates the `.kettle/` marker that says *this* directory is the project whose issues live in it — every command resolves the store by walking up to the nearest one, and with no marker anywhere they stop and name the directories they searched rather than picking a plausible one. ``` /kettle:init ``` Only you can run it; Claude can't invoke it on its own. Which directory is a project is a statement, and a model guessing at one is the failure the marker exists to prevent. It is idempotent, adds `.kettle/` to `.gitignore`, and moves an older store in if it finds one. Don't run it inside a `git worktree`: the marker is gitignored, so a worktree has none by design and reaches the main checkout's store on its own. That is all `/kettle:issue` needs — no login, no network, no tracker. For the Gitea side, give this machine a credential and pin it to the project: ``` /kettle:auth ``` Claude will list what `kettle auth` already holds and ask you to pick. Adding a login is yours to do — the token is read from standard input so it never lands in shell history, and never in a transcript: ```bash kettle auth add --name noodles --url https://git.example.com < token.txt kettle init --login noodles --repo owner/name ``` Tokens live in `~/.config/kettle/logins.yaml`, mode 0600, outside every working tree. What goes in the repository is the login's **name**, in `.kettle/config.yaml` — worth nothing on its own, which is what makes it safe there. `kettle config` prints everything a directory resolved to and never prints a token. After that, just ask Claude to do something with issues — it loads the right skill on its own. ## The kettle-runner agent The skills carry meaning; the binary carries work. `kettle-runner` is a subagent on Haiku that does the second half in its own context and hands back a receipt — what ran, what it touched, what failed, verbatim. Delegate a **batch**: pull a milestone and rebuild the index, push the three issues you just wrote, bootstrap the label set, post a comment from a file you prepared. Spawning it for a single `kettle pull 42` costs more than running the command yourself; the saving is in the loop, the retry, and reading somebody else's stderr. It cannot decide anything. No `Edit`, no `Write`, no `--force`, no widening the set it was handed, no request it composed itself — `kettle api` goes out as the caller spelled it, and a deletion is never something it adds. A missing type, a failed validation, an unpushed dependency come back as a question, not as a guess. ## Project layout ``` .claude-plugin/ plugin.json plugin manifest (the marketplace catalog lives one level up, in the repo root's .claude-plugin/marketplace.json) agents/ kettle-runner.md subagent (Haiku) that runs the commands hooks/ hooks.json registers the one hook agents-sync.sh keeps AGENTS.md real and CLAUDE.md a symlink to it skills/ init/ /kettle:init — make a directory a project auth/ /kettle:auth — the credential a project runs under project/ generated flag reference: init, auth, config, gen issue/ /kettle:issue — the issue domain, offline references/format.md canonical issue format (identity, types, templates) sync/ /kettle:sync — the bridge to Gitea api/ /kettle:api — every other Gitea entity, by endpoint ``` `AGENTS.md` carries the same layout with the reasoning behind it, and the binary's own design is in [`cli/AGENTS.md`](../../cli/AGENTS.md). If any two disagree, the binary is right and the prose is stale. ## Local issue store Issues live in `/.kettle/issues/` as flat markdown with one metadata field per line — so `grep -l 'labels:.*type/bug' .kettle/issues/*.md` works without a parser. The directory is gitignored by `kettle init`; drop that line if your team decides otherwise. It holds two kinds of file and only one of them is a store: - **An `origin: local` file *is* the issue** — the only copy of that work. It is a complete state, not a draft, and nothing evicts it, in any state. - **Anything with a tracker origin is a working copy.** A successful push deletes it — `--update` too, one rule with no exception — and only after the tracker confirms the write. Get it back with `kettle pull `: same slug, same `depends:`, even after a rename in the web UI. - Identity is a slug (`wire-sqlc-appclick.md`), never a tracker number. Numbers live in a `gitea:` field. - A closed issue is **evicted, not archived**. The store is a working set. - Pulling overwrites the body: a fetch, not a merge. Checkbox state is the one exception, because a tick is monotone. - Nothing tracks drift, and there is nothing to track: a file that is still here has not been pushed. ## Development ```bash cd cli && go test ./... # the binary's suite cd cli && go build -o /tmp/kettle ./cmd/kettle /tmp/kettle gen skills --out plugins/kettle/skills --check # docs vs binary ``` The second command is the plugin's only mechanical invariant: what the skills say about a command matches the command. Run `gen skills` without `--check` to rewrite the generated regions after changing the CLI, and never edit inside the `` markers by hand.