01fb5a2703
There is no CI: the instance has no act_runner and none is planned, so releases are cut by hand. That makes `make check` the only thing standing between a mistake and the tracker, and it is one command: gofmt, vet, the suite with the cache defeated, `go mod verify`, a vendored build, and `kettle gen skills --check`. The last one is the invariant worth having — the plugin's SKILL.md command reference is generated from the binary's registry, so a flag that changed cannot ship with documentation that recommends the old one. `cli/cmd/release` publishes to Gitea using the same SDK the binary already vendors, which is a pleasing thing to be able to say: nothing third-party handles the artifacts. It is a second binary rather than a `kettle` subcommand on purpose — `kettle`'s command tree is what generates the plugin's skills, so a verb there ships to every operator, and publishing a release is build infrastructure. It is idempotent end to end: an existing release for the tag is reused, an asset of the same name is replaced rather than doubled, and a retried run converges instead of duplicating. `make release` refuses three things, each with its own message: a dirty working tree, a TAG that is not what `git describe` reports, and a tag the remote does not have. A release built from uncommitted code is unreproducible and nobody finds out until they need to reproduce it. `kettle version` reports the stamp, the toolchain and the VCS revision. The default is `dev`, and a hand build says so and means it — a binary out of somebody's working tree is not a release and must not claim to be one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
215 lines
9.0 KiB
Markdown
215 lines
9.0 KiB
Markdown
# 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:use` skill | `tea` CLI reference for the Gitea entities kettle does not cover: pulls, releases, milestones, webhooks, actions |
|
|
| `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 two `agents-*` hooks are Python scripts; `python3` must be on
|
|
`$PATH`. Nothing else here needs it.
|
|
- **`tea`** (optional) — Gitea's own CLI, only for `/kettle:use`. `brew install
|
|
tea`, or from [gitea.com/gitea/tea/releases](https://gitea.com/gitea/tea/releases).
|
|
It keeps its own logins (`tea logins add`), separate from kettle's.
|
|
|
|
### Installing the binary
|
|
|
|
Building it needs **Go 1.26** — `cli/go.mod` says so because the Gitea SDK
|
|
requires it. There is no `vendor/` directory: the first build downloads eight
|
|
modules, verified against `go.sum`.
|
|
|
|
```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 raw `tea`. 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
|
|
use/ /kettle:use — tea CLI reference
|
|
references/tea/ command docs
|
|
```
|
|
|
|
`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 `<project>/.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 <n>`: 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
|
|
`<!-- kettle:gen -->` markers by hand.
|