f18a633185
The plugin required `tea`, Gitea's own CLI, for everything that is not an issue: releases, pull requests, milestones, branches, actions, webhooks. That put a second binary, a second set of logins nothing here could see, and 400 lines documenting somebody else's flags outside anything this repository can test. One command over the transport that already existed removes all three. Transport: `post` — the hand-rolled request the SDK cannot express, written for the dependency endpoint — is generalized to an exported `Do`, and `post` is three lines on top of it. Same http.Client, so the same RoundTripper files the body under .kettle/payload/, the same `token …` header authenticates it, and a non-2xx is the same *APIError. It does not paginate, does not reformat the answer, and names no domain concept, so the layering test is untouched. The endpoint rule is `tea api`'s, so an endpoint table written for that tool still works — with one restriction it did not have: a full URL must be on this instance. Every request carries the project's token in a header, and a URL on another host would hand the token to whatever was typed. Command: `kettle api <endpoint>` in a new `api` group, so the generator writes plugins/kettle/skills/api/SKILL.md — group, directory and /kettle:api are one word. No --repo and no --login, for the reason no sync command has them: a cross-repository address is an address, and another instance is KETTLE_URL. `-X DELETE` needs `--yes`; a flag typed on purpose is an operator's decision. Scopes: a token minted for issues carries write:issue and answers 403 on the first request outside issues, naming no scope. Gitea cannot be asked what a token may do — its own token listing needs a password — so `auth add --scopes` records it, `auth list` and `config` show it, and a 403 says which category it is likely to be. Documentation only; nothing is checked against it. skills/use — the tea reference, 239 lines of it — becomes skills/api: what to ask for, which endpoints paginate, and how to write a body. Every mention of `tea` as a requirement is gone from the manifests, the READMEs, the runner and the four other skills; what survives is the back-compat with the old plugin, which is a decision and not a debt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
243 lines
13 KiB
Markdown
243 lines
13 KiB
Markdown
# AGENTS.md — the kettle CLI
|
|
|
|
`kettle` is a globally installed binary. **It owns this project's connection to
|
|
its tracker** — the credentials, the transport, the payload scratchpad — and
|
|
issues are its main subject but no longer its only one: what an issue is, where
|
|
the store lives, who this machine is, how issues move to and from Gitea, and,
|
|
through `kettle api`, every other Gitea entity that has no command of its own.
|
|
It replaced a set of Python scripts that used to ship inside the plugin.
|
|
|
|
That last clause is a deliberate widening and it is worth being straight about.
|
|
"Issues and nothing else" was the line until two things crossed it: `cmd/release`,
|
|
which publishes this repository's own releases, and `kettle api`, which exists
|
|
because the alternative was requiring `tea` — a second CLI with a second set of
|
|
logins that nothing here could see, documented in 400 lines of somebody else's
|
|
flag reference that nothing here could check. One door for every request is worth
|
|
more than a slogan: the token is held in one place, every body lands in one
|
|
scratchpad, and no skill has to explain which tool is authenticated as whom.
|
|
What has *not* widened is the domain — `internal/issue` still knows nothing about
|
|
trackers, and `api` is transport plus a command, touching neither it nor
|
|
`internal/mapping`.
|
|
|
|
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. `kettle api` did not put it back: it takes an
|
|
endpoint and a body, never a login, and a full URL on another host is refused
|
|
rather than sent with this project's token attached.
|
|
|
|
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`, `api` — 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.
|