refactor!: rewire the plugin onto the kettle binary, and rename it
BREAKING: the plugin is `kettle`, not `tea`, and its commands are `/kettle:*`. It also now needs a binary on PATH that it did not need before; the README and every skill say how to get one and what a missing one looks like. The plugin was 3800 lines of Python doing what a compiled binary does better, and the name pointed at a tool that no longer takes part: `tea` is Gitea's CLI, and since the transport moved into the binary nothing here shells out to it for issues at all. A plugin named after it was going to keep suggesting otherwise. Deleted: 19 scripts, the 14-file unittest suite, and the tea-guard hook. The guard blocked any `tea` invocation that would run under a login the model picked instead of the operator; the binary holds its own credentials and reads the pinned login out of the project's own config, so that failure is no longer expressible and there is nothing left to police. agents-sync stays — it is about AGENTS.md symlinks and has nothing to do with any of this. What the plugin keeps is what only a plugin can carry: the rules an operator states and a binary cannot enforce. `init` still refuses to run inside a linked worktree and still may not be model-invoked, because which directory is the project is a statement a person makes. The issue format reference stays here and stays the source of truth. The runner subagent is still for batches and still may not decide what an issue says. The command reference in the issue, sync and project skills is GENERATED from the binary's own command registry, between markers, so a flag that changed cannot ship with a skill that recommends the old one. `kettle gen skills --check` exits non-zero when they drift. The generator owns the region and nothing outside it: the frontmatter description, which is what decides whether a skill loads at all, stays hand-written. `use` survives and is the one place `tea` is still named — for releases, webhooks and actions, which kettle does not cover. Its instruction to write `--login "$GITEA_LOGIN"` and let the hook substitute the pin was true until this commit and is now rewritten: `tea` keeps its own configuration, kettle keeps its own, and configuring one configures nothing in the other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
---
|
||||
name: issue
|
||||
description: Work with this project's issues as units of work — create, read, grep, validate, tick checkboxes, evict closed ones, and walk their dependency graph, with the `kettle` binary's offline commands (new, check, ac, tree, index, evict). Entirely offline; issues are local markdown files in `.kettle/issues/` and need no tracker, no login and no network. Load when the user asks to file or create an issue, read or find issues, check one against the format, or see what depends on what. Pushing to or pulling from Gitea is /kettle:sync.
|
||||
---
|
||||
|
||||
# /kettle:issue — issues as units of work
|
||||
|
||||
An issue is a markdown file in `<project>/.kettle/issues/`. This skill covers
|
||||
everything you do **with** an issue: writing one, reading one, checking it
|
||||
against the canonical format, ticking its boxes, and walking the dependency
|
||||
graph.
|
||||
|
||||
**Nothing here touches the network.** No tracker, no login, no token. An issue
|
||||
that lives only on this machine is a first-class issue, not a draft waiting to
|
||||
be uploaded. Synchronizing with a tracker is a separate, optional layer —
|
||||
`/kettle:sync`.
|
||||
|
||||
Read [`references/format.md`](references/format.md) before creating or editing
|
||||
an issue. It is the single source of truth for identity, metadata, types,
|
||||
labels, templates, and language rules.
|
||||
|
||||
**No `kettle` on PATH?** `command not found: kettle` is the whole story — the
|
||||
Python scripts this plugin used to ship are gone and `tea` is not a substitute.
|
||||
Stop and tell the operator to install it: `cd cli && go build -o
|
||||
~/.local/bin/kettle ./cmd/kettle` in the marketplace repository (go.mod requires
|
||||
**go 1.26**), or `go install
|
||||
git.noodles.cam/claude-skills/marketplace/cli/cmd/kettle@latest`.
|
||||
|
||||
## Identity: the slug
|
||||
|
||||
The file name is the id and the id is a slug —
|
||||
`.kettle/issues/wire-sqlc-appclick.md`. It never changes: not when the title
|
||||
changes, not when the issue is pushed somewhere. Tracker numbers live in a
|
||||
metadata field (`gitea: owner/repo#42`), never in a file name and never in
|
||||
`depends:`.
|
||||
|
||||
Consequence worth internalizing: **`#42` means nothing in this layer.** Refer to
|
||||
issues by id.
|
||||
|
||||
```
|
||||
.kettle/issues/INDEX.md table of every issue — read this first
|
||||
.kettle/issues/wire-sqlc-appclick.md metadata block + `# Title` + body
|
||||
.kettle/issues/wire-sqlc.comments.md comment thread (written by /kettle:sync only)
|
||||
.kettle/issues/tree-<id>.md saved graph (kettle tree --write)
|
||||
```
|
||||
|
||||
## Where the store is
|
||||
|
||||
`<project root>/.kettle/issues` — **not** `.kettle/issues` relative to wherever
|
||||
you are standing. The project root is the nearest directory up from where you
|
||||
are that holds a `.kettle/` marker: the binary walks up from
|
||||
`$CLAUDE_PROJECT_DIR`, then from the working directory, and out of a linked
|
||||
worktree to its main checkout. Every command sees one store no matter which
|
||||
subdirectory it runs in, and a `cd` into a *different* project correctly answers
|
||||
with that project's issues.
|
||||
|
||||
**A project has a store because an operator ran `/kettle:init` in it.** The
|
||||
marker is never inferred from the tree — `.git` is in every clone. **With no
|
||||
marker anywhere, every command stops and names the directories it searched.** It
|
||||
does not fall back to a plausible directory. If you see that, either you are not
|
||||
in the project you think you are, or nobody has initialized it: tell the operator
|
||||
to run `/kettle:init`. It is theirs to run, and it carries the worktree and
|
||||
migration-clash rules a bare `kettle init` does not.
|
||||
|
||||
`--out` overrides all of it and is taken **literally**: an absolute path as
|
||||
given, a relative one relative to the working directory. `kettle config` prints
|
||||
every path this directory resolved to and is the fastest way to explain a run
|
||||
that went somewhere unexpected.
|
||||
|
||||
Two things follow, both deliberate: a store that is not there reports `does not
|
||||
exist` while a store with nothing in it reports `is empty` — different problems —
|
||||
and nothing conjures a store as a side effect of a write.
|
||||
|
||||
## Reading: grep, don't parse
|
||||
|
||||
Metadata is one field per line with inline lists precisely so plain `grep`
|
||||
works. `INDEX.md` first, then the files:
|
||||
|
||||
```bash
|
||||
grep -l 'labels:.*type/bug' .kettle/issues/*.md # all bugs
|
||||
grep -l 'origin: local' .kettle/issues/*.md # never pushed anywhere
|
||||
grep -ln 'depends:.*migrate-schema' .kettle/issues/*.md # who depends on it
|
||||
grep -A3 '## Acceptance criteria' .kettle/issues/wire-*.md
|
||||
grep -c '^- \[ \]' .kettle/issues/wire-sqlc-appclick.md # open checkboxes
|
||||
```
|
||||
|
||||
Read whole files only for the issues the task actually needs.
|
||||
|
||||
## Creating an issue
|
||||
|
||||
1. **Read the format**: [`references/format.md`](references/format.md).
|
||||
2. **Pick the type** — `bug`, `task`, `refactor`, `test`, `feature` (a container
|
||||
for several issues with one business value), or `draft` (an idea not ready
|
||||
for work). If it is not obvious from the request, ask the user; one question.
|
||||
3. **Scaffold it** with `kettle new` — English imperative title, no type prefix,
|
||||
`--depends` takes ids.
|
||||
4. **Fill the sections** with Edit — every section of the template present and
|
||||
in order, **headers English, prose Russian**. `## Spec` gets a repo path, a
|
||||
URL, or the literal `none`; ask the user if you cannot determine which.
|
||||
5. **Check it** with `kettle check <id>`.
|
||||
|
||||
One file = one issue. Several related issues = several files, linked through
|
||||
`depends:`.
|
||||
|
||||
The issue is real and complete the moment the file exists. `origin: local` is a
|
||||
finished state, not a draft — and while it says local, **that file is the only
|
||||
copy of the work.** Publishing it to Gitea is a separate decision
|
||||
(`/kettle:sync`) and it ends that state: a push hands the issue over and deletes
|
||||
the file.
|
||||
|
||||
## Editing an issue
|
||||
|
||||
Edit the file. Change `state:` to close it, edit `labels:`, add ids to
|
||||
`depends:`. Re-run `kettle check` afterwards, and `kettle index` to refresh the
|
||||
table. Checkboxes are the exception — use `kettle ac`.
|
||||
|
||||
If the issue is synced (`origin:` names a tracker), the file is a working copy:
|
||||
your edit is local until `kettle push --update`, and that push **deletes the
|
||||
file** once the tracker has it. Closing one of those is `kettle close` — it moves
|
||||
the state on both sides in one run, where editing `state:` here alone would only
|
||||
ever tell this machine. Get the file back with `kettle pull <n>`; the slug does
|
||||
not change.
|
||||
|
||||
## Ticking checkboxes
|
||||
|
||||
A checkbox is the one part of a body that is **state** and not prose, so it has
|
||||
a command of its own. Never rewrite a body to tick a box: the rewrite re-flows
|
||||
lines and re-words sentences, and the issue's diff swells around a change that
|
||||
means one character. `kettle ac <id>` lists them numbered with their state,
|
||||
`--check` / `--uncheck` take a number or a substring.
|
||||
|
||||
- **Every checkbox in the body counts, not just `## Acceptance criteria`.** A
|
||||
`type/feature` keeps its children as checkboxes under `## Issues` and they are
|
||||
in the same numbering.
|
||||
- **A substring must match exactly one item.** Two matches is an error listing
|
||||
both; pick by number. It never guesses.
|
||||
- **Exactly one character of the file changes.** Wording, wrapping and trailing
|
||||
whitespace come back byte for byte, so both `git diff` and the tracker's diff
|
||||
show the tick and nothing else.
|
||||
- Examples inside a ``` fence are markup, not state — they are skipped.
|
||||
- Whether a criterion is actually *met* is a judgement about content. Tick what
|
||||
the caller named, never what looks done.
|
||||
|
||||
Getting the tick to the tracker is a separate step — `kettle push --update`.
|
||||
|
||||
## Writing a proper description
|
||||
|
||||
Issues get filed on the run — "comments aren't pulled", "the guard broke". That
|
||||
is a request, not a statement of work: no reproduction steps, no
|
||||
`path/file:line`, acceptance criteria nobody can check. Rewriting one into the
|
||||
canonical format is a procedure, not improvisation.
|
||||
|
||||
1. **Read the issue whole**, and everything it points at — the ids in
|
||||
`depends:`, the `## Spec` target, the files it names.
|
||||
2. **Determine the type and its template.** The `type/*` label selects one of
|
||||
the templates in [`references/format.md`](references/format.md), and that
|
||||
template's section list is the shape you are aiming at. If the label is
|
||||
missing or wrong, decide it now and fix `labels:`; promoting a `type/draft`
|
||||
to a concrete type is this same step.
|
||||
3. **Locate the anchor points in the code.** Grep the repo for every file,
|
||||
symbol, command and error string the issue mentions, until you can name
|
||||
lines. Work that does not exist yet still has anchor points — the files the
|
||||
change will land in, and the ones that will call it.
|
||||
4. **Gather the missing context.** What has to be there when you are done:
|
||||
- code references in the `path/file.ext:line` form, for every place the
|
||||
change lands;
|
||||
- reproduction steps — exact commands and their real output (`type/bug`
|
||||
splits them across `## Steps to reproduce` / `## Expected` / `## Actual`);
|
||||
- acceptance criteria that are objectively checkable: a command that exits 0,
|
||||
a file that exists, a section that is present — not aspirations;
|
||||
- a real value for `## Spec` — a repo path, a URL, or the literal `none`.
|
||||
|
||||
**A missing fact is either found in the repository or becomes a question to
|
||||
the user. Inventing one is forbidden.** Ask in one batch, and keep `none` in
|
||||
`## Spec` as the legitimate answer it is — never a plausible-looking link.
|
||||
5. **Rewrite the sections** with Edit: every section of the template, in the
|
||||
template's order, English headers and Russian prose. Replace the body; do not
|
||||
append a second telling of the same issue below the old one.
|
||||
6. **Check it** with `kettle check <id>`. Errors mean malformed, warnings mean
|
||||
the type's template is not fully filled in. Re-run `kettle index` if the
|
||||
labels changed.
|
||||
|
||||
The procedure is identical for a local issue and a synced one — it works on
|
||||
`.kettle/issues/<id>.md` and this layer does not know the difference. Getting the
|
||||
rewritten body into the tracker is `kettle push --update` and is no part of this.
|
||||
|
||||
## Evicting closed issues
|
||||
|
||||
The store is a working set, not an archive. A closed issue is not a unit of work
|
||||
any more, and `kettle evict` takes it out — no `rm`, no rebuilding `INDEX.md` by
|
||||
hand. Two conditions, both read off the file, and the second is the whole safety
|
||||
argument:
|
||||
|
||||
| `state:` | `origin:` | what eviction does |
|
||||
|---|---|---|
|
||||
| `closed` | a tracker | removes `<id>.md` and every sidecar under that slug |
|
||||
| `closed` | `local` | **keeps it, always**, and says why |
|
||||
| `open` | anything | keeps it |
|
||||
|
||||
**`origin: local` is never evicted, in any state, not even when you name it on
|
||||
the command line.** That file *is* the issue; there is no copy to fetch back.
|
||||
Only a file whose own metadata says the work lives somewhere else may go — the
|
||||
same trade a push makes when it drops a file the tracker just confirmed.
|
||||
|
||||
`.remote.json` is deliberately **not** pruned: it is the number → slug ledger and
|
||||
its entries are supposed to outlive the files they name, which is what makes a
|
||||
later `kettle pull <n>` land on the same slug. And eviction is **not a one-off
|
||||
migration** — a pull by number fetches an issue in any state, so a closed issue
|
||||
pulled after an eviction lands on disk again. Evict it again when you are done.
|
||||
|
||||
This command decides from `state:` in the file, which is only as fresh as the
|
||||
last pull. To have the tracker's answer instead — an issue closed in the web UI
|
||||
five minutes ago — use `kettle sync-evict` from `/kettle:sync`, which refreshes
|
||||
`state:` first and then makes exactly this decision.
|
||||
|
||||
## Dependency graph
|
||||
|
||||
`depends:` is the authoritative edge list; the body's `## Depends on` section is
|
||||
prose for humans, and `kettle check` warns when they disagree. `kettle tree`
|
||||
draws downwards — what an issue depends on. The other direction is a grep, not a
|
||||
flag:
|
||||
|
||||
```bash
|
||||
grep -ln 'depends:.*migrate-schema' .kettle/issues/*.md
|
||||
```
|
||||
|
||||
A `type/feature` plus its children read as one document: draw the tree once for
|
||||
the shape, then grep the files.
|
||||
|
||||
## Layering rule
|
||||
|
||||
Everything below is offline. No command in this skill opens a socket, reads a
|
||||
token, or knows what an issue number is — that is `/kettle:sync`, and the domain
|
||||
would not notice if the tracker did not exist. If you find yourself wanting a
|
||||
tracker concept here — a number, a login, an HTTP call, a label colour — it
|
||||
belongs on the other side of that line.
|
||||
|
||||
The commands themselves follow. Their usage lines, flags, defaults and examples
|
||||
are generated from the binary's own command registry, so they cannot disagree
|
||||
with the binary; `kettle help <command>` prints the same text. Editing them here
|
||||
changes nothing.
|
||||
|
||||
<!-- kettle:gen -->
|
||||
**Generated from the kettle command registry by `kettle gen skills`.** Everything between the two markers is replaced on the next run — hand-written prose belongs outside them.
|
||||
|
||||
## `kettle ac <id>`
|
||||
|
||||
list and tick an issue's checkboxes
|
||||
|
||||
A checkbox is the one part of a body that is *state* and not prose. Everything
|
||||
else is written once; boxes get ticked as the work goes, and the only other ways
|
||||
to tick one are a human with an editor or a model rewriting the whole body — the
|
||||
second worse than the first, because the rewrite re-flows the text and the
|
||||
issue's diff swells around a change of one character. This changes that one
|
||||
character and nothing else.
|
||||
|
||||
Named after `## Acceptance criteria`, where most boxes live, but every checkbox in
|
||||
the body is listed and tickable: a type/feature keeps its children under
|
||||
`## Issues`, and binding this to one heading would silently lose half of them.
|
||||
|
||||
A substring picks an item only when it picks exactly one. Two matches is an
|
||||
error listing both — a coin flip would tick the wrong box and look like it
|
||||
worked.
|
||||
|
||||
Delivering the changed body to a tracker is not part of this; that is
|
||||
`kettle push --update`.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--check` | — | tick one item: number or substring |
|
||||
| `--out` | — | store root (default: <project>/.kettle/issues) |
|
||||
| `--uncheck` | — | untick one item: number or substring |
|
||||
|
||||
```bash
|
||||
kettle ac wire-sqlc-appclick # numbered list with state
|
||||
kettle ac wire-sqlc-appclick --check 3 # tick by number
|
||||
kettle ac wire-sqlc-appclick --check регресс # tick by substring
|
||||
kettle ac wire-sqlc-appclick --uncheck 3 # untick it again
|
||||
```
|
||||
|
||||
## `kettle check [<id>…]`
|
||||
|
||||
validate issues against the canonical format
|
||||
|
||||
The same check the sync layer runs before it pushes anything, available on its
|
||||
own so a local-only issue can be held to the format without a tracker being
|
||||
involved.
|
||||
|
||||
Errors mean malformed; warnings mean it deviates from its type's template or its
|
||||
graph looks suspect. An unticked checkbox is neither: work not done yet is the
|
||||
normal state of a perfectly well-formed issue.
|
||||
|
||||
Exit status is 1 when anything has errors, which is what makes this usable in a
|
||||
hook or a CI step.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--out` | — | store root (default: <project>/.kettle/issues) |
|
||||
| `--quiet` | `false` | exit status only, print nothing |
|
||||
| `--strict` | `false` | treat warnings as errors |
|
||||
|
||||
```bash
|
||||
kettle check # every issue in the store
|
||||
kettle check wire-sqlc-appclick # one issue
|
||||
kettle check --quiet # exit status only
|
||||
kettle check --strict # treat warnings as errors
|
||||
```
|
||||
|
||||
## `kettle evict [<id>…]`
|
||||
|
||||
remove closed issues from the local store
|
||||
|
||||
The store is a working set, not an archive. What is evicted is two conditions,
|
||||
both read off the file:
|
||||
|
||||
state: closed the work is done
|
||||
origin: <tracker> the work is somewhere else too
|
||||
|
||||
THE SECOND CONDITION IS THE WHOLE SAFETY ARGUMENT. `origin: local` means this
|
||||
file IS the issue — there is no other copy and deleting it deletes the work. It
|
||||
is never evicted, in any state, not even when named explicitly on the command
|
||||
line: a closed local issue is reported and kept.
|
||||
|
||||
Eviction asks the file rather than the tracker, because state and origin are
|
||||
domain fields and the answer is already in the store — which is why this needs
|
||||
no network and no login. `kettle sync-evict` is the variant that refreshes state
|
||||
from the tracker first and then makes the same decision.
|
||||
|
||||
Not a one-off migration: a pull by number fetches an issue in any state, so a
|
||||
closed issue pulled after an eviction lands on disk again. Evict it again when
|
||||
you are done with it.
|
||||
|
||||
INDEX.md is rebuilt, because it IS a view of the directory. The number -> slug
|
||||
ledger is deliberately not pruned: its entries outlive the files they name, and
|
||||
that is what makes a pull land on the same slug afterwards.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--dry-run` | `false` | print what would be removed; touch nothing |
|
||||
| `--out` | — | store root (default: <project>/.kettle/issues) |
|
||||
|
||||
```bash
|
||||
kettle evict # every closed issue that is not origin: local
|
||||
kettle evict old-thing another-thing # only these
|
||||
kettle evict --dry-run # print what would go; touch nothing
|
||||
```
|
||||
|
||||
## `kettle index`
|
||||
|
||||
rebuild INDEX.md from what is on disk
|
||||
|
||||
A map of the local store, nothing else. The `origin` column is the only place
|
||||
the index acknowledges that a tracker exists: `local` means the issue has never
|
||||
left this machine, anything else names the tracker it also lives in. Both are
|
||||
ordinary issues here.
|
||||
|
||||
`progress` counts the body's checkboxes, ticked over total, and is read off the
|
||||
body at build time rather than stored — a second copy of that state in a
|
||||
metadata field would be wrong by the next edit.
|
||||
|
||||
An existing store with nothing in it is a legitimate thing to index and gets an
|
||||
"_empty_" table. A store that is not there is an error, not a directory to
|
||||
create.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--out` | — | store root (default: <project>/.kettle/issues) |
|
||||
|
||||
```bash
|
||||
kettle index # rebuild the index for this project
|
||||
```
|
||||
|
||||
## `kettle new`
|
||||
|
||||
create a local issue from its type template
|
||||
|
||||
The issue is real the moment this writes the file. Nothing is pending, nothing
|
||||
is a draft awaiting a tracker: `origin: local` is a complete state and pushing it
|
||||
later is optional.
|
||||
|
||||
While it says local, this file is the ONLY copy of the work — the store, not a
|
||||
cache of anything. That is what a push changes: it hands the issue to the
|
||||
tracker and removes the file.
|
||||
|
||||
Writes .kettle/issues/<slug>.md prefilled with the type's template, prints the
|
||||
path, and rebuilds INDEX.md. Fill the sections in an editor, then run
|
||||
`kettle check <id>`.
|
||||
|
||||
Body prose is Russian, section headers and the title are English.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--assignee` | — | assignee login; repeat |
|
||||
| `--depends` | — | id this issue depends on; repeat |
|
||||
| `--id` | — | slug (default: derived from the title) |
|
||||
| `--label` | — | extra label, e.g. tech/sql; repeat |
|
||||
| `--milestone` | — | milestone title |
|
||||
| `--out` | — | store root (default: <project>/.kettle/issues) |
|
||||
| `--severity` | — | severity/* label, one of: low, medium, high, showstopper, critical |
|
||||
| `--title` | — | English, imperative, no type prefix |
|
||||
| `--type` | — | issue type, one of: bug, task, refactor, test, feature, draft (becomes the exclusive type/* label) |
|
||||
|
||||
```bash
|
||||
kettle new --type task --title "Wire sqlc into the appclick repo layer" --label tech/sql --label comp/appclick # a task with two free-form labels
|
||||
kettle new --type bug --title "Fix the index rebuild on an empty store" --depends wire-sqlc-appclick --milestone v0.2 # a bug that is blocked by another issue
|
||||
```
|
||||
|
||||
## `kettle tree [<id>…]`
|
||||
|
||||
draw the dependency graph of the local store
|
||||
|
||||
Edges come from the `depends:` metadata, which is the authoritative edge list;
|
||||
prose in the body is never walked. Because the graph is slugs all the way down,
|
||||
this works identically for issues that were never pushed anywhere.
|
||||
|
||||
Downwards is what this draws — what an issue depends on. The other direction is
|
||||
a grep, not a flag:
|
||||
|
||||
grep -ln 'depends:.*migrate-schema' .kettle/issues/*.md
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--depth` | `6` | maximum depth |
|
||||
| `--out` | — | store root (default: <project>/.kettle/issues) |
|
||||
| `--write` | `false` | also write <store>/tree-<slug>.md |
|
||||
|
||||
```bash
|
||||
kettle tree # every root (nothing depends on it)
|
||||
kettle tree wire-sqlc-appclick # one subtree
|
||||
kettle tree --depth 2 --write # shallow, and saved beside the issues
|
||||
```
|
||||
<!-- /kettle:gen -->
|
||||
@@ -0,0 +1,372 @@
|
||||
# Issue format
|
||||
|
||||
Canonical format for every issue in this project, whether it ever reaches a
|
||||
tracker or not. Designed to be unambiguous for both humans and LLMs: fixed
|
||||
English section headers in a fixed order, verifiable acceptance criteria, one
|
||||
issue = one deliverable.
|
||||
|
||||
Nothing here depends on Gitea. How these files are mapped onto a tracker is the
|
||||
sync layer's business — see `/kettle:sync`.
|
||||
|
||||
## Identity
|
||||
|
||||
An issue is one file, `.kettle/issues/<id>.md`, and `id` is a slug: lowercase
|
||||
ASCII, digits, single dashes, derived from the title. **The slug is the
|
||||
identity.** It is stable for the life of the issue — a retitled issue keeps its
|
||||
slug; an issue pushed to a tracker, deleted locally and fetched back a month
|
||||
later keeps it too. Tracker numbers are a foreign key stored in a field, never
|
||||
the name of anything.
|
||||
|
||||
```
|
||||
.kettle/issues/wire-sqlc-appclick.md
|
||||
```
|
||||
|
||||
A slug never contains a dot, which is how the store tells an issue from the
|
||||
files parked beside it (`<id>.comments.md`).
|
||||
|
||||
Stability is a promise the format makes, so something has to keep it once the
|
||||
file is gone. That is the sync layer's problem and its answer is a marker in the
|
||||
body — see `/kettle:sync`; the domain neither writes nor reads it, and it never
|
||||
appears in the file on disk.
|
||||
|
||||
## Metadata block
|
||||
|
||||
One field per line, lists inline, so plain `grep` works without a parser:
|
||||
|
||||
```markdown
|
||||
---
|
||||
id: wire-sqlc-appclick
|
||||
state: open
|
||||
labels: [type/task, tech/sql]
|
||||
assignees: [naudachu]
|
||||
milestone: v0.2
|
||||
depends: [migrate-schema]
|
||||
origin: gitea
|
||||
branch: feat/wire-sqlc
|
||||
gitea: claude-skills/tea#42
|
||||
remote-updated: 2026-08-09T18:24:01Z
|
||||
synced: 2026-08-09T18:40:00Z
|
||||
url: https://git.noodles.cam/claude-skills/tea/issues/42
|
||||
---
|
||||
# Wire sqlc into the appclick repo layer
|
||||
|
||||
## Summary
|
||||
…
|
||||
```
|
||||
|
||||
| Field | Owner | Meaning |
|
||||
|---|---|---|
|
||||
| `id` | domain | slug; equals the file name |
|
||||
| `state` | domain | `open` or `closed` |
|
||||
| `labels` | domain | see namespaces below; exactly one `type/*` |
|
||||
| `assignees` | domain | logins; may be empty |
|
||||
| `milestone` | domain | title, or `none` |
|
||||
| `depends` | domain | ids this issue depends on — **the authoritative graph** |
|
||||
| `origin` | domain | `local`, or the name of a tracker this also lives in |
|
||||
| `gitea` | sync | the handle in that tracker: `owner/repo#N` |
|
||||
| `branch` | sync | the tracker's branch link (Gitea `ref`); push fills an empty one with the current git branch, and never overwrites a filled one |
|
||||
| `url`, `synced`, `remote-updated`, `comments` | sync | bookkeeping |
|
||||
|
||||
Domain fields render first, in the order above; sync fields follow, sorted.
|
||||
|
||||
`origin` is domain-owned on purpose: *whether* a piece of work exists anywhere
|
||||
but here is a fact about the work. *Where* that is, and how to reach it, is the
|
||||
sync layer's business — the domain carries `gitea:` and the rest through
|
||||
load/save verbatim and never reads them. That passthrough is why one file can
|
||||
represent a local issue and a synced one without a second format.
|
||||
|
||||
`origin: local` is a **complete state, not a pending one.** An issue that never
|
||||
leaves this machine is valid and finished work; pushing it is optional and
|
||||
nothing here treats it as a draft.
|
||||
|
||||
It is not a *permanent* state, and it is what the file's fate depends on:
|
||||
|
||||
| `origin:` | what the file is | what a push does to it | what eviction does to it |
|
||||
|---|---|---|---|
|
||||
| `local` | the issue itself — the only copy there is | creates it in the tracker, then deletes the file | **nothing, ever** — in any state, named or not |
|
||||
| a tracker | a working copy of something the tracker already has | updates the tracker, then deletes the file | removes it once `state: closed` |
|
||||
|
||||
**A successful push deletes `.kettle/issues/<id>.md`** (and `<id>.comments.md`), on
|
||||
create and on `--update` alike. What is in the store is what has not left this
|
||||
machine; everything else is fetched again when it is needed. The rule, its
|
||||
safety conditions, and how the slug survives are `/kettle:sync`'s to state.
|
||||
|
||||
**A closed issue is evicted from the store** by `kettle evict` — same trade,
|
||||
one condition more: the work is done *and* it exists somewhere else. An
|
||||
`origin: local` issue is never evicted, because there is nowhere to fetch it
|
||||
back from. The store is a working set, not an archive; `kettle pull <n>` fetches a
|
||||
closed issue again whenever it is wanted.
|
||||
|
||||
The `id` never changes across that round trip, which is why `depends:` in other
|
||||
issues keeps working. That is the format's promise; the mechanism is not.
|
||||
|
||||
## Language rules
|
||||
|
||||
- **Issue title**: English, imperative mood, no type prefix — the type lives in
|
||||
the label, not the title. Good: `Fix the index rebuild on an empty store`.
|
||||
Bad: `fix: crash`, `[bug] crash`, `Крашится гвард`.
|
||||
- **Section headers**: the exact English literals below, as `##` headings, in
|
||||
the given order. Do not translate, rename, or reorder them.
|
||||
- **Body prose** (text inside sections): Russian.
|
||||
|
||||
## Label namespaces
|
||||
|
||||
Four namespaces classify an issue. Two are exclusive (at most one label from
|
||||
the namespace), two are free-form:
|
||||
|
||||
| Namespace | Exclusive | Purpose |
|
||||
|---|---|---|
|
||||
| `type/*` | yes | What kind of work; primarily its business value. Mandatory, exactly one. |
|
||||
| `severity/*` | yes | Business impact. At most one; apply when the impact is known. |
|
||||
| `tech/*` | no | Technology the issue is bound to. Any number. |
|
||||
| `comp/*` | no | System component of this repo. Any number; no preset — project-specific. |
|
||||
|
||||
### `type/*` — mandatory, exactly one
|
||||
|
||||
| Label | Meaning |
|
||||
|---|---|
|
||||
| `type/bug` | Something behaves incorrectly in existing code |
|
||||
| `type/task` | Implementation of new functionality |
|
||||
| `type/refactor` | Internal restructuring: file moves, architecture; behavior must not change |
|
||||
| `type/test` | Writing or fixing tests |
|
||||
| `type/feature` | Container: several issues delivering one unit of business value |
|
||||
| `type/draft` | Idea captured for later; not ready for work |
|
||||
|
||||
### `severity/*` — at most one
|
||||
|
||||
`severity/low`, `severity/medium`, `severity/high`, `severity/showstopper`,
|
||||
`severity/critical`.
|
||||
|
||||
### `tech/*` — any number
|
||||
|
||||
Technology-bound labels, e.g. `tech/sql` (pgx, sqlc, sql-migrate — persistent
|
||||
storage), `tech/obs` (grafana, loki, prometheus, alloy — observability),
|
||||
`tech/postgres`.
|
||||
|
||||
### `comp/*` — any number
|
||||
|
||||
Components of this repo's system, e.g. `comp/appclick`. No preset list —
|
||||
derive from the project.
|
||||
|
||||
> Label **colors** are not part of the format: a hex code is how a tracker
|
||||
> paints a chip, not what an issue is. They live in the binary's mapping layer
|
||||
> (`cli/internal/mapping`) and are applied on push.
|
||||
|
||||
## Dependencies
|
||||
|
||||
`depends:` in the metadata block is the graph, and it holds **ids**:
|
||||
|
||||
```markdown
|
||||
depends: [migrate-schema, add-pool-cfg]
|
||||
```
|
||||
|
||||
An optional `## Depends on` section, placed right after `## Spec`, carries the
|
||||
human explanation — one reference per line, with a reason where it helps:
|
||||
|
||||
```markdown
|
||||
## Depends on
|
||||
- migrate-schema — нужна схема БД из этого issue
|
||||
- add-pool-cfg
|
||||
```
|
||||
|
||||
The section is prose and is passed to and from a tracker unchanged; only
|
||||
`depends:` is walked when the graph is computed. Keeping them consistent is on
|
||||
you — `kettle check` warns when the section names an id that `depends:` does
|
||||
not list. Omit the section when there are no dependencies; never write an empty
|
||||
one.
|
||||
|
||||
A `type/feature` container writes the same relation under `## Issues` instead
|
||||
(see the template below). Same direction, same rule: every id named there also
|
||||
belongs in that issue's `depends:`. The warning names whichever of the two
|
||||
sections the reference actually came from.
|
||||
|
||||
Draw the graph with `kettle tree`. The reverse direction is a grep:
|
||||
|
||||
```bash
|
||||
grep -ln 'depends:.*migrate-schema' .kettle/issues/*.md
|
||||
```
|
||||
|
||||
## Shared rules
|
||||
|
||||
- `## Summary` is always the first section; `## Acceptance criteria` is always
|
||||
present (exception: `type/draft`). These two are the anchors every reader
|
||||
(human or LLM) relies on.
|
||||
- `## Spec` is **mandatory in every type**. Its value is a repo path
|
||||
(`docs/specs/auth.md`), a URL, or the literal `none` when no spec exists.
|
||||
Never omit the section and never invent a link — `none` is an explicit,
|
||||
valid answer.
|
||||
- Acceptance criteria are `- [ ]` checkboxes; each item is an objectively
|
||||
checkable condition, not an aspiration.
|
||||
- A checkbox is **item markup, not a property of one section**: `- [ ]`
|
||||
unticked, `- [x]` ticked, and it means the same under `## Issues` as under
|
||||
`## Acceptance criteria`. An item that wraps continues on an indented line
|
||||
and is still one item. A `- [ ]` inside a ``` code fence is an example of the
|
||||
markup, not state. Tick them with `kettle ac`, which reads the whole body
|
||||
on exactly these rules and rewrites one character; progress (`3/7`) is
|
||||
counted off the body and is never a metadata field.
|
||||
- Code references use the `path/file.ext:line` form; related issues by id.
|
||||
- Screenshots are allowed but their content must be duplicated as text — an
|
||||
LLM reading these files cannot see images.
|
||||
- If acceptance criteria grow past ~5 unrelated items, split the issue (or
|
||||
promote it to a `type/feature` container with child issues).
|
||||
|
||||
## Template: `type/bug`
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
Что сломано и где проявляется, одно-два предложения.
|
||||
|
||||
## Spec
|
||||
`docs/specs/auth.md`, URL — или `none`.
|
||||
|
||||
## Steps to reproduce
|
||||
1. …
|
||||
2. …
|
||||
|
||||
## Expected
|
||||
Что должно было произойти.
|
||||
|
||||
## Actual
|
||||
Что происходит на самом деле: вывод команды, лог.
|
||||
|
||||
## Environment
|
||||
Только релевантное: версии, ОС, конфигурация.
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] баг не воспроизводится по шагам выше
|
||||
- [ ] добавлена проверка на регрессию (если применимо)
|
||||
```
|
||||
|
||||
## Template: `type/task`
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
Что нужно сделать, одно-два предложения.
|
||||
|
||||
## Spec
|
||||
Ссылка или `none`.
|
||||
|
||||
## Motivation
|
||||
Какую проблему пользователя/системы это решает.
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] проверяемое условие
|
||||
- [ ] …
|
||||
|
||||
## Constraints
|
||||
Что НЕ входит в объём; технические рамки. (опционально)
|
||||
```
|
||||
|
||||
## Template: `type/refactor`
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
Что перестраиваем и в каких файлах (`path/file:line`).
|
||||
|
||||
## Spec
|
||||
Ссылка или `none`.
|
||||
|
||||
## Motivation
|
||||
Чем плохо текущее состояние: дублирование, связность, читаемость.
|
||||
|
||||
## Invariants
|
||||
Что НЕ должно измениться: поведение, публичные API, форматы данных.
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] проверяемое условие (тесты зелёные, старый путь удалён, …)
|
||||
```
|
||||
|
||||
## Template: `type/test`
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
Что покрываем тестами и где (`path/file:line`).
|
||||
|
||||
## Spec
|
||||
Ссылка или `none`.
|
||||
|
||||
## Motivation
|
||||
Зачем: регрессия после бага, пробел в покрытии, флаки-тест.
|
||||
|
||||
## Test cases
|
||||
- сценарий → ожидаемый результат
|
||||
- …
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] перечисленные кейсы покрыты и зелёные
|
||||
- [ ] тесты проходят в CI
|
||||
```
|
||||
|
||||
## Template: `type/feature`
|
||||
|
||||
A container: one unit of business value delivered by several child issues.
|
||||
Child issues carry their own `type/*` (`task`, `bug`, `test`, …) and know
|
||||
nothing about the container.
|
||||
|
||||
**The container depends on its children, never the reverse.** Every child id
|
||||
goes in the container's own `depends:` and, as prose, in its `## Issues`
|
||||
section; a child's `depends:` is for that child's real dependencies and must
|
||||
not point back at the container. Keep implementation detail in the children;
|
||||
the feature body stays at business level.
|
||||
|
||||
That direction is not a convention picked at random. "The container is closed
|
||||
when its children are closed" *is* a dependency relation. "This child belongs
|
||||
to that feature" is a membership relation, and membership has no place in a
|
||||
dependency graph. Pointed the other way the two rules contradict each other:
|
||||
the moment the container listed a child that already depended on it,
|
||||
`kettle check` would report `ERROR cycle`. With the edge going down, the
|
||||
graph reads as nesting — `kettle tree` draws the container as the root with
|
||||
its children beneath it — and the check is green.
|
||||
|
||||
So the container's metadata block carries the children:
|
||||
|
||||
```markdown
|
||||
depends: [wire-sqlc-appclick, add-pool-cfg]
|
||||
```
|
||||
|
||||
and its body repeats them for a human:
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
Бизнес-ценность одним-двумя предложениями.
|
||||
|
||||
## Spec
|
||||
Ссылка или `none`.
|
||||
|
||||
## Motivation
|
||||
Какую проблему пользователя/системы это решает.
|
||||
|
||||
## Issues
|
||||
- [ ] wire-sqlc-appclick — краткое описание части
|
||||
- [ ] add-pool-cfg — краткое описание части
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] все дочерние issues закрыты
|
||||
- [ ] проверяемое условие уровня фичи (например, e2e-сценарий работает)
|
||||
```
|
||||
|
||||
## Template: `type/draft`
|
||||
|
||||
A parking spot for ideas that are not fleshed out yet. Minimal structure, no
|
||||
acceptance criteria required. Before implementation starts, a draft MUST be
|
||||
promoted: relabeled to a concrete type and rewritten into that type's template.
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
Идея одним-двумя предложениями.
|
||||
|
||||
## Spec
|
||||
Ссылка или `none` (для драфтов обычно `none`).
|
||||
|
||||
## Notes
|
||||
Свободные заметки: что известно, открытые вопросы, варианты.
|
||||
```
|
||||
|
||||
## Containers beyond `type/feature`
|
||||
|
||||
- **Milestone** — a set of issues with an optional time bound. Locally it is
|
||||
just the `milestone:` field; a tracker-side milestone must already exist for
|
||||
a push to attach the issue to it.
|
||||
- **Project** — a set of issues tracked by status columns (Backlog, ToDo,
|
||||
InProgress, Ready, Done). Not represented in this format and not reachable
|
||||
through the Gitea API — web UI only.
|
||||
Reference in New Issue
Block a user