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,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 -->
|
||||
Reference in New Issue
Block a user