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,98 @@
|
||||
---
|
||||
name: auth
|
||||
description: Give `kettle` a Gitea credential and choose which login this project runs under — `kettle auth list/add/remove` manages the machine-wide token file, `kettle init --login <name>` pins one of those names into `.kettle/config.yaml`. Load when a sync command reports no login, a 401, or an unknown login name, or when the user asks to switch the account this project's issues are pushed under. The OPERATOR picks the login; you never type a token.
|
||||
---
|
||||
|
||||
# /kettle:auth — the credential a project runs under
|
||||
|
||||
Two files, and the split is the whole design.
|
||||
|
||||
| where | what is in it | who writes it |
|
||||
|---|---|---|
|
||||
| `~/.config/kettle/logins.yaml` | the tokens, one file per machine, mode 0600, outside every working tree | `kettle auth add` |
|
||||
| `<project>/.kettle/config.yaml` | the **name** of one of those logins, and the tracker repo | `kettle init --login … --repo …` |
|
||||
|
||||
A name is worth nothing on its own, which is what makes it safe to keep in a
|
||||
file inside a repository. A token in a working tree ends up in a commit
|
||||
eventually, and a secret that has ever been pushed has to be rotated.
|
||||
`$KETTLE_CONFIG_HOME` or `$XDG_CONFIG_HOME` move the machine file;
|
||||
`KETTLE_LOGIN`, `KETTLE_URL` and `KETTLE_TOKEN` override it outright, which is
|
||||
how CI runs with no token on disk.
|
||||
|
||||
**There is no login pinned in `.claude/settings.local.json` any more, and no hook
|
||||
that rewrites a `--login` argument.** That mechanism is gone with the Python
|
||||
scripts; nothing here reads Claude's settings. If you find a `GITEA_LOGIN` in a
|
||||
settings file, it is dead weight from the old plugin.
|
||||
|
||||
## The one hard rule: the operator chooses, and holds the token
|
||||
|
||||
- **Never pick a login.** Not from memory, not from the repo URL, not from a
|
||||
previous session. Present the choice with `AskUserQuestion` — name, url and
|
||||
user out of `kettle auth list` — and let the operator answer. Exactly one login
|
||||
on the machine is the only case where you may propose, and you still confirm.
|
||||
- **Never type, echo, paste or read a token.** `kettle auth add` takes it on
|
||||
stdin precisely so it does not land in shell history; a token that goes through
|
||||
a model's context is a token in a transcript. Adding a login is the operator's
|
||||
own terminal, not a Bash call you make for them.
|
||||
|
||||
## Steps
|
||||
|
||||
1. See what this machine holds. It never prints a token, and there is no flag to
|
||||
make it:
|
||||
|
||||
```bash
|
||||
kettle auth list
|
||||
```
|
||||
|
||||
2. **Nothing there** — stop and hand the operator the command to run themselves:
|
||||
|
||||
```bash
|
||||
kettle auth add --name noodles --url https://git.example.com --user naudachu < token.txt
|
||||
pass show gitea/token | kettle auth add --name noodles --url https://git.example.com
|
||||
```
|
||||
|
||||
`--user` is documentation only. `kettle auth remove <name>` forgets one.
|
||||
|
||||
3. **Pin the choice into the project.** Ask `kettle config` first and only
|
||||
proceed if it answers with a project — `kettle init` in a directory that is
|
||||
not one would *create* a project there, which is the one statement that is
|
||||
never yours to make (`/kettle:init`):
|
||||
|
||||
```bash
|
||||
kettle config
|
||||
kettle init --login noodles
|
||||
```
|
||||
|
||||
`init` on an initialized project prints `already initialized — nothing to do`
|
||||
and rewrites only the settings it was given, so the repo pinned earlier stays.
|
||||
|
||||
4. Confirm with `kettle config`. Tell the operator which login is pinned and
|
||||
which file it went in. It is live immediately — nothing caches it, no restart.
|
||||
|
||||
```
|
||||
login noodles
|
||||
url https://git.example.com
|
||||
token (set)
|
||||
repo claude-skills/marketplace
|
||||
```
|
||||
|
||||
## When it goes wrong
|
||||
|
||||
| what you see | what it means |
|
||||
|---|---|
|
||||
| `no login "X" in …/logins.yaml — known: …` | the project pins a name this machine does not hold. Either add it (step 2) or pin one that is there |
|
||||
| `no .kettle/ found — searched up from …` | not a project. `/kettle:init`, and it is the operator's to run |
|
||||
| `401` / `403` from a sync command | report it verbatim. Do **not** try another login, and do not edit or remove one to route around it — that is somebody's identity, not a setting |
|
||||
| `token none` in `kettle config` | a name is pinned but no credential answers to it |
|
||||
|
||||
**`tea` does not read any of this.** The `tea` CLI keeps its own configuration
|
||||
under `$XDG_CONFIG_HOME/tea` and its own logins (`tea logins list`), and
|
||||
configuring one tool configures nothing in the other — see `/kettle:use`.
|
||||
|
||||
**No `kettle` on PATH?** `command not found: kettle` is the whole story. 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`.
|
||||
|
||||
The full flag table for `auth`, `config` and `init` is the generated block in
|
||||
`/kettle:project`.
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: init
|
||||
description: Make THIS directory a project that tracks issues — run `kettle init`, which creates the `.kettle/` marker every other command resolves the store from, migrates an older store in, and gitignores it. Operator-invoked only; carries the rules the binary cannot enforce — never inside a linked worktree, never an `--at` nobody named, never a migration clash resolved for them.
|
||||
argument-hint: "[--at DIR] [--login NAME] [--repo owner/name] [--dry-run]"
|
||||
disable-model-invocation: true
|
||||
allowed-tools: Bash(kettle init:*), Bash(kettle config:*), Bash(git rev-parse:*)
|
||||
---
|
||||
|
||||
# /kettle:init — make this directory a project
|
||||
|
||||
Initializing is a statement, and the operator makes it: *this* directory is the
|
||||
project whose issues live in it. Nothing infers it — `.git` is in every clone,
|
||||
and a tool that inferred its root from one wrote other projects' issues into its
|
||||
own versioned cache. It is answered once, by a person, and every command
|
||||
downstream reads the answer instead of guessing.
|
||||
|
||||
The binary does the work and is idempotent. What this skill carries is the three
|
||||
things it cannot decide for itself.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Refuse inside a linked worktree.** Two different paths mean one:
|
||||
|
||||
```bash
|
||||
git rev-parse --path-format=absolute --git-dir --git-common-dir
|
||||
```
|
||||
|
||||
Stop and say so. `.kettle/` is gitignored, so a worktree has no marker by
|
||||
design and reaches the main checkout's store on its own — the walk crosses to
|
||||
it through the `gitdir:` in the `.git` *file*. A marker here gives one project
|
||||
two stores, and the second is deleted with the branch. If anything needs
|
||||
initializing it is the main checkout, which is the second path's parent.
|
||||
|
||||
2. Run it, passing the operator's arguments through unchanged:
|
||||
|
||||
```bash
|
||||
kettle init $ARGUMENTS
|
||||
```
|
||||
|
||||
With no `--at` it initializes the current directory. **Never supply an `--at`
|
||||
the operator did not name.** Which directory is the project is the one
|
||||
question this command exists to have a person answer; picking a plausible one
|
||||
is the failure the marker replaces.
|
||||
|
||||
3. Report what it printed, verbatim. `already initialized — nothing to do` is a
|
||||
success, not something to work around.
|
||||
|
||||
`--login` pins a login by name and `--repo` names the tracker repository; both
|
||||
are optional and both can be added later by running `init` again — it writes the
|
||||
config without disturbing settings it was not given. Neither is a credential:
|
||||
the tokens live in one file per machine, `/kettle:auth`.
|
||||
|
||||
## When it stops
|
||||
|
||||
- **A name clash on the migration** — the same file name on both sides. It exits
|
||||
having changed nothing and names the files. Report that. Do **not** move,
|
||||
delete, or merge either side: one of them may be an `origin: local` issue,
|
||||
which *is* the issue and the only copy of that work. The operator decides
|
||||
which survives.
|
||||
- **A marker already exists above this directory.** A second one gives that
|
||||
project a second store and the nearer one wins. Confirm with the operator
|
||||
before going ahead; usually they are standing in a subdirectory and there is
|
||||
nothing to do.
|
||||
|
||||
**No `kettle` on PATH?** `command not found: kettle` is the whole story. 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`.
|
||||
|
||||
## After
|
||||
|
||||
- `/kettle:issue` works now — offline, no login, no network.
|
||||
- `/kettle:auth` puts a token on this machine and pins the login this project
|
||||
runs under; needed only for the tracker side, `/kettle:sync`.
|
||||
- `kettle config` prints every path and setting this directory resolved to, and
|
||||
is the first thing to run when something looks like it landed in the wrong
|
||||
place.
|
||||
|
||||
The full flag table for `init` is the generated block in `/kettle:project`.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,161 @@
|
||||
---
|
||||
name: project
|
||||
description: Generated flag reference for the project-level `kettle` commands — `kettle init`, `kettle auth`, `kettle config`, `kettle gen`. Load it to look up the exact flags and defaults of one of those four, or when a command answers "no project" / "no login" and you need `kettle config` to say what this directory resolved to. The rules around initializing are /kettle:init and the credential workflow is /kettle:auth; this file is the flag table both of them point at.
|
||||
---
|
||||
|
||||
# kettle project — the project itself
|
||||
|
||||
`kettle` resolves everything from one marker. `<project>/.kettle/` is created by
|
||||
`kettle init` and never inferred: `.git` is in every clone, so a tool that
|
||||
guessed a root from one would write issues into whatever tree it happened to be
|
||||
standing in. With no marker anywhere the command stops and names the directories
|
||||
it searched — that is an answer, not a fallback.
|
||||
|
||||
Two configuration files, and the split is the point. `<project>/.kettle/config.yaml`
|
||||
holds the tracker repository and the **name** of a login; the name is worth
|
||||
nothing on its own, which is what makes it safe inside a working tree.
|
||||
`~/.config/kettle/logins.yaml` (0600, one per machine, `$KETTLE_CONFIG_HOME` or
|
||||
`$XDG_CONFIG_HOME` move it) holds the tokens. `KETTLE_LOGIN`, `KETTLE_REPO`,
|
||||
`KETTLE_URL` and `KETTLE_TOKEN` each override the file they shadow.
|
||||
|
||||
**No `kettle` on PATH?** `command not found: kettle` is the whole story — no
|
||||
script and no `tea` invocation substitutes for it. 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`.
|
||||
|
||||
`kettle gen` is a maintainer command: it rewrites the generated region of these
|
||||
SKILL.md files from the command registry the binary was built from. Run it after
|
||||
changing the CLI, never to "fix" documentation by hand.
|
||||
|
||||
<!-- 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 auth list | add | remove <name>`
|
||||
|
||||
manage the tokens this machine holds
|
||||
|
||||
Credentials live in one file per machine, outside every working tree, mode
|
||||
0600. A project pins a login by NAME; the name is worth nothing on its own,
|
||||
which is what makes it safe to keep in a file inside the repository.
|
||||
|
||||
The token is read from standard input unless --token is given, because an
|
||||
argument is in the shell history the moment it is typed:
|
||||
|
||||
kettle auth add --name noodles --url https://git.example.com < token.txt
|
||||
pass show gitea/token | kettle auth add --name noodles --url https://git.example.com
|
||||
|
||||
`list` never prints a token. There is no flag to make it.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--name` | — | login name (add) |
|
||||
| `--token` | — | token, if you would rather not use stdin (add) |
|
||||
| `--url` | — | instance URL, e.g. https://git.example.com (add) |
|
||||
| `--user` | — | account this token belongs to; documentation only (add) |
|
||||
|
||||
```bash
|
||||
kettle auth list # what this machine holds
|
||||
pass show gitea | kettle auth add --name noodles --url https://git.example.com # add one, token on stdin
|
||||
kettle auth remove noodles # forget it
|
||||
```
|
||||
|
||||
## `kettle config`
|
||||
|
||||
show what this project resolved to
|
||||
|
||||
Every path and every setting, with the overrides already applied, so a run that
|
||||
went somewhere unexpected can be explained without guessing.
|
||||
|
||||
The token is never printed — only whether one was found.
|
||||
|
||||
This is the command to reach for when the store looks empty, when a push says
|
||||
401, or when two directories disagree about which project they are in.
|
||||
|
||||
```bash
|
||||
kettle config # resolved paths and settings
|
||||
```
|
||||
|
||||
## `kettle gen skills`
|
||||
|
||||
write the plugin's SKILL.md files from the command registry
|
||||
|
||||
A SKILL.md tells an agent how to invoke this binary. Hand-written, it drifts: a
|
||||
flag is renamed here and the documentation goes on recommending the old one,
|
||||
and the agent that reads it fails in a way nobody traces back to a stale
|
||||
sentence. Everything those files say about a command — its usage line, its
|
||||
flags with their defaults, its worked examples — is already in the registry
|
||||
this binary is built from, so it is written from there and cannot disagree.
|
||||
|
||||
THE GENERATOR OWNS A REGION, NOT A FILE. Each SKILL.md carries a pair of HTML
|
||||
comment markers — `kettle:gen` to open and `/kettle:gen` to close, both written in
|
||||
the `<!-- … -->` form and visible at the top and bottom of the block below.
|
||||
Everything between them is replaced on every run; every byte outside them comes
|
||||
back exactly as it was, which matters most for `description:`, the prose that
|
||||
decides whether an agent loads the skill at all, and the one thing here that no
|
||||
generator can write.
|
||||
|
||||
A file with no markers is REPORTED AND LEFT ALONE, never overwritten: clobbering
|
||||
somebody's prose because they forgot a marker is the failure this design exists
|
||||
to prevent. A file that does not exist yet is created with a frontmatter stub
|
||||
around a generated block, for a human to fill in.
|
||||
|
||||
The output is deterministic to the byte — no timestamps, no map iteration — so
|
||||
regenerating something that has not changed produces no diff. --check is that
|
||||
property made useful: it writes nothing and exits 1 when any file on disk
|
||||
differs from what would be generated, which is what a pre-commit hook or a CI
|
||||
step calls. It wins over --dry-run when both are given.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--check` | `false` | write nothing, exit 1 if anything is out of date |
|
||||
| `--dry-run` | `false` | print what would change; write nothing |
|
||||
| `--out` | — | directory the skills live in; one <group>/SKILL.md under it |
|
||||
|
||||
```bash
|
||||
kettle gen skills --out ../plugins/kettle/skills # write the region in every group's SKILL.md
|
||||
kettle gen skills --out ../plugins/kettle/skills --dry-run # print what would change; write nothing
|
||||
kettle gen skills --out ../plugins/kettle/skills --check # exit 1 if the docs are out of date
|
||||
```
|
||||
|
||||
## `kettle init`
|
||||
|
||||
make this directory a project that tracks issues
|
||||
|
||||
Creates `.kettle/` — the marker every other command resolves the store from,
|
||||
and `.kettle/config.yaml`, which says which tracker repository these issues
|
||||
belong to and which login to reach it under.
|
||||
|
||||
The marker is deliberately something an operator makes, not something inferred
|
||||
from the tree: `.git` is in every clone, so anything that inferred a root from
|
||||
one would write issues into whatever it happened to be installed in.
|
||||
|
||||
--login pins a name, never a credential. The tokens live in one file per
|
||||
machine, outside every working tree, managed with `kettle auth`.
|
||||
|
||||
All of it is idempotent: it creates .kettle/issues and .kettle/payload, migrates
|
||||
an older store in if it finds one (either layout the tea plugin used, oldest
|
||||
first), writes the config without disturbing settings it was not given, and adds
|
||||
.kettle/ to .gitignore. Each migration is a move, not a copy — two stores is the
|
||||
state the marker exists to prevent — and it refuses to pick a winner when both
|
||||
sides hold a file of the same name.
|
||||
|
||||
Do NOT run this inside a linked worktree. A worktree is the same project on
|
||||
another branch and reaches the store by a hop out to the main checkout; a marker
|
||||
here would give one project two stores, and the directory holding the second one
|
||||
disappears with the branch.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--at` | — | directory to initialize (default: the working directory) |
|
||||
| `--dry-run` | `false` | report what would happen; change nothing |
|
||||
| `--login` | — | name of a login in the machine-wide file (see `kettle auth`) |
|
||||
| `--repo` | — | tracker repository, as owner/name |
|
||||
|
||||
```bash
|
||||
kettle init # initialize the current directory
|
||||
kettle init --login noodles --repo claude-skills/marketplace # and point it at a tracker
|
||||
kettle init --at ~/code/x # initialize somewhere else
|
||||
kettle init --dry-run # say what it would do, touch nothing
|
||||
```
|
||||
<!-- /kettle:gen -->
|
||||
@@ -0,0 +1,569 @@
|
||||
---
|
||||
name: sync
|
||||
description: Move issues between this project's local store and its Gitea tracker with the `kettle` binary — pull issues into `.kettle/issues/`, push local ones up (which deletes the local file), list what the tracker holds, post comments, close and reopen, and evict what the tracker says is closed. Load when the user asks to fetch or publish an issue, see what exists in the tracker, comment on one, or close one. Writing, grepping, validating and graphing an issue's content is /kettle:issue and needs no network.
|
||||
---
|
||||
|
||||
# /kettle:sync — the bridge between the local store and the tracker
|
||||
|
||||
One job: carry issues between `<project>/.kettle/issues/<id>.md` and Gitea.
|
||||
Everything about **what an issue is** — format, types, validation, the dependency
|
||||
graph — belongs to `/kettle:issue`, and this layer neither redefines nor
|
||||
second-guesses it. Knowledge flows one way: delete the tracker from the world and
|
||||
the issue domain does not notice.
|
||||
|
||||
**No `kettle` on PATH?** `command not found: kettle` is the whole story — the
|
||||
Python scripts this plugin used to ship are gone and raw `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`.
|
||||
|
||||
## No `--login`, no `--repo`, no guard
|
||||
|
||||
Which login this project runs under and which repository its issues belong to
|
||||
are facts about the project, stated once by `kettle init` and kept in
|
||||
`.kettle/config.yaml`; the token lives in one file per machine that no working
|
||||
tree can see. There is nothing to pass and nothing to police — the old PreToolUse
|
||||
guard hook and its `--login "$GITEA_LOGIN"` placeholder are gone, along with the
|
||||
failure they existed to catch. `kettle labels --repo owner/name` is the
|
||||
single exception, because bootstrapping a repository's label set is the one
|
||||
operation whose target is not this project.
|
||||
|
||||
A cross-repository *address* is still an address: `kettle pull owner/repo#42`
|
||||
re-points the client for that one call and comes back with the same credentials
|
||||
and the same scratchpad. `42`, `#42`, `owner/repo#42` and a full issue URL are
|
||||
four spellings of one key.
|
||||
|
||||
No login, an unknown login name, a 401: report it and stop — `/kettle:auth`.
|
||||
|
||||
## Never read an issue through a raw API dump
|
||||
|
||||
`tea issues 42 -o json` and `tea api …/issues/42` put the whole payload —
|
||||
avatars, nested user objects, every comment body — into your context whether you
|
||||
need it or not. `kettle pull` writes flat markdown and prints a compact line per
|
||||
issue; `kettle remote` lists the tracker without writing anything at all. Use
|
||||
those.
|
||||
|
||||
## The round trip is one rule
|
||||
|
||||
**The store holds what has not left this machine.**
|
||||
|
||||
A successful `kettle push` deletes `<id>.md` and every sidecar under that slug —
|
||||
on create and on `--update` alike, one rule with no exception — and prints the
|
||||
number and URL the issue now lives at. Once the tracker has the issue, the
|
||||
tracker *is* the issue.
|
||||
|
||||
The deletion is the last thing that happens, and only after all three of: the
|
||||
call came back 2xx, the answer carries the number that was written, and the
|
||||
number → slug ledger has been written. Network down, a 422, an answer about
|
||||
another issue — the file stays exactly where it is and the run stops.
|
||||
|
||||
**An `origin: local` issue that has never been pushed is never touched by any of
|
||||
this.** That file is the only copy of that work.
|
||||
|
||||
The slug survives the round trip two ways over, which is why the file can be
|
||||
deleted at all:
|
||||
|
||||
| where | survives |
|
||||
|---|---|
|
||||
| `<!-- kettle:id wire-sqlc-appclick -->`, first line of the **tracker-side** body | a rename in the web UI, a lost ledger, a fresh clone, another machine |
|
||||
| `.kettle/issues/.remote.json`, number → slug | the local file being deleted |
|
||||
|
||||
The marker never appears in the local file: one is put at the top on the way up
|
||||
and every one is stripped on the way down. A pull consults the ledger first (it
|
||||
is the one that knows what is on disk *now*), then the marker, then slugifies the
|
||||
title for an issue filed in the web UI that never had a local name — and a marker
|
||||
is taken at its word only when that slug is free, because a name already in use
|
||||
is a collision and not an identity.
|
||||
|
||||
Nothing prunes the ledger — not a push, not an eviction. Its entries are meant to
|
||||
outlive the files they name.
|
||||
|
||||
## Pulling is a fetch, not a merge
|
||||
|
||||
A pull overwrites the body. Unpushed local edits are lost, with exactly one
|
||||
exception: **checkbox state**. A tick is monotone, so for a checkbox line whose
|
||||
text matches on both sides `[x]` wins from either — tick it in the web UI, tick
|
||||
it locally, tick it in both, the tick survives. The price is real and stated:
|
||||
**a box unticked in the web UI comes back on the next pull.** Untick locally, then
|
||||
`kettle push --update`.
|
||||
|
||||
Two spellings, and they are different operations:
|
||||
|
||||
- **by key** — an address. It fetches the issue in **any** state, because a
|
||||
number is not a question about state.
|
||||
- **by filter** (`--milestone`, `--label`, `-q`) — a query. Closed issues are
|
||||
enumerated and left out, and `--limit` bounds what is **stored**, never what is
|
||||
read.
|
||||
|
||||
Do not loop over numbers to fetch a group; pass the filter. And a pull returns
|
||||
the **unit of work**, not one row of it: blockers come down with it recursively
|
||||
to `--depth`, which costs a request per issue and per outside blocker. That is
|
||||
what `--no-deps` buys back. A blocker the filter did not select still lands on
|
||||
disk, deliberately — it is there because a stored issue named it.
|
||||
|
||||
Comments ride along into `<id>.comments.md` with no flag, and cost nothing when
|
||||
the payload says the thread is empty. **They are pull-only in the store**: editing
|
||||
that file changes nothing in the tracker. `kettle comment` is the way, and it
|
||||
refetches the thread after writing so the local copy is not stale by the comment
|
||||
it just made.
|
||||
|
||||
## Closing, and evicting what the tracker says is closed
|
||||
|
||||
`kettle close` sends `{"state": …}` and nothing else — no title, no body, no
|
||||
labels. **Closing is not an edit**; editing is pull → change → `push --update`.
|
||||
Explicit ids only: there is no `--milestone` and no `--label`, because which
|
||||
issues are finished is a judgement about content and this command only carries
|
||||
one out. The local file is rewritten only after the tracker confirms that very
|
||||
write. A tracker that refuses to close an issue its own dependency graph still
|
||||
blocks says so in its own words — close the blockers first, or unlink them.
|
||||
|
||||
`kettle sync-evict` is `kettle evict` with one thing in front of it: a `state:`
|
||||
that is not stale. Every candidate is asked about **before anything is removed**,
|
||||
and one bad answer evicts nothing at all — not even the issues whose answers had
|
||||
already arrived. `origin: local` is never asked about and never evicted; a
|
||||
tracked issue whose handle is unreadable is reported and kept.
|
||||
|
||||
## Labels
|
||||
|
||||
Push creates the labels its issues happen to use, which means a repository grows
|
||||
the set in pieces and nobody can filter by `type/bug` in the web UI until
|
||||
somebody pushes a bug. `kettle labels` lays the canonical `type/*` and
|
||||
`severity/*` set down in one run instead. An exact name is left alone; a
|
||||
**lookalike** (`bug`, `Bug`, `kind/bug`, `type: bug`) is reported with its id and
|
||||
never touched, because renaming somebody else's label is a decision and not a
|
||||
step; colour or `exclusive` drift is corrected only under `--fix`. `tech/*` and
|
||||
`comp/*` stay open-ended and push-created. A milestone must already exist — push
|
||||
attaches, it never creates.
|
||||
|
||||
## What crosses the boundary, and what does not
|
||||
|
||||
| domain | tracker | note |
|
||||
|---|---|---|
|
||||
| `id` (slug) | `<!-- kettle:id … -->` | first line of the tracker-side body; stripped out of the local copy |
|
||||
| title, body | `title`, `body` | verbatim, both ways, except the marker and the checkbox union |
|
||||
| `state` | `state` | same vocabulary |
|
||||
| `labels` | `labels[]` | names both ways |
|
||||
| `assignees` | `assignees[]` | logins |
|
||||
| `milestone` | `milestone.title` | resolved to an id on write |
|
||||
| `depends` | native issue links | slugs here, `{index, owner, repo}` there; push writes them, a pull reads them back |
|
||||
| — | `ref` | lands in `branch:`; push fills an empty one with the current git branch |
|
||||
| — | `number`, `html_url` | land in `gitea:` / `url:` |
|
||||
|
||||
`depends:` is always slugs, and the body's `## Depends on` prose is passed
|
||||
through **unchanged** in both directions — a translator that edits prose churns
|
||||
the body on every round trip. The edge the tracker acts on is the native link,
|
||||
not the text, which is exactly why the text can be left alone. Push only ever
|
||||
**adds** links: a dependency deleted from `depends:` leaves its tracker link
|
||||
standing, and unlinking is a web UI job.
|
||||
|
||||
Dependencies go up in topological order so a blocker has its number before the
|
||||
issue that names it. One that is still local-only gets no link and is reported,
|
||||
never silently dropped.
|
||||
|
||||
## Drift
|
||||
|
||||
There is none tracked, and there is very little left to track: a published issue
|
||||
has **one** copy — the tracker's — except while somebody is working on it, and
|
||||
that window closes at the next push. Nothing watches the tracker, nothing
|
||||
reconciles, nothing warns that a synced issue changed upstream. `synced:` says how
|
||||
old your working copy is, `remote-updated:` what the server said at that moment.
|
||||
The old question — "I edited this locally, does the server have it, whose text is
|
||||
newer?" — is answered by the store's contents rather than by a mechanism: **a file
|
||||
that is here has not been pushed.**
|
||||
|
||||
The checkbox union is not an exception. It reads only the two bodies in front of
|
||||
it; there is no base version and no way for it to report that anything diverged.
|
||||
|
||||
## Payloads
|
||||
|
||||
Every request body goes to `<project>/.kettle/payload/` first and is kept there
|
||||
for a retry or a post-mortem. It is a **sibling** of the store and never a child:
|
||||
request bodies are debris of the transport, and a scratchpad inside a store makes
|
||||
`ls .kettle/issues` lie about what exists. Nothing in it is anybody's only copy —
|
||||
deleting it costs nothing. A run that sends nothing leaves no directory behind.
|
||||
|
||||
For Gitea entities `kettle` does not cover — releases, webhooks, actions, pull
|
||||
requests — the tool is `tea`, and it keeps its own configuration and its own
|
||||
logins. `/kettle:use`.
|
||||
|
||||
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 close <id|number> [<id|number>…]`
|
||||
|
||||
close or reopen issues in the tracker, and on disk with them
|
||||
|
||||
STATE ONLY. This sends `{"state": …}` and nothing else: no title, no body, no
|
||||
labels, no milestone. Editing an issue is `kettle pull` -> edit ->
|
||||
`kettle push --update`; closing it is not an edit.
|
||||
|
||||
EXPLICIT IDS ONLY. No --milestone, no --label, no "close everything that looks
|
||||
done". Which issues are finished is a judgement about content; this carries that
|
||||
judgement out, one named id at a time. Nothing here deletes an issue either —
|
||||
the tracker can, and it is not an operation of this workflow.
|
||||
|
||||
WHAT MAY BE NAMED: a local slug, or a tracker key (42, #42, owner/repo#42, an
|
||||
issue URL). Both, and for the same reason: a push deletes the local file, so
|
||||
most issues in the tracker have no slug on disk to name them by. A slug is
|
||||
resolved through the file's `gitea:` handle when the file is there, and through
|
||||
the ledger (`.remote.json`) when push has already dropped it. A bare number is
|
||||
this project's repository; a qualified key names its own, so a foreign #42 can
|
||||
never be closed against the repository that happens to be configured here.
|
||||
|
||||
An `origin: local` issue cannot be closed. It is not in the tracker, so there is
|
||||
no state there to change, and the run stops naming the id rather than quietly
|
||||
editing one field of a local file. Push it first, or delete it.
|
||||
|
||||
THE LOCAL FILE IS WRITTEN ONLY AFTER THE TRACKER CONFIRMS: the answer has to be
|
||||
the very issue that was patched, in the state that was asked for. Anything else
|
||||
and the file is left exactly as it was. An issue whose local copy is gone
|
||||
(pushed and dropped) is closed in the tracker and nothing is written; the state
|
||||
comes down with the next pull.
|
||||
|
||||
A tracker that refuses to close an issue its own dependency graph still blocks
|
||||
says so in the answer, and the run stops with its words: close the blockers
|
||||
first, or unlink them.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--dry-run` | `false` | print what would change; makes no request |
|
||||
| `--out` | — | store root (default: <project>/.kettle/issues) |
|
||||
| `--reopen` | `false` | set the state back to open instead of closed |
|
||||
|
||||
```bash
|
||||
kettle close wire-sqlc-appclick # one issue, by slug
|
||||
kettle close wire-sqlc-appclick 42 #43 # several, by slug or number
|
||||
kettle close --reopen 42 # the same thing backwards
|
||||
kettle close --dry-run 42 43 # what would change; no request at all
|
||||
```
|
||||
|
||||
## `kettle comment <id>`
|
||||
|
||||
post or edit a comment on a synced issue
|
||||
|
||||
The target is a LOCAL ID, not a number. Which issue this is, is a fact about the
|
||||
work; where it lives in the tracker is bookkeeping, and the `gitea:` handle on the
|
||||
file is what turns one into the other. An `origin: local` issue cannot be
|
||||
commented on at all — it is not in the tracker, so there is nothing there to
|
||||
comment on; push it first.
|
||||
|
||||
The body comes from a file or from --body, and multi-line prose is what --file
|
||||
is for. This is why comments go through the API rather than through a tracker
|
||||
CLI: an entity command with an empty-looking positional opens $EDITOR, and on a
|
||||
TTY that does not exist it hangs forever.
|
||||
|
||||
After the write the whole thread is refetched into `<id>.comments.md`, so the
|
||||
local copy is not stale by one comment — the one this run just made.
|
||||
|
||||
COMMENTS ARE PULL-ONLY IN THE STORE. Nothing round-trips them back: editing
|
||||
`<id>.comments.md` by hand changes nothing in the tracker. Use --edit with a
|
||||
comment id for that.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--body` | — | comment body inline (short, single-line) |
|
||||
| `--edit` | `0` | comment id to rewrite, instead of posting a new one |
|
||||
| `--file` | — | markdown file holding the comment body |
|
||||
| `--out` | — | store root (default: <project>/.kettle/issues) |
|
||||
|
||||
```bash
|
||||
kettle comment wire-sqlc-appclick --file notes.md # post the contents of a file
|
||||
kettle comment wire-sqlc-appclick --body "готово, задеплоено" # post one line
|
||||
kettle comment wire-sqlc-appclick --file fix.md --edit 1234 # rewrite comment 1234 instead
|
||||
```
|
||||
|
||||
## `kettle labels`
|
||||
|
||||
put the canonical type/* and severity/* labels into a repository
|
||||
|
||||
Every `type/*` and every `severity/*` the domain taxonomy defines, created up
|
||||
front instead of trickling in as a side effect of whichever push first happens
|
||||
to use one. Until a name exists in the repository nobody can filter by it in the
|
||||
web UI, so somebody makes their own — foreign colour, no `exclusive` — and the
|
||||
set arrives in pieces over months.
|
||||
|
||||
NO LABEL NAME IS SPELLED OUT HERE. The names come from the domain taxonomy and
|
||||
are painted by the mapping layer, because a hex code is how a tracker paints a
|
||||
chip and not what an issue is. Add a type over in the domain and the next run
|
||||
creates it.
|
||||
|
||||
THE REPOSITORY'S OWN LABELS ARE READ BEFORE ANYTHING IS WRITTEN, and read from
|
||||
the repository, never from a cache — a cache answers "what did we create last
|
||||
time" and the question here is "what does this repository have right now". A
|
||||
name that matches exactly is left alone; a colour or `exclusive` that disagrees
|
||||
with the spec is reported, and corrected only under --fix. A name that merely
|
||||
RESEMBLES a canonical one (the same tail, up to case, separator and whatever
|
||||
namespace is in front: `x`, `X`, `kind/x`, `type: x` against `type/x`) is
|
||||
reported with its id and never touched — renaming somebody else's label is a
|
||||
decision, not a step.
|
||||
|
||||
Out of scope by design: `tech/*` and `comp/*`, which are open-ended and are
|
||||
created by push as they come up, and deleting or renaming anything at all. Only
|
||||
repository labels are read; an organization's own labels sit behind a different
|
||||
endpoint and are neither read nor written.
|
||||
|
||||
The issue store is out of scope too, and not incidentally: a label belongs to
|
||||
the repository and not to any issue, so this neither reads the store nor creates
|
||||
it. Request bodies go to the transport's own scratchpad, which is a sibling of
|
||||
the store and never a child.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--dry-run` | `false` | print the plan; not one writing request |
|
||||
| `--fix` | `false` | also patch colour/exclusive on labels that already exist |
|
||||
| `--repo` | — | repository to bootstrap, as owner/name (default: this project's) |
|
||||
|
||||
```bash
|
||||
kettle labels --dry-run # print the plan; not one writing request
|
||||
kettle labels # create whatever is missing
|
||||
kettle labels --fix # also patch colour / exclusive drift
|
||||
kettle labels --repo owner/name # bootstrap another repository
|
||||
```
|
||||
|
||||
## `kettle pull [<key>…]`
|
||||
|
||||
fetch issues from the tracker into the local store
|
||||
|
||||
THIS IS HOW A PUSHED ISSUE COMES BACK. `kettle push` deletes the local file the
|
||||
moment the tracker confirms the write, so a pull is not a refresh of a copy you
|
||||
kept — it is how the copy comes to exist at all.
|
||||
|
||||
It lands under the SAME slug it had before, after a rename in the web UI and on
|
||||
a machine that has never seen the issue. Three sources answer "what is this
|
||||
issue called here", in this order:
|
||||
|
||||
.remote.json the number -> slug ledger; the only one that knows what
|
||||
is on disk right now, so it wins
|
||||
<!-- kettle:id … --> the marker in the tracker-side body; it survives a lost
|
||||
ledger, a fresh clone, another machine, and a retitling
|
||||
the title slugified — where an issue filed in the web UI gets its
|
||||
first local name
|
||||
|
||||
A marker is taken at its word only when the slug is free; a name already in use
|
||||
is a collision, not an identity, and is uniquified rather than allowed to
|
||||
overwrite somebody else's issue. The marker itself is stripped out of what lands
|
||||
on disk.
|
||||
|
||||
TWO WAYS TO NAME WHAT TO PULL, and they are not the same operation:
|
||||
|
||||
kettle pull 42 #43 owner/repo#44 by key — an ADDRESS
|
||||
kettle pull --milestone v0.2 by filter — a QUERY
|
||||
|
||||
A key fetches an issue in ANY state, because a number is an address and not a
|
||||
question about state. Only filter mode leaves closed issues out — a closed issue
|
||||
is not a unit of work — and only `--state closed` puts one in the store. An issue
|
||||
already on disk is refreshed either way, so a local copy learns it was closed
|
||||
instead of staying open forever, and the count that stayed out goes to stderr.
|
||||
|
||||
`--limit` IS ON THE WRITE, NOT ON THE SELECTION. It counts the issues this run puts
|
||||
in the store and never the closed ones it enumerated and threw away, so pages
|
||||
keep coming until the budget is full — and stop the moment it is. A filter that
|
||||
matches almost only closed issues ends in a warning and a short answer rather
|
||||
than a walk of the whole tracker.
|
||||
|
||||
A PULL RETURNS THE UNIT OF WORK, NOT ONE ROW OF IT. `depends:` is filled from the
|
||||
tracker's own dependency graph and every blocker comes down with it, recursively,
|
||||
to --depth. What that costs, stated rather than hidden: one request per issue
|
||||
that lands in the store, plus one per blocker the selection did not already
|
||||
carry. `--no-deps` is the way back to one request, and narrows the answer to the
|
||||
one issue you asked for. Dependencies are outside --limit: a blocker is followed
|
||||
because a stored issue named it, not because the filter selected it, so a
|
||||
filtered pull can leave more files behind than its limit — including one from
|
||||
another milestone. The one blocker that does not land is a closed one.
|
||||
|
||||
PULLING OVERWRITES THE BODY: a fetch, not a merge. Local edits you have not
|
||||
pushed are lost, with exactly one exception — checkbox state. A tick is monotone,
|
||||
so a `[x]` on either side wins for any item whose text matches; unticking is not,
|
||||
so untick locally and push. `--cached` skips an issue before any of that.
|
||||
|
||||
Comments ride along: the thread lands beside the issue in <id>.comments.md. It
|
||||
costs no request when the payload says there are none, and a file left over from
|
||||
an earlier pull is deleted — so no file means "no comments", never "not asked
|
||||
for". The thread is pull-only; post with `kettle comment`.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--cached` | `false` | skip issues already on disk instead of refetching |
|
||||
| `--depth` | `3` | how deep to follow blockers |
|
||||
| `--label` | — | filter by label; repeat for AND |
|
||||
| `--limit` | `100` | filter mode: how many issues to STORE, not to enumerate |
|
||||
| `--milestone` | — | pull a whole milestone (id or title) |
|
||||
| `--no-deps` | `false` | do not fill depends: and do not follow blockers |
|
||||
| `--out` | — | store root (default: <project>/.kettle/issues) |
|
||||
| `--q` | — | search text in title and body |
|
||||
| `--query` | — | the long spelling of -q |
|
||||
| `--state` | `open` | filter mode only: open, closed or all |
|
||||
|
||||
```bash
|
||||
kettle pull 42 # the issue and everything blocking it, in any state
|
||||
kettle pull 42 --no-deps # just that one issue — one request
|
||||
kettle pull owner/repo#42 # an issue in another repository
|
||||
kettle pull --milestone v0.2 --limit 20 # 20 open issues from a milestone, blockers included
|
||||
kettle pull --label type/bug --state all # every bug; the closed ones are enumerated, not stored
|
||||
```
|
||||
|
||||
## `kettle push [<id>…]`
|
||||
|
||||
send local issues to the tracker; the local copy goes with them
|
||||
|
||||
A SUCCESSFUL PUSH DELETES THE LOCAL FILE — <id>.md and every sidecar under that
|
||||
slug — and prints the number and the URL the issue now lives at. Once the tracker
|
||||
has the issue, the tracker IS the issue: what is left in the store is what has
|
||||
not left this machine. Get it back with `kettle pull <n>`, which returns it under
|
||||
the same slug, because the slug travelled up in the body as <!-- kettle:id … -->
|
||||
and was recorded in the number -> slug ledger.
|
||||
|
||||
ONE RULE, NO EXCEPTION: --update deletes as well. A PATCH is a push, and an issue
|
||||
that has just been sent is no more local than one that was just created. Two
|
||||
rules would put back exactly the question this removes — "is my copy the fresh
|
||||
one?".
|
||||
|
||||
THE DELETION IS THE LAST THING THAT HAPPENS TO AN ISSUE, and only after all
|
||||
three of:
|
||||
|
||||
1. the call came back without an error and with a 2xx,
|
||||
2. the answer carries a plausible number — on --update the very number that
|
||||
was PATCHed, and
|
||||
3. the ledger has been written with number -> slug.
|
||||
|
||||
Network down, non-2xx, an answer that does not confirm the write: the file stays
|
||||
and the run stops. Nothing removes a file it has not just watched the tracker
|
||||
accept, and nothing removes a file for an issue it did not send — `origin: local`
|
||||
work that has never been pushed is never touched by any of this. Get the ordering
|
||||
wrong and a slug is lost at exactly the moment the local copy stops being the
|
||||
record, which is why the ledger is written before anything is deleted and not
|
||||
after.
|
||||
|
||||
Every issue is validated against the canonical format first, offline and before
|
||||
a socket is opened. --force posts anyway; say why when you use it.
|
||||
|
||||
DEPENDENCIES GO FIRST, in topological order, so a blocker has its number before
|
||||
the issue that names it. Every `depends:` entry that has a number becomes a NATIVE
|
||||
tracker link — the same /dependencies a pull reads back, so the tracker shows the
|
||||
blocking panel and refuses to close a blocked issue first. A link that is already
|
||||
there is skipped, not re-POSTed, which is what makes a repeat push a no-op. A
|
||||
dependency that is still local-only has no number and becomes no link: it is
|
||||
reported, never silently dropped.
|
||||
|
||||
REMOVING a link is out of scope — push only ever adds. A dependency deleted from
|
||||
`depends:` leaves its tracker link standing; unlink it in the web UI.
|
||||
|
||||
The `## Depends on` prose is never touched: slugs stay slugs and are not rewritten
|
||||
to #N, so a pull -> push round trip is byte for byte.
|
||||
|
||||
Labels the repository is missing are created with the canonical colour and, for
|
||||
type/* and severity/*, exclusive: true. `branch:` carries the tracker's `ref`: an
|
||||
empty one is filled with the current git branch and an already-set one is sent as
|
||||
written. Detached HEAD or no repository at all is not an error — no ref is sent
|
||||
and a warning says so.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--dry-run` | `false` | validate and print the plan; no network, nothing deleted |
|
||||
| `--force` | `false` | push despite format violations |
|
||||
| `--out` | — | store root (default: <project>/.kettle/issues) |
|
||||
| `--update` | `false` | PATCH issues that already carry a gitea: field |
|
||||
|
||||
```bash
|
||||
kettle push # every issue the tracker does not have yet, blockers first
|
||||
kettle push wire-sqlc-appclick # one issue
|
||||
kettle push --update wire-sqlc-appclick # PATCH one that is already there — the file still goes
|
||||
kettle push --dry-run # validate and print the plan; no network, nothing deleted
|
||||
```
|
||||
|
||||
## `kettle remote`
|
||||
|
||||
list what exists in the tracker, one line each
|
||||
|
||||
Discovery only: this prints and WRITES NOTHING. The local store is a store, not a
|
||||
search-results folder, and a listing that landed in it would leave files nobody
|
||||
asked for beside the issues somebody did. Pick the numbers here, then pull them.
|
||||
|
||||
#42 open type/task, tech/sql Wire sqlc into the repo layer
|
||||
└─ local: wire-sqlc-appclick
|
||||
|
||||
The second line appears when the number is already in the local ledger, so it is
|
||||
obvious what a pull would refresh and what it would add.
|
||||
|
||||
--limit here caps the LISTING: N lines out, closed ones among them. That is not
|
||||
what the same flag means to `kettle pull`, and the difference is not an oversight —
|
||||
pull bounds what it WRITES, this command writes nothing, and enumeration is the
|
||||
whole job.
|
||||
|
||||
Projects are not filterable: the projects API is not exposed by Gitea. Use
|
||||
milestones or labels, or the web UI.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--label` | — | filter by label; repeat for AND |
|
||||
| `--limit` | `30` | how many lines to print |
|
||||
| `--milestone` | — | milestone id or title |
|
||||
| `--out` | — | store root (default: <project>/.kettle/issues) |
|
||||
| `--q` | — | search text in title and body |
|
||||
| `--query` | — | the long spelling of -q |
|
||||
| `--state` | `open` | open, closed or all |
|
||||
|
||||
```bash
|
||||
kettle remote # the open issues, 30 of them
|
||||
kettle remote --state all --label type/bug --limit 50 # every bug, open and closed
|
||||
kettle remote --milestone v0.2 # what is in a milestone
|
||||
kettle remote -q sqlc # keyword search over title and body
|
||||
```
|
||||
|
||||
## `kettle sync-evict [<id>…]`
|
||||
|
||||
refresh state from the tracker, then evict what is closed
|
||||
|
||||
`kettle evict` is the command that decides and deletes. This adds exactly one
|
||||
thing in front of it: a `state:` that is not stale. A local `state:` is only as
|
||||
fresh as the last pull, so an issue closed in the web UI an hour ago still reads
|
||||
`open` here and the offline command will — correctly — leave it alone. That is
|
||||
the gap this closes, and before it existed the operator had to pull the five
|
||||
closed issues back onto disk before anything could remove them.
|
||||
|
||||
ORDER OF OPERATIONS, AND IT IS THE WHOLE SAFETY ARGUMENT:
|
||||
|
||||
1. every candidate's state is fetched — ALL of them, before anything is
|
||||
removed;
|
||||
2. each answer must be the issue that was asked about, in a state the domain
|
||||
recognizes;
|
||||
3. only then is the eviction run, by handing the refreshed issues to the
|
||||
domain — the same decision, the same deletion, the same protection of
|
||||
`origin: local`, in one place.
|
||||
|
||||
A dead connection, a non-2xx, an answer about another issue, a state nobody
|
||||
recognizes: the run stops at step 2 and NOTHING is deleted, not even the issues
|
||||
whose answers had already arrived. That is stricter than push, which deletes as
|
||||
it goes, and it costs nothing here — there is no ordering constraint between
|
||||
evictions, so there is no reason to start before every answer is in.
|
||||
|
||||
A candidate is an issue carrying a `gitea:` handle. `origin: local` work has
|
||||
none, is never asked about, and is never evicted — it is not in the tracker to
|
||||
be closed. A tracked issue whose handle is missing or unreadable cannot be
|
||||
verified, so it is reported and kept rather than guessed at.
|
||||
|
||||
Cost: one request per candidate. The store is a working set that push keeps
|
||||
small, and a wrong answer here deletes a file, so each issue is asked about by
|
||||
its own address rather than inferred from a list a limit could have truncated.
|
||||
|
||||
The refreshed state is written back even for the issues that stay: the answer is
|
||||
already paid for, and a store that keeps a state the tracker has disowned is the
|
||||
thing this command exists to fix.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--dry-run` | `false` | ask the tracker and report; write and delete nothing |
|
||||
| `--out` | — | store root (default: <project>/.kettle/issues) |
|
||||
|
||||
```bash
|
||||
kettle sync-evict # ask about every synced issue; evict the closed ones
|
||||
kettle sync-evict old-thing another-thing # only these
|
||||
kettle sync-evict --dry-run # ask, report, write and delete nothing
|
||||
```
|
||||
<!-- /kettle:gen -->
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
name: use
|
||||
description: Reference docs for the `tea` CLI — Gitea's own command-line client, and the way to reach every Gitea entity the `kettle` binary does not cover. Load when the user asks about pull requests, releases, milestones, labels, repos, branches, actions, webhooks, notifications or times, to look up the right `tea` command and flags. `tea` keeps its own configuration and its own logins, entirely separate from kettle's. Issues are NOT handled here: /kettle:issue works on them offline and /kettle:sync moves them to and from the tracker.
|
||||
---
|
||||
|
||||
# /kettle:use — tea CLI reference
|
||||
|
||||
Reference material for `tea`, Gitea's official command-line client. Use these
|
||||
docs to look up commands, flags, filters and output fields before running `tea`
|
||||
via Bash.
|
||||
|
||||
`kettle` covers issues and nothing else. Everything else Gitea has — pulls,
|
||||
releases, milestones, labels, repos, branches, actions, webhooks, notifications,
|
||||
times — is reached through `tea`, and this skill is how.
|
||||
|
||||
## Issues are somewhere else
|
||||
|
||||
Do **not** reach for `tea issues` or `tea api …/issues/…` to read or create an
|
||||
issue. Two skills own that, and they keep the payload out of your context:
|
||||
|
||||
| Skill | Scope |
|
||||
|---|---|
|
||||
| `/kettle:issue` | issues as units of work — create, read, grep, validate, tick, dependency graph. Offline. |
|
||||
| `/kettle:sync` | moving issues between the local store and the tracker — pull, push, comment, close, evict. |
|
||||
|
||||
## Login: `tea` has its own configuration, and it is not kettle's
|
||||
|
||||
Two tools, two credential stores, no connection between them:
|
||||
|
||||
| tool | where its logins live | how they are managed |
|
||||
|---|---|---|
|
||||
| `tea` | `$XDG_CONFIG_HOME/tea` | `tea logins list`, `tea logins add` (interactive), `tea logins default` |
|
||||
| `kettle` | `~/.config/kettle/logins.yaml` + `<project>/.kettle/config.yaml` | `kettle auth`, `kettle init --login` (`/kettle:auth`) |
|
||||
|
||||
**Configuring one configures nothing in the other.** `/kettle:auth` does not give
|
||||
`tea` a credential, and `tea logins add` does not give `kettle` one. A project
|
||||
whose `kettle` commands work fine can still have no `tea` login at all, and the
|
||||
error you get will be about the login `tea` chose for itself.
|
||||
|
||||
**There is no `$GITEA_LOGIN` placeholder and no hook that substitutes one.** The
|
||||
PreToolUse guard that used to rewrite it was deleted along with the Python
|
||||
scripts; writing `--login "$GITEA_LOGIN"` now passes an empty variable to `tea`
|
||||
and fails in a way that reads like a `tea` bug. If you find that spelling
|
||||
anywhere, it is stale.
|
||||
|
||||
How to name a login honestly:
|
||||
|
||||
- Inside a checkout, `tea` auto-detects owner, repo and login from the git
|
||||
remote. That is usually right and usually enough — run the command without
|
||||
`--login`.
|
||||
- When the machine holds more than one login, or you are outside a checkout,
|
||||
pass `--login <name>` with a name out of `tea logins list`. **Which one is the
|
||||
operator's call**: ask with `AskUserQuestion` rather than picking the one that
|
||||
looks likely. A wrong identity writes to a real tracker under somebody else's
|
||||
account.
|
||||
- `no gitea login detected, falling back to login '…'` is a **hard failure**, not
|
||||
a warning. Stop, do not act on the result, surface the line.
|
||||
- **Never mutate login state**: no `tea logins add/edit/delete/default`, no
|
||||
`tea logout`. `tea logins list` is the only login command that is yours to run,
|
||||
and adding a login is interactive — the operator does it in their own terminal.
|
||||
|
||||
## How to use
|
||||
|
||||
1. Identify the entity in the request: pulls, labels, milestones, releases,
|
||||
times, repos, branches, actions, webhooks, notifications, etc.
|
||||
2. Find the matching command in the index below.
|
||||
3. Run it via Bash, e.g. `tea pulls list --repo owner/repo --state open`.
|
||||
|
||||
`tea` auto-detects owner/repo from `$PWD` inside a git repo; otherwise pass
|
||||
`--repo owner/repo` (or `-r`).
|
||||
|
||||
### `--repo` takes a slug — except where a checkout is required
|
||||
|
||||
A few commands touch local git, not just the API, and for those `--repo` **must
|
||||
be a path to a checkout**; a slug is rejected:
|
||||
|
||||
```
|
||||
Error: local repository required: execute from a repo dir, or specify a path with --repo
|
||||
```
|
||||
|
||||
The message reads like the flag is missing even when it was passed. Confirmed
|
||||
for `pulls create`, `pulls checkout` and `pulls clean` (tea 0.14.x). Everything
|
||||
that is only an API call — `pulls list`, `milestones`, `releases`, `times`,
|
||||
`labels`, `issues` — takes the slug from any directory.
|
||||
|
||||
Three working forms for `pulls create`:
|
||||
|
||||
```bash
|
||||
# 1. cwd inside the checkout, no --repo at all
|
||||
tea pulls create --head feat/x --base main --title "…" --description "…"
|
||||
|
||||
# 2. from anywhere, --repo as a PATH (this is also the git-worktree answer:
|
||||
# point it at the main checkout)
|
||||
tea pulls create --repo /path/to/checkout \
|
||||
--head feat/x --base main --title "…" --description "…"
|
||||
|
||||
# 3. no checkout in reach — POST it, where owner/repo is a slug again
|
||||
tea api -X POST -d @tmp/pull/x.json repos/{owner}/{repo}/pulls
|
||||
```
|
||||
|
||||
## Index
|
||||
|
||||
- [tea CLI overview](references/tea/index.md) — global flags, common options, output formats
|
||||
- [ENTITIES](references/tea/entities.md) — issues, pulls, labels, milestones, releases, times, repos, branches, actions, webhooks, comment
|
||||
- [HELPERS](references/tea/helpers.md) — open, notifications, clone, api
|
||||
- [MISC](references/tea/misc.md) — whoami, admin
|
||||
- [SETUP](references/tea/setup.md) — logins, logout, ssh-keys
|
||||
|
||||
The canonical issue format lives in
|
||||
[`../issue/references/format.md`](../issue/references/format.md) — it describes
|
||||
local files, not `tea` commands.
|
||||
|
||||
## Rich payloads — write to `$PWD/tmp/` first, then `tea api`
|
||||
|
||||
Entity subcommands (`tea comment`, `tea pulls create`, `tea releases create`, …)
|
||||
are built for humans at a TTY. With a large or formatted body they can hang
|
||||
silently — an empty-looking positional arg triggers the `$EDITOR` fallback, or a
|
||||
scope/confirm prompt waits on a TTY that doesn't exist. The harness eventually
|
||||
kills the process (e.g. exit 144 = 128 + SIGURG on macOS).
|
||||
|
||||
**Rule:** for any non-trivial body (multi-line, or containing markdown / code
|
||||
fences / backticks / pipes / tables), bypass entity commands. Save the full
|
||||
request payload to `$PWD/tmp/` first, then POST via `tea api`.
|
||||
|
||||
Issues and issue comments are already wrapped — use `/kettle:sync` rather than
|
||||
hand-rolling their JSON. `.kettle/payload/` is kettle's own scratchpad and is
|
||||
written by kettle only; do not put hand-made bodies there. The procedure below
|
||||
covers everything else.
|
||||
|
||||
### Procedure
|
||||
|
||||
1. Ensure the target dir exists: `mkdir -p tmp/{kind}` where `{kind}` is
|
||||
`pull`, `release`, etc.
|
||||
2. Write the **complete request body as JSON** to `$PWD/tmp/{kind}/<slug>.json`.
|
||||
One file = one request. Use a quoted heredoc to avoid shell expansion:
|
||||
```bash
|
||||
mkdir -p tmp/release
|
||||
cat > tmp/release/v0-2-0.json <<'EOF'
|
||||
{"tag_name": "v0.2.0", "name": "v0.2.0", "body": "## Changes\n\nMulti-line markdown with `code`."}
|
||||
EOF
|
||||
```
|
||||
Newlines inside the body must be encoded as `\n` in the JSON string. If
|
||||
composing programmatically, pipe through
|
||||
`jq -Rs '{body: .}' < body.md > tmp/release/v0-2-0.json`.
|
||||
3. POST with `tea api`, passing the file with `-d @<path>`:
|
||||
```bash
|
||||
tea api -X POST -d @tmp/release/v0-2-0.json repos/{owner}/{repo}/releases
|
||||
```
|
||||
4. Keep the file. `tmp/` should be gitignored; the saved payload is useful for
|
||||
retries, edits (`PATCH`), and debugging failed posts.
|
||||
|
||||
### Common endpoints
|
||||
|
||||
| Action | Method + endpoint |
|
||||
|---|---|
|
||||
| Create PR | `POST repos/{owner}/{repo}/pulls` |
|
||||
| Edit PR body or title | `PATCH repos/{owner}/{repo}/issues/{n}` |
|
||||
| Comment on a PR | `POST repos/{owner}/{repo}/issues/{n}/comments` |
|
||||
| Edit comment | `PATCH repos/{owner}/{repo}/issues/comments/{id}` |
|
||||
| Create release | `POST repos/{owner}/{repo}/releases` |
|
||||
| Create milestone | `POST repos/{owner}/{repo}/milestones` |
|
||||
|
||||
Short single-line bodies (e.g. `tea comment 42 "lgtm"`) are still fine via
|
||||
entity commands.
|
||||
|
||||
## Tips
|
||||
|
||||
- Pass `-o json` for structured output when parsing programmatically — on
|
||||
**entity commands only**. On `tea api`, `-o` is a *file name*: `-o json`
|
||||
writes the response body to a file called `json` and leaves stdout empty.
|
||||
The response is already JSON, so there is nothing to format; use `-` for
|
||||
stdout, or leave the flag off.
|
||||
- Use `--fields, -f` to narrow columns.
|
||||
- Pagination: `--page, -p <n>` and `--limit, --lm <n>` (defaults 1 / 30).
|
||||
- A `tea` command that fails on identity is a login problem in **tea's** own
|
||||
config, never in kettle's — `tea logins list`, and the operator decides.
|
||||
@@ -0,0 +1,137 @@
|
||||
# tea CLI — ENTITIES
|
||||
|
||||
See [`./index.md`](./index.md) for global options and common flags shared by all commands.
|
||||
|
||||
## `tea issues` (aliases: `issue`, `i`)
|
||||
Without args lists issues; with `<index>` shows issue detail.
|
||||
|
||||
Shared filters: `--state {all|open|closed}` (default: open), `--kind {issues|pulls|all}`, `--keyword/-k`, `--labels/-L`, `--milestones/-m`, `--author/-A`, `--assignee/-a`, `--mentions/-M`, `--owner/--org`, `--from/-F`, `--until/-u`, `--comments`. Available fields: `index,state,kind,author,author-id,url,title,body,created,updated,deadline,assignees,milestone,labels,comments,owner,repo`.
|
||||
|
||||
Subcommands:
|
||||
- `list, ls` — list (same filters as above).
|
||||
- `create, c` — create an issue. Options: `--title/-t`, `--description/-d`, `--assignees/-a`, `--labels/-L`, `--milestone/-m`, `--deadline/-D`, `--referenced-version/-v` (commit hash or tag).
|
||||
- `edit, e <idx>...` — edit. `--title`, `--description`, `--add-assignees/-a`, `--add-labels/-L`, `--remove-labels`, `--milestone`, `--deadline`, `--referenced-version`. To unset a value pass an empty string (`--milestone ""`).
|
||||
- `reopen, open <idx>...`
|
||||
- `close <idx>...`
|
||||
|
||||
## `tea pulls` (aliases: `pull`, `pr`)
|
||||
Without args lists PRs; with `<index>` shows PR detail. Fields: `index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments,ci`.
|
||||
|
||||
Subcommands:
|
||||
- `list, ls` (`--state`)
|
||||
- `checkout, co <idx>` — check out PR locally. `--branch/-b` creates a local branch if missing. Needs a checkout, same as `create`: `--repo` is a path here, not a slug.
|
||||
- `clean <idx>` — delete local and remote feature branches for a closed PR. `--ignore-sha` matches branch by name instead of commit hash. Needs a checkout, same as `create`.
|
||||
- `create, c` — create a PR. `--head <user:branch>`, `--base/-b`, `--allow-maintainer-edits/--edits`, `--agit`, `--topic`, plus all issue-style fields (`--title`, `--description`, `--assignees`, `--labels`, `--milestone`, `--deadline`, `--referenced-version`).
|
||||
**Needs a local checkout.** `--repo owner/repo` is *not* accepted here — the
|
||||
slug fails with `local repository required: execute from a repo dir, or
|
||||
specify a path with --repo`, whose advice reads like the flag was missing.
|
||||
Run it with cwd inside the checkout and no `--repo`, or pass `--repo
|
||||
/path/to/checkout`. From a git worktree, point `--repo` at the main
|
||||
checkout. With no checkout in reach, `POST repos/{owner}/{repo}/pulls`
|
||||
through `tea api`, which takes the slug.
|
||||
- `close <idx>...`, `reopen, open <idx>...`
|
||||
- `edit, e <idx>...` — like `issues edit` plus `--add-reviewers/-r`, `--remove-reviewers`.
|
||||
- `review <idx>` — interactive review.
|
||||
- `approve, lgtm, a <idx> [comment]`
|
||||
- `reject <idx> <reason>`
|
||||
- `merge, m <idx>` — `--style/-s {merge|rebase|squash|rebase-merge}` (default merge), `--title/-t`, `--message/-m`.
|
||||
- `review-comments, rc <idx>` — list review comments. Fields: `id,body,reviewer,path,line,resolver,created,updated,url`.
|
||||
- `resolve <comment-id>` / `unresolve <comment-id>`
|
||||
|
||||
## `tea labels` (alias: `label`)
|
||||
- `list, ls` — `--save/-s` dumps labels to a file.
|
||||
- `create, c` — `--name`, `--color`, `--description`, `--file` (bulk import from file).
|
||||
- `update` — `--id`, `--name`, `--color`, `--description`.
|
||||
- `delete, rm` — `--id`.
|
||||
|
||||
## `tea milestones` (aliases: `milestone`, `ms`)
|
||||
Fields: `title,state,items_open,items_closed,items,duedate,description,created,updated,closed,id`.
|
||||
|
||||
- `list, ls` (`--state`)
|
||||
- `create, c` — `--title/-t`, `--description/-d`, `--deadline/--expires/-x`, `--state`.
|
||||
- `close <name>...` — `--force/-f` deletes instead of closing.
|
||||
- `reopen, open <name>...`
|
||||
- `delete, rm <name>`
|
||||
- `issues, i <name>` — manage milestone contents:
|
||||
- `add, a <name> <issue-idx>`
|
||||
- `remove, r <name> <issue-idx>`
|
||||
|
||||
## `tea releases` (aliases: `release`, `r`)
|
||||
- `list, ls`
|
||||
- `create, c [<tag>]` — `--tag`, `--target` (branch/commit), `--title/-t`, `--note/-n`, `--note-file/-f`, `--draft/-d`, `--prerelease/-p`, `--asset/-a <path>` (repeatable).
|
||||
- `edit, e <tag>...` — `--tag`, `--target`, `--title/-t`, `--note/-n`, `--draft/-d <bool>`, `--prerelease/-p <bool>`.
|
||||
- `delete, rm <tag>...` — `--confirm/-y` required; `--delete-tag` also removes the git tag.
|
||||
- `assets, asset, a` — manage release attachments:
|
||||
- `list, ls <tag>`
|
||||
- `create, c <tag> <asset>...`
|
||||
- `delete, rm <tag> <attachment-name>...` — `--confirm/-y`.
|
||||
|
||||
## `tea times` (aliases: `time`, `t`)
|
||||
Time tracking on issues/PRs. Fields: `id,created,repo,issue,user,duration`. Command-level: `--from/-f`, `--until/-u`, `--total/-t`, `--mine/-m`.
|
||||
|
||||
- `add, a <issue> <duration>` — e.g. `tea times add 1 1h25m`.
|
||||
- `delete, rm <issue> <time-id>`
|
||||
- `reset <issue>`
|
||||
- `list, ls [username | #issue]` — username filters by user on the repo; `#N` filters by issue; `--mine` aggregates across all repos.
|
||||
|
||||
## `tea organizations` (aliases: `organization`, `org`)
|
||||
- `list, ls`
|
||||
- `create, c <name>` — `--full-name/-n`, `--description/-d`, `--website/-w`, `--location/-L`, `--visibility/-v`, `--repo-admins-can-change-team-access`.
|
||||
- `delete, rm <name>`
|
||||
|
||||
## `tea repos` (alias: `repo`)
|
||||
Fields: `description,forks,id,name,owner,stars,ssh,updated,url,permission,type`.
|
||||
|
||||
- `list, ls` — `--watched/-w`, `--starred/-s`, `--owner/-O`, `--type/-T {fork|mirror|source}`.
|
||||
- `search, s [term]` — `--topic/-t`, `--type/-T`, `--owner/-O`, `--private {true|false}`, `--archived {true|false}`.
|
||||
- `create, c` — `--name`, `--owner/-O`, `--private`, `--description/--desc`, `--init`, `--labels`, `--gitignores/--git`, `--license`, `--readme`, `--branch`, `--template`, `--trustmodel {committer|collaborator|collaborator+committer}`, `--object-format {sha1|sha256}`.
|
||||
- `create-from-template, ct` — `--template/-t`, `--name/-n`, `--owner/-O`, `--private`, `--description/--desc`, copy toggles: `--content`, `--githooks`, `--avatar`, `--labels`, `--topics`, `--webhooks`.
|
||||
- `fork, f` — `--owner/-O` (default: current user).
|
||||
- `migrate, m` — `--name`, `--owner`, `--clone-url`, `--service {git|gitea|gitlab|gogs}`, `--mirror`, `--mirror-interval`, `--private`, `--template`, copy toggles: `--wiki`, `--issues`, `--labels`, `--pull-requests`, `--releases`, `--milestones`, `--lfs`, `--lfs-endpoint`, auth: `--auth-user`, `--auth-password`, `--auth-token`.
|
||||
- `delete, rm` — `--name`, `--owner/-O`, `--force/-f`.
|
||||
- `edit, e` — `--name`, `--description/--desc`, `--website`, `--private <bool>`, `--template <bool>`, `--archived <bool>`, `--default-branch`.
|
||||
|
||||
## `tea branches` (aliases: `branch`, `b`)
|
||||
Fields: `name,protected,user-can-merge,user-can-push,protection`.
|
||||
|
||||
- `list, ls`
|
||||
- `protect, P <branch>` — enable branch protection.
|
||||
- `unprotect, U <branch>` — remove protection.
|
||||
- `rename, rn <old> <new>`
|
||||
|
||||
## `tea actions` (alias: `action`)
|
||||
CI management: secrets, variables, workflow definitions, workflow runs.
|
||||
|
||||
### `tea actions secrets` (alias: `secret`)
|
||||
- `list, ls`
|
||||
- `create, add, set <name> [value]` — `--file` or `--stdin` to read the value.
|
||||
- `delete, remove, rm <name>` — `--confirm/-y`.
|
||||
|
||||
### `tea actions variables` (aliases: `variable`, `vars`, `var`)
|
||||
- `list, ls` — `--name` to fetch a single variable.
|
||||
- `set, create, update <name> [value]` — `--file`, `--stdin`.
|
||||
- `delete, remove, rm <name>` — `--confirm/-y`.
|
||||
|
||||
### `tea actions runs` (alias: `run`)
|
||||
- `list, ls` — `--status {success|failure|pending|queued|in_progress|skipped|canceled}`, `--branch`, `--event`, `--actor`, `--since`, `--until`.
|
||||
- `view, show, get <run-id>` — `--jobs` prints the jobs table.
|
||||
- `delete, remove, rm, cancel <run-id>` — `--confirm/-y`.
|
||||
- `logs, log <run-id>` — `--job <id>`, `--follow/-f` (requires the job to be in progress).
|
||||
|
||||
### `tea actions workflows` (alias: `workflow`)
|
||||
- `list, ls`
|
||||
- `view, show, get <workflow-id>`
|
||||
- `dispatch, trigger, run <workflow-id>` — `--ref/-r`, `--input/-i key=value` (repeatable), `--follow/-f`.
|
||||
- `enable <workflow-id>`
|
||||
- `disable <workflow-id>` — `--confirm/-y`.
|
||||
|
||||
## `tea webhooks` (aliases: `webhook`, `hooks`, `hook`)
|
||||
Scope is selected by flag: `--repo`, `--org`, `--global`.
|
||||
|
||||
- `list, ls`
|
||||
- `create, c <webhook-url>` — `--type {gitea|gogs|slack|discord|dingtalk|telegram|msteams|feishu|wechatwork|packagist}` (default: gitea), `--secret`, `--events` (default: push), `--active`, `--branch-filter`, `--authorization-header`.
|
||||
- `update, edit, u <id>` — `--url`, `--secret`, `--events`, `--active` / `--inactive`, `--branch-filter`, `--authorization-header`.
|
||||
- `delete, rm <id>` — `--confirm/-y`.
|
||||
|
||||
## `tea comment, c <issue/pr index> [body]`
|
||||
Add a comment to an issue or PR. Body may be passed as an argument or supplied interactively.
|
||||
@@ -0,0 +1,30 @@
|
||||
# tea CLI — HELPERS
|
||||
|
||||
See [`./index.md`](./index.md) for global options and common flags shared by all commands.
|
||||
|
||||
## `tea open, o`
|
||||
Open the current repository/context in a web browser.
|
||||
|
||||
## `tea notifications` (aliases: `notification`, `n`)
|
||||
Defaults to the current repo; `--mine/-m` aggregates across all your repos. Fields: `id,status,updated,index,type,state,title,repository`. Filters: `--types/-t {issue|pull|repository|commit}`, `--states/-s {pinned|unread|read}` (default: `unread,pinned`).
|
||||
|
||||
- `ls, list`
|
||||
- `read, r [all | <id>]`
|
||||
- `unread, u [all | <id>]`
|
||||
- `pin, p [all | <id>]`
|
||||
- `unpin [all | <id>]`
|
||||
|
||||
## `tea clone, C <repo-slug> [target-dir]`
|
||||
Clone without requiring a local git install. Accepts slug forms: `gitea/tea`, `tea`, `gitea.com/gitea/tea`, `git@gitea.com:gitea/tea`, `https://gitea.com/gitea/tea`, `ssh://gitea.com:22/gitea/tea`. A host in the slug overrides `--login`. Options: `--depth/-d`, `--login/-l`.
|
||||
|
||||
## `tea api <endpoint>`
|
||||
Authenticated HTTP request to the Gitea API. Endpoints are auto-prefixed with `/api/v1/` unless they start with `/api/` or `http(s)://`. Placeholders `{owner}` and `{repo}` are filled from the repo context.
|
||||
|
||||
- `--method/-X {GET|POST|PUT|PATCH|DELETE}` (default GET; switches to POST automatically when a body is provided)
|
||||
- `--field/-f key=value` — string field on body (repeatable).
|
||||
- `--Field/-F key=value` — typed field (numbers, booleans, null, JSON arrays/objects); `@file` or `@-` (stdin); `"null"` forces literal string.
|
||||
- `--data/-d` — raw JSON body (`@file` / `@-`). Incompatible with `-f`/`-F`.
|
||||
- `--header/-H key:value` (repeatable)
|
||||
- `--include/-i` — write status + response headers to stderr.
|
||||
- `--output/-o <file>` — write response body to file (`-` = stdout). **Not the entity commands' format flag**: `-o json` here creates a file named `json` and prints nothing. The body is already JSON.
|
||||
- Quote the endpoint if it contains `?` or `&` to prevent shell expansion.
|
||||
@@ -0,0 +1,36 @@
|
||||
# tea CLI — Index
|
||||
|
||||
Version: `tea 0.14.1` (go-sdk v0.25.1). Source: recursive `--help` traversal. Upstream: https://gitea.com/gitea/tea
|
||||
|
||||
`tea` is a productivity helper for Gitea. It uses the current git repository context (`$PWD`) — owner/repo/login are auto-detected when inside a repo. Config is persisted in `$XDG_CONFIG_HOME/tea`.
|
||||
|
||||
## Global options
|
||||
|
||||
- `--debug, --vvv` — enable debug mode
|
||||
- `--help, -h`, `--version, -v`
|
||||
|
||||
## Common flags (present on nearly every command)
|
||||
|
||||
| Flag | Purpose |
|
||||
|---|---|
|
||||
| `--login, -l <name>` | use a specific login from the config |
|
||||
| `--repo, -r <owner/repo>` | override repository context (local path or slug). **A slug only works where the command is pure API.** `pulls create`, `pulls checkout` and `pulls clean` need a real checkout and read this flag as a path — see [SKILL.md](../../SKILL.md) |
|
||||
| `--remote, -R <name>` | discover login from this git remote |
|
||||
| `--output, -o <fmt>` | output format: `simple, table, csv, tsv, yaml, json`. **Entity commands only** — on `tea api` the same flag is a FILE NAME, see [HELPERS](./helpers.md) |
|
||||
| `--page, -p <n>` / `--limit, --lm <n>` | pagination (defaults 1 / 30) |
|
||||
| `--fields, -f <list>` | which columns to print |
|
||||
|
||||
## Command categories
|
||||
|
||||
```
|
||||
ENTITIES: issues, pulls, labels, milestones, releases, times,
|
||||
organizations, repos, branches, actions, webhooks, comment
|
||||
HELPERS: open, notifications, clone, api
|
||||
MISC: whoami, admin
|
||||
SETUP: logins, logout, ssh-keys
|
||||
```
|
||||
|
||||
- [ENTITIES](./entities.md)
|
||||
- [HELPERS](./helpers.md)
|
||||
- [MISC](./misc.md)
|
||||
- [SETUP](./setup.md)
|
||||
@@ -0,0 +1,17 @@
|
||||
# tea CLI — MISCELLANEOUS
|
||||
|
||||
See [`./index.md`](./index.md) for global options and common flags shared by all commands.
|
||||
|
||||
## `tea whoami`
|
||||
Show the currently logged in user.
|
||||
|
||||
## `tea admin, a`
|
||||
Operations requiring admin access on the Gitea instance.
|
||||
|
||||
### `tea admin users` (alias: `u`)
|
||||
Fields: `id,login,full_name,email,avatar_url,language,is_admin,restricted,prohibit_login,location,website,description,visibility,activated,lastlogin_at,created_at`.
|
||||
|
||||
- `list, ls`
|
||||
- `create, add, new` — `--username/-u`, `--password/-p` / `--password-file` / `--password-stdin`, `--email/-e`, `--full-name`, `--admin`, `--restricted`, `--prohibit-login`, `--no-must-change-password`, `--visibility {public|limited|private}`.
|
||||
- `edit, update, e, u <username>` — paired flags: `--password` (or `--password-file`/`--password-stdin`), `--email/-e`, `--full-name`, `--description`, `--website`, `--location`, `--admin`/`--no-admin`, `--restricted`/`--no-restricted`, `--prohibit-login`/`--allow-login`, `--active`/`--inactive`, `--no-must-change-password`, `--visibility`, `--max-repo-creation` (-1 = unlimited), `--allow-git-hook`/`--no-allow-git-hook`, `--allow-import-local`/`--no-allow-import-local`, `--allow-create-organization`/`--no-allow-create-organization`.
|
||||
- `delete, rm, remove <username>` — `--confirm/-y`.
|
||||
@@ -0,0 +1,19 @@
|
||||
# tea CLI — SETUP
|
||||
|
||||
See [`./index.md`](./index.md) for global options and common flags shared by all commands.
|
||||
|
||||
## `tea logins` (alias: `login`)
|
||||
- `list, ls`
|
||||
- `add` — interactive when called without args. `--name/-n`, `--url/-u` (`$GITEA_SERVER_URL`), `--token/-t` (`$GITEA_SERVER_TOKEN`), `--user` (`$GITEA_SERVER_USER`), `--password/--pwd` (`$GITEA_SERVER_PASSWORD`), `--otp` (`$GITEA_SERVER_OTP`), `--scopes` (`$GITEA_SCOPES`), `--ssh-key/-s`, `--ssh-agent-principal/-c`, `--ssh-agent-key/-a`, `--insecure/-i`, `--no-version-check/--nv`, `--helper/-j`, `--oauth/-o` (plus `--client-id`, `--redirect-url`).
|
||||
- `edit, e` — interactive.
|
||||
- `delete, rm <name>`
|
||||
- `default [<login>]` — get or set the default login.
|
||||
- `oauth-refresh [<login>]` — refresh an OAuth token (opens browser if the refresh token is also expired).
|
||||
|
||||
## `tea logout <name>`
|
||||
Remove a stored login.
|
||||
|
||||
## `tea ssh-keys` (alias: `ssh-key`)
|
||||
- `list, ls`
|
||||
- `add <key-file>` — `--title/-t` (defaults to filename without extension).
|
||||
- `delete, rm <key-id>` — `--confirm/-y`.
|
||||
Reference in New Issue
Block a user