091dceec1d
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>
126 lines
5.1 KiB
Markdown
126 lines
5.1 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_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)
|
|
```
|
|
|
|
## 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:`, 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`.
|