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>
227 lines
12 KiB
Markdown
227 lines
12 KiB
Markdown
# AGENTS.md — the kettle CLI
|
|
|
|
`kettle` is a globally installed binary. It owns everything mechanical about
|
|
issues: what an issue is, where the store lives, who this machine is, and how
|
|
issues move to and from Gitea. It replaced a set of Python scripts that used to
|
|
ship inside the plugin.
|
|
|
|
The plugin keeps what only a plugin can carry — the rules an operator states and
|
|
a binary cannot enforce. Everything else is here.
|
|
|
|
**This file is the binary's map.** Each package documents its own rules in its own
|
|
directory; nothing below is repeated there and nothing there is repeated here.
|
|
|
|
## Why a binary
|
|
|
|
Three failures in the Python version were failures of *runtime*, not of logic:
|
|
|
|
- the store resolved from `__file__`, so it landed inside a versioned plugin
|
|
cache and issues written from one project were invisible from the next;
|
|
- the walk that answers "which directory is the project" was written three
|
|
times — store, login pin, guard hook — and in a linked worktree the three
|
|
disagreed;
|
|
- `sys.path.insert` was the import mechanism, so the layering rule was a
|
|
convention checked by grep.
|
|
|
|
A compiled binary answers all three by construction. There is one walk
|
|
([`internal/project`](internal/project/AGENTS.md)), it is imported rather than
|
|
re-derived, and the layering rule is a build graph a test walks.
|
|
|
|
## Layers
|
|
|
|
Knowledge flows one way. The arrow means "imports"; follow a name to that
|
|
package's own AGENTS.md.
|
|
|
|
```
|
|
cmd/kettle thin main; exit status only
|
|
cmd/release build infrastructure, not a kettle verb — see its own file
|
|
|
|
internal/cmd the command tree: flags, receipts, exit codes
|
|
│ │ │
|
|
│ │ └────► internal/config who this machine is, what this
|
|
│ │ project points at; yaml lives here
|
|
│ │ and only here
|
|
│ └───────────► internal/gitea TRANSPORT: one door for every
|
|
│ │ request, pagination, payload dumps,
|
|
│ │ the number -> slug ledger
|
|
│ ▼
|
|
├────────────────────► internal/wire ADDRESSES: Repo and Key, and the
|
|
│ ▲ parsing that reads them. Imports
|
|
│ │ nothing.
|
|
└──► internal/mapping ─────┘ BRIDGE: md <-> the SDK's payloads,
|
|
│ no I/O; label colours live here
|
|
▼
|
|
internal/issue DOMAIN what an issue is: format, taxonomy, validation,
|
|
│ checkboxes, dependency graph, the store, eviction
|
|
│ offline — no tracker, no network, no JSON
|
|
▼
|
|
internal/project ROOT which directory is the project, and every path
|
|
resolved from it: store, payload, config
|
|
depends on nothing
|
|
```
|
|
|
|
| package | layer | what its own file opens with |
|
|
|---|---|---|
|
|
| [`cmd/kettle`](cmd/kettle/AGENTS.md) | entry point | `os.Exit(cmd.Main(os.Args[1:]))`, and why there is nothing else in it |
|
|
| [`cmd/release`](cmd/release/AGENTS.md) | build tool | why publishing a release is not a `kettle` verb, and why it goes around the transport |
|
|
| [`internal/cmd`](internal/cmd/AGENTS.md) | commands | the registry every command is a value in, and the generator that writes the plugin's docs from it |
|
|
| [`internal/config`](internal/config/AGENTS.md) | configuration | two files, and why the tokens are not in the one inside the repository |
|
|
| [`internal/gitea`](internal/gitea/AGENTS.md) | transport | the SDK, the payload scratchpad, the ledger, the dependency endpoint |
|
|
| [`internal/mapping`](internal/mapping/AGENTS.md) | bridge | md ↔ payload, the id marker, label colours, the checkbox merge |
|
|
| [`internal/wire`](internal/wire/AGENTS.md) | addresses | `42`, `#42`, `owner/repo#42`, a URL — four spellings of one thing |
|
|
| [`internal/issue`](internal/issue/AGENTS.md) | domain | the format, the taxonomy, the store, eviction — all of it offline |
|
|
| [`internal/project`](internal/project/AGENTS.md) | root | the walk, and every path resolved from its answer |
|
|
|
|
The rules that hold the layers apart, and the seven tests that fail when one
|
|
breaks, are in [`internal/AGENTS.md`](internal/AGENTS.md). Read the diagram
|
|
bottom-up: each layer knows strictly less about trackers than the one above it.
|
|
|
|
## Dependencies, and building
|
|
|
|
`gopkg.in/yaml.v3` and `code.gitea.io/sdk/gitea` — eight modules once the SDK's
|
|
own are counted. No cobra: commands are values in a registry, which is what lets
|
|
the plugin's SKILL.md files be generated from the same struct that holds the code.
|
|
|
|
```bash
|
|
make install # build straight onto your PATH, version stamped
|
|
make install BINDIR=$(go env GOPATH)/bin
|
|
|
|
go build -o ~/.local/bin/kettle ./cmd/kettle # the same thing, unstamped
|
|
go install git.noodles.cam/claude-skills/marketplace/cli/cmd/kettle@latest
|
|
```
|
|
|
|
**`make` is the build, and it is also the CI.** There is no act_runner on the
|
|
instance this lives on and none is planned, so nothing runs on a push: `make check`
|
|
— fmt, vet, test, `go mod verify`, build, `gen skills --check` — is the only thing
|
|
standing between a mistake and the tracker, and it is on whoever is committing to run
|
|
it. `make help` lists the rest; `make dist` cross-compiles four platforms with a
|
|
`SHA256SUMS`, and `make release TAG=v1.2.3` publishes them through
|
|
[`cmd/release`](cmd/release/AGENTS.md) after refusing a dirty tree, a `TAG` that is
|
|
not what `git describe` reports, and a tag the remote does not have.
|
|
|
|
A hand build says `dev` for `kettle version` and means it: a binary out of somebody's
|
|
working tree is not a release and must not claim to be one. The version is stamped at
|
|
link time into `internal/cmd.Version`, derived from `git describe` rather than kept in
|
|
a file — a number somebody has to remember to bump is a number that will be wrong.
|
|
|
|
**`go.mod` says `go 1.26`**, which the SDK requires, and **`vendor/` is
|
|
committed** — 281 files, 2.3 MB, which is the price of knowing exactly what
|
|
compiled.
|
|
|
|
Be precise about what that buys, because it is easy to overclaim: vendoring
|
|
pins the *contents* of every dependency in this repository's own history, so a
|
|
dependency that is retagged, yanked or unreachable cannot change what this
|
|
binary is built from. It does **not** by itself give a network-free build.
|
|
`go 1.26` in `go.mod` means `GOTOOLCHAIN=auto` fetches a toolchain over the
|
|
network on any machine whose local Go is older, which is most of them. If an
|
|
air-gapped build is ever a requirement, pin the toolchain too — vendoring alone
|
|
will not get you there.
|
|
|
|
`make check` runs both halves: `go mod verify` says the module cache matches
|
|
`go.sum`, and `go build -mod=vendor ./...` says the committed tree is complete
|
|
and is what actually compiles. A `vendor/` that has drifted from `go.mod` fails
|
|
nothing until somebody builds with a cold cache, which is exactly when nobody
|
|
wants to find out.
|
|
|
|
The transport is the official SDK rather than hand-rolled `net/http`. What that
|
|
bought: the payload shapes are one vocabulary maintained by the people who
|
|
maintain the server, and the instance's version arrives for free, which is what
|
|
lets the transport answer "does this instance have issue dependencies?" from the
|
|
version instead of guessing from a status code. What it cost is written down where
|
|
it happened — see [`internal/gitea`](internal/gitea/AGENTS.md) and
|
|
[`internal/mapping`](internal/mapping/AGENTS.md).
|
|
|
|
The SDK is imported as `sdk` everywhere, so one type has one spelling across the
|
|
tree.
|
|
|
|
## The round trip
|
|
|
|
`push` and `pull` are the two halves of one rule, and the rule is that **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 and the number → slug ledger is written. A pull is how the copy
|
|
comes back, under the same slug, on a machine that has never seen it.
|
|
|
|
The mechanics of each half live with the commands that implement them, in
|
|
[`internal/cmd`](internal/cmd/AGENTS.md); the ledger that survives the deletion is
|
|
[`internal/gitea`](internal/gitea/AGENTS.md)'s, and the marker that survives a lost
|
|
ledger is [`internal/mapping`](internal/mapping/AGENTS.md)'s.
|
|
|
|
## No guard hook
|
|
|
|
The Python version needed a `PreToolUse` hook to block any `tea` command that
|
|
would run under a login the model picked instead of the operator. That whole
|
|
apparatus is gone. The binary holds its own credentials and reads the login out
|
|
of the project's own configuration, so there is no argument to police and no way
|
|
for the transport and the guard to disagree — the failure the hook existed to
|
|
catch is not expressible any more.
|
|
|
|
There is also no `--login` and no `--repo` on any sync command bar `labels`.
|
|
A cross-repository address is still an address: `kettle pull owner/repo#42`
|
|
re-points the client for that one call, which is bookkeeping and not a second
|
|
connection.
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
make check # fmt, vet, test, go mod verify, build, docs — the whole gate
|
|
go test ./... # just the tests
|
|
```
|
|
|
|
Three disciplines every test follows:
|
|
|
|
- **`internal/cmd` builds the binary once in `TestMain` and runs it as a
|
|
subprocess** against a throwaway project in a temp directory. The binary is
|
|
never run in the directory it was built in, because that is exactly the
|
|
arrangement that hid the `__file__` bug: a tool is installed in one place and
|
|
used on projects in another, and a test that collapses the two proves nothing
|
|
about resolution.
|
|
- **Every fixture strips `CLAUDE_PROJECT_DIR`**, the first anchor of the walk, or
|
|
the harness's own value would point every fixture at this repository. Anything
|
|
touching credentials sets `KETTLE_CONFIG_HOME` at a temp directory, so a run can
|
|
neither read nor overwrite the developer's own tokens.
|
|
- **Every fake tracker answers `/api/v1/version`**, because building an SDK client
|
|
asks for it. The fakes say 1.26.1; one says 1.19.4, and that one is a test — an
|
|
instance too old for the dependency endpoints is answered from its version with
|
|
no request made.
|
|
|
|
## Documentation that is generated
|
|
|
|
```bash
|
|
kettle gen skills --out ../plugins/kettle/skills # rewrite the blocks
|
|
kettle gen skills --out ../plugins/kettle/skills --check # exit 1 if stale
|
|
```
|
|
|
|
Everything between `<!-- kettle:gen -->` and `<!-- /kettle:gen -->` in the
|
|
plugin's SKILL.md files comes from the command registry, so a renamed flag cannot
|
|
ship with documentation that recommends the old one. `--check` is what a
|
|
pre-commit hook or a CI step calls. The generator is
|
|
[`internal/cmd`](internal/cmd/AGENTS.md)'s, and the seam between command groups
|
|
and plugin skills is [the plugin's](../plugins/kettle/AGENTS.md).
|
|
|
|
Note the asymmetry, because it is easy to get backwards: **a SKILL.md command
|
|
block is generated and must never be hand-edited; every AGENTS.md in this tree is
|
|
hand-written and must never be generated.** One is a flag table, the other is a
|
|
reason.
|
|
|
|
## Status
|
|
|
|
Done and tested: every package in the table above, and the commands `init`, `auth`,
|
|
`config`, `gen`, `version`, `new`, `check`, `ac`, `tree`, `index`, `evict`, `pull`,
|
|
`push`, `remote`, `comment`, `close`, `labels`, `sync-evict` — plus `cmd/release`,
|
|
against a fake Gitea.
|
|
|
|
The plugin is rewired: it lives at `plugins/kettle`, ships no Python domain code
|
|
and no guard hook, and its command reference is generated from this registry.
|
|
|
|
## Keeping this file true
|
|
|
|
- **Scope:** the binary as a whole — why it exists, what it depends on, how it is
|
|
built and tested, and where each package's own document is. Files: `go.mod`,
|
|
`go.sum`, `Makefile`, and the shape of the tree.
|
|
- **Update it when** a package is added or removed (the diagram and the table both
|
|
name every one), a dependency changes, a `make` target is added or its meaning
|
|
changes, or a cross-package contract like the round trip changes.
|
|
- **Do not** describe a package's internals here. That is what the files it links to
|
|
are for, and a summary that drifts is worse than a link.
|