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>
306 lines
15 KiB
Markdown
306 lines
15 KiB
Markdown
# claude-skills — a Claude Code plugin marketplace
|
|
|
|
One repository, one marketplace, two plugins, and the `kettle` binary the issue
|
|
plugin is built on. Register the marketplace once and install whichever pieces you
|
|
want; each plugin is independent and carries its own manifest and docs.
|
|
|
|
## Installation
|
|
|
|
```
|
|
/plugin marketplace add https://git.noodles.cam/claude-skills/marketplace.git
|
|
```
|
|
|
|
Working from a local clone? Point at the directory instead:
|
|
|
|
```
|
|
/plugin marketplace add /path/to/marketplace
|
|
```
|
|
|
|
Then install what you need:
|
|
|
|
```
|
|
/plugin install kettle@claude-skills
|
|
/plugin install tdl@claude-skills
|
|
```
|
|
|
|
Use `/plugin` to enable, disable, or update them later.
|
|
|
|
**`kettle` also needs its binary**, which no plugin can install for you. Build it
|
|
from this repository — `cli/go.mod` requires **Go 1.26**, and the first build
|
|
downloads eight modules, verified against `go.sum`:
|
|
|
|
```bash
|
|
cd cli && make install # ~/.local/bin/kettle, version stamped
|
|
cd cli && make install BINDIR=$(go env GOPATH)/bin
|
|
|
|
# or plain go, without the version stamp:
|
|
cd cli && go build -o ~/.local/bin/kettle ./cmd/kettle
|
|
go install git.noodles.cam/claude-skills/marketplace/cli/cmd/kettle@latest
|
|
```
|
|
|
|
Put the target directory on your `PATH` and check with `kettle version`. A skill
|
|
that answers `command not found: kettle` is telling you exactly this.
|
|
|
|
## What ships here
|
|
|
|
| Plugin | Commands | What it does |
|
|
|---|---|---|
|
|
| [`kettle`](plugins/kettle) | `/kettle:init` `/kettle:auth` `/kettle:issue` `/kettle:sync` `/kettle:api` `/kettle:project` | Issues as local markdown, cleanly layered. Issues are units of work offline first and tracker rows second; the `kettle` binary does the work, and each skill's command reference is generated from the binary's own command registry so it cannot drift |
|
|
| [`tdl`](plugins/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`](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 skills` | rewrites the plugin's SKILL.md command reference from the binary's own command registry |
|
|
| `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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
grep -l 'labels:.*type/bug' .kettle/issues/*.md
|
|
grep -ln 'depends:.*migrate-schema' .kettle/issues/*.md # who depends on it
|
|
```
|
|
|
|
### The tracker
|
|
|
|
```bash
|
|
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**:
|
|
|
|
```bash
|
|
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 skills --check` | the plugin's SKILL.md command reference still agrees with the binary it documents. Exit 1 the moment it does not, and `gen skills` without `--check` is the fix |
|
|
|
|
That last one is the invariant that keeps the plugin honest: everything a
|
|
SKILL.md says about a `kettle` command — its usage line, its flags, its
|
|
examples — is generated from the registry the binary is built from, between
|
|
`<!-- kettle:gen -->` markers. Prose outside the markers is never touched.
|
|
|
|
### Where the design notes are
|
|
|
|
**Every directory with a story carries its own `AGENTS.md`**, starting at [the
|
|
repository root](AGENTS.md) and recursing into [`cli/`](cli/AGENTS.md), each of its
|
|
packages, and each plugin. A `CLAUDE.md` beside one is a generated symlink to it, and
|
|
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. Read the one 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](AGENTS.md) and
|
|
recursing into [`cli/`](cli/AGENTS.md), 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 plugin's documentation.
|
|
|
|
```bash
|
|
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.
|