refactor: split issue domain from Gitea transport
An issue was a Gitea row that happened to be cached locally: its identity was the tracker's number (42.md), its dependencies were tracker numbers (depends: [#12]), and a local issue existed only as a draft that push deleted on success. Nothing could be planned or tracked without a tracker. Split into layers, with knowledge flowing one way: skills/issue DOMAIN what an issue is: format, validation, dep graph ^ offline; stdlib imports only, no subprocess | imports skills/sync BRIDGE map.py md <-> Gitea JSON, pure, no I/O _gitea.py login pin, api, pagination, filters skills/use REFERENCE tea CLI docs for non-issue entities skills/issue never imports skills/sync. Delete the sync layer and the domain keeps working. Identity is now a slug derived from the title (wire-sqlc-appclick.md) and is stable across retitles and pushes. Tracker numbers live in a `gitea:` field, never in a file name and never in `depends:`; the pair is indexed in .remote.json, which is a cache over the files, not a second source of truth. Behavior changes: - Pushing is additive. The file is never deleted; it gains gitea:/url:/ synced: and origin: flips from local to gitea. `origin: local` is a durable state, not a pending one. - Pushes go in topological order so dependencies get numbers first. - The dependency graph is computed offline from `depends:` metadata; body prose is passed through unchanged in both directions rather than being rewritten between slugs and #N. - `origin` is domain-owned (whether work exists elsewhere is a fact about the work); the handle and how to reach it stay with sync. Script moves: issue_get.py -> sync/pull.py issue_push.py -> sync/push.py issue_list.py -> sync/remote.py issue_index.py -> issue/issue_index.py _tea.py -> split into issue/issue.py, sync/map.py, sync/_gitea.py New: issue/issue_new.py, issue/issue_check.py, issue/issue_tree.py, and sync/comment.py — comment posting was the last issue operation still hand-rolled through raw `tea api`. references/issue-format.md moves to skills/issue/references/format.md; label hex colors move out of it into map.py, since a color is how a tracker paints a chip, not what an issue is. Verified: offline path end to end (new, check, tree, index, push --dry-run) and read-only against Gitea (remote listing, pull with mapping, comment guard). Write paths of push.py and comment.py are not exercised here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+106
-54
@@ -1,73 +1,125 @@
|
||||
---
|
||||
name: issue
|
||||
description: Create a Gitea issue in the project's canonical format. Run when the user asks to file/create an issue, or types /tea:issue. Writes a local draft during planning, then pushes it with issue_push.py, which validates the format, ensures exclusive type/* labels exist, and posts via tea api.
|
||||
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 — draft locally, push when agreed
|
||||
# /tea:issue — issues as units of work
|
||||
|
||||
Thin procedure on top of the canonical format defined in
|
||||
[`../use/references/issue-format.md`](../use/references/issue-format.md).
|
||||
Read that file first — it is the single source of truth for types, labels,
|
||||
templates, and language rules.
|
||||
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.
|
||||
|
||||
Two phases, deliberately separated: planning writes **local files only** (no
|
||||
network, no `tea`), and one push turns them into real issues. Scripts live in
|
||||
`../use/scripts/` (see `/tea:use` for the full set).
|
||||
**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`.
|
||||
|
||||
## Phase 1 — draft (no network)
|
||||
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.
|
||||
|
||||
1. **Read the format**: load `../use/references/issue-format.md`.
|
||||
2. **Pick the type** — `bug`, `task`, `refactor`, `test`, `feature` (a
|
||||
container for several issues with one business value), or `draft` (for
|
||||
ideas not ready for work). If it is not obvious from the request, ask the
|
||||
user (one question).
|
||||
3. **Write `tmp/issues/drafts/<slug>.md`**: a metadata block carrying
|
||||
`labels:` only, then `# Title`, then the type's template.
|
||||
## Identity: the slug
|
||||
|
||||
```markdown
|
||||
---
|
||||
labels: [type/task, tech/sql, comp/appclick]
|
||||
---
|
||||
# Wire sqlc into the appclick repo layer
|
||||
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:`.
|
||||
|
||||
## Summary
|
||||
...
|
||||
```
|
||||
Consequence worth internalizing: **`#42` means nothing in this layer.** Refer to
|
||||
issues by id.
|
||||
|
||||
English imperative title with no type prefix; every section of the
|
||||
template present and in order; headers English, prose Russian; `## Spec`
|
||||
filled with a repo path, a URL, or the literal `none` — ask the user if you
|
||||
cannot determine which. Add `## Depends on` right after `## Spec` when the
|
||||
issue depends on others (one `#N` per line); omit it otherwise.
|
||||
One draft file = one issue. Several related issues = several drafts.
|
||||
4. **Check the format without posting** (optional, free):
|
||||
```bash
|
||||
python3 ../use/scripts/issue_push.py --all --dry-run
|
||||
```
|
||||
## Scripts
|
||||
|
||||
## Phase 2 — push (once the plan is agreed)
|
||||
All offline, all in `<skill-base-dir>/scripts/`.
|
||||
|
||||
```bash
|
||||
python3 ../use/scripts/issue_push.py --all
|
||||
| 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_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)
|
||||
```
|
||||
|
||||
The script validates the format (exactly one `type/*`, at most one
|
||||
`severity/*`, English title, `## Summary` / `## Spec` / `## Acceptance
|
||||
criteria` present), creates any missing labels — `exclusive: true` for
|
||||
`type/*` and `severity/*`, canonical colors from the format doc — POSTs each
|
||||
issue, prints `#N <url>`, and **deletes the draft**. The issue lives in Gitea
|
||||
now; the local copy is not a mirror and must not linger.
|
||||
## Reading: grep, don't parse
|
||||
|
||||
Flags: `--keep` writes `tmp/issues/<n>.md` instead of deleting, `--dry-run`
|
||||
validates only, `--force` posts despite format violations (say why).
|
||||
Metadata is one field per line with inline lists precisely so plain `grep`
|
||||
works. `INDEX.md` first, then the files:
|
||||
|
||||
Report the issue URLs and the applied labels to the user.
|
||||
```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
|
||||
```
|
||||
|
||||
## Editing an existing issue
|
||||
Read whole files only for the issues the task actually needs.
|
||||
|
||||
Drafts only create. To bring an existing issue to the format: fetch it with
|
||||
`python3 ../use/scripts/issue_get.py <n>` (writes `tmp/issues/<n>.md`,
|
||||
prints a compact line), restructure the body into the type's template without
|
||||
losing information, then `PATCH repos/{owner}/{repo}/issues/{n}` via `tea api`
|
||||
with the new title/body and ensure exactly one `type/*` label is set. Login is
|
||||
always the placeholder `--login "$GITEA_LOGIN"` (see `/tea:use`).
|
||||
## 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:`, tick checkboxes in
|
||||
`## Acceptance criteria`, add ids to `depends:`. Re-run `issue_check.py`
|
||||
afterwards, and `issue_index.py` to refresh the table.
|
||||
|
||||
If the issue is synced (`origin: gitea`), your edit is local until you run
|
||||
`push.py --update` from `/tea:sync`. Nothing tracks that drift automatically.
|
||||
|
||||
## 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`.
|
||||
|
||||
Reference in New Issue
Block a user