Files
marketplace/skills/issue/SKILL.md
T
naudachu e629d14585 feat: drop the local copy after a successful push
Gitea becomes the source of truth. Once a push is confirmed, push.py
deletes tmp/issues/<id>.md and <id>.comments.md and prints the number and
URL the issue now lives at; the current state is obtained by pulling
again rather than by reconciling. --update follows the same rule, with no
exception: what is local is what has not left.

This reverses three statements AGENTS.md used to make, and rewriting them
is part of the change:

  - "tmp/issues/ is the store, not a cache of Gitea" — it is both, split
    by origin:. An origin: local file is the only copy of the work; an
    origin: gitea file is a deletable working copy.
  - "Pushing is additive: the file is never deleted" — it is deleted.
  - "origin: local is a durable state" — complete, but not durable:
    pushing ends it.

Slug stability, which the format promises for the life of an issue, can
no longer rest on a file push is about to delete. The slug goes up in the
body as a hidden marker, <!-- tea:id <slug> -->, on the first line:
map.to_payload strips every marker and prepends exactly one, map.from_api
strips every marker on the way down, so the local file never holds one
and a body cannot accumulate them however many round trips it makes. The
marker survives a rename in the web UI, a lost .remote.json, a fresh
clone and another machine — none of which a local index does.

Deletion is the last thing that happens to an issue and only after the
transport returned, the answer carried a positive integer number (and, on
--update, the number that was PATCHed — push.confirmed_number), and
.remote.json was written. A raised transport, a non-2xx, an empty or
mismatched body each leave the file on disk and stop the run.

.remote.json is no longer "only an index over the files": its entries now
deliberately outlive them, so it is the local number -> slug ledger and
rebuild_map merges into it instead of reconstructing it from files that
may be gone. It stays recoverable, from the markers in Gitea rather than
from the files. push.dep_state reads it too, so a blocker whose file an
earlier push dropped still gets its native dependency link.

Also fixes a pre-existing bug the new tests hit: issue.all_ids treated
<id>.comments.md as an issue called "<id>.comments", so a bare push.py in
a store holding pulled threads tried to file a comment thread as a unit
of work. A slug has no dot in it.

tests/test_drop_after_push.py covers the round trip (push -> gone -> pull
-> identical in slug, depends: and body), the marker's algebra, and every
failure path separately. test_push_dependencies.py is updated where it
encoded the old "never deleted" contract. 183 tests, no network.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 16:38:16 +05:00

232 lines
11 KiB
Markdown

---
name: issue
description: Work with this project's issues as units of work — create, read, grep, validate, and walk their dependency graph. Entirely offline; issues are local markdown files and need no tracker. Load when the user asks to file/create an issue, read or find issues, check an issue against the format, or see what depends on what. For pushing to or pulling from Gitea, load /tea:sync instead.
---
# /tea:issue — issues as units of work
An issue is a markdown file in `tmp/issues/`. This skill covers everything you
do **with** an issue: writing one, reading one, checking it against the
canonical format, and walking the dependency graph.
**Nothing here touches the network.** No `tea`, no Gitea, no login. 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 —
`/tea: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.
## Identity: the slug
The file name is the id and the id is a slug — `tmp/issues/wire-sqlc-appclick.md`.
It never changes, not when the title changes and 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.
## Scripts
All offline, all in `<skill-base-dir>/scripts/`.
| Script | What it does |
|---|---|
| `issue_new.py --type T --title "…"` | create `tmp/issues/<slug>.md` from the type's template |
| `issue_check.py [id…]` | validate against the canonical format; exit 1 on errors |
| `issue_ac.py <id> [--check N\|TEXT]` | list the body's checkboxes; tick or untick one |
| `issue_tree.py [id…]` | draw the dependency graph from `depends:` |
| `issue_index.py` | rebuild `tmp/issues/INDEX.md` |
| `issue.py` | the domain module the others import — not a command |
```
tmp/issues/INDEX.md table of every issue — read this first
tmp/issues/wire-sqlc-appclick.md metadata block + `# Title` + body
tmp/issues/wire-sqlc.comments.md comment thread (written by /tea:sync only)
tmp/issues/tree-<id>.md saved graph (issue_tree.py --write)
```
## Where the store is
`<repo root>/tmp/issues`**not** `tmp/issues` relative to wherever you are
standing. The scripts resolve it by walking up from their own file to the
nearest `.git` or `AGENTS.md`, so they all see one store no matter which
directory you run them from, and a `cd` earlier in the session changes nothing.
`--out` overrides that and is taken **literally**: an absolute path is used as
given, a relative one stays relative to the current directory. Nothing rewrites
what you typed.
Two things follow, and both are deliberate:
- A store that is not there reports `does not exist`; a store with no issues in
it reports `is empty`. They are different problems.
- No script conjures a store as a side effect of writing. Only `issue_new.py`
creates one — the first issue in a fresh checkout — and it says so on stderr.
## 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' tmp/issues/*.md # all bugs
grep -l 'origin: local' tmp/issues/*.md # never pushed anywhere
grep -ln 'depends:.*migrate-schema' tmp/issues/*.md # who depends on it
grep -A3 '## Acceptance criteria' tmp/issues/wire-*.md
grep -c '^- \[ \]' tmp/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:**
```bash
python3 <skill-base-dir>/scripts/issue_new.py \
--type task --title "Wire sqlc into the appclick repo layer" \
--label tech/sql --label comp/appclick --depends migrate-schema
```
English imperative title with 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:**
```bash
python3 <skill-base-dir>/scripts/issue_check.py wire-sqlc-appclick
```
One file = one issue. Several related issues = several files, linked through
`depends:`.
The issue is now real and complete. Publishing it to Gitea is a separate
decision — `/tea:sync` — and does not change the file's status here.
## Editing an issue
Edit the file. Change `state:` to close it, edit `labels:`, add ids to
`depends:`. Re-run `issue_check.py` afterwards, and `issue_index.py` to refresh
the table. Checkboxes are the exception — use `issue_ac.py`, below.
If the issue is synced (`origin: gitea`), the file is a working copy: your edit
is local until you run `push.py --update` from `/tea:sync`, and that push
**deletes the file** once Gitea has it. Nothing tracks drift, and with one copy
at a time there is little to track — a file that is still here has not been
pushed. Get it back with `pull.py <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 just 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.
```bash
python3 <skill-base-dir>/scripts/issue_ac.py wire-sqlc-appclick
python3 <skill-base-dir>/scripts/issue_ac.py wire-sqlc-appclick --check 3
python3 <skill-base-dir>/scripts/issue_ac.py wire-sqlc-appclick --check "регресс"
python3 <skill-base-dir>/scripts/issue_ac.py wire-sqlc-appclick --uncheck 3
```
With no flag it prints the numbered list with each item's state, grouped by the
heading the item sits under. `--check` / `--uncheck` take that number or a
substring of the item's text (case-insensitive).
- **Every checkbox in the body counts, not just `## Acceptance criteria`.** A
`type/feature` keeps its children as checkboxes under `## Issues`, and they
are numbered in the same list. The script is named after the section most
boxes live in, nothing more.
- **A substring must match exactly one item.** Two matches is an error that
lists them; pick by number instead. It never guesses.
- **Exactly one character of the file changes.** Metadata, wording, wrapping
and trailing whitespace all come back byte for byte, so `git diff` and the
tracker's diff show the tick and nothing else.
- Examples inside a ``` fence are markup, not state — they are skipped.
- `INDEX.md` gains a `progress` column (`3/7`, blank when the issue has no
boxes), recomputed from the body on every build and stored in no field.
`issue_ac.py` rebuilds the index after a successful tick.
Getting the tick to the tracker is a separate step — `push.py --update` in
`/tea:sync`.
## 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:
```bash
grep -rn 'GITEA_LOGIN' hooks/ skills/
```
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:**
```bash
python3 <skill-base-dir>/scripts/issue_check.py wire-sqlc-appclick
```
Errors mean malformed, warnings mean the type's template is not fully
filled in. Re-run `issue_index.py` if the labels changed.
The procedure is identical for `origin: local` and `origin: gitea` — it works
on `tmp/issues/<id>.md`, and this layer does not know the difference. Getting
the rewritten body into the tracker is a separate decision — `push.py --update`
in `/tea:sync` — and is no part of this.
## Dependency graph
`depends:` is the authoritative edge list; the body's `## Depends on` section
is prose for humans. `issue_check.py` warns when they disagree.
```bash
python3 <skill-base-dir>/scripts/issue_tree.py # all roots
python3 <skill-base-dir>/scripts/issue_tree.py wire-sqlc-appclick --write
```
A `type/feature` plus its children read as one document: draw the tree once for
the shape, then grep the files.
## Layering rule
This skill must keep working with `skills/sync/` deleted. Every import under
`scripts/` is stdlib, and `subprocess` is not among them:
```bash
grep -rhn '^import\|^from' skills/issue/scripts/ | sort -u
```
If you find yourself wanting a tracker concept here — an issue number, a login,
an HTTP call — it belongs in `/tea:sync`.