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:
naudachu
2026-08-09 23:37:32 +05:00
parent 335b0bbd54
commit 091dceec1d
24 changed files with 2504 additions and 1284 deletions
+35 -136
View File
@@ -1,6 +1,6 @@
---
name: use
description: Reference docs for the `tea` CLI — Gitea's command-line client. Load when the user asks about Gitea repos, issues, pulls, releases, actions, or other Gitea entities, to look up the right `tea` command and flags. Always write the login as the literal placeholder --login "$GITEA_LOGIN" — the tea-guard hook substitutes the operator-pinned login; set it with /tea:auth.
description: Reference docs for the `tea` CLI — Gitea's command-line client. Load when the user asks about Gitea repos, pulls, releases, milestones, labels, actions, webhooks, or other Gitea entities, to look up the right `tea` command and flags. Always write the login as the literal placeholder --login "$GITEA_LOGIN" — the tea-guard hook substitutes the operator-pinned login; set it with /tea:auth. Issues are NOT handled here: use /tea:issue to work on them and /tea:sync to move them to and from Gitea.
---
# /tea:use — tea CLI reference
@@ -9,6 +9,19 @@ Reference material for the `tea` CLI (Gitea's official command-line client).
Use these docs to look up commands, flags, filters, and output fields before
running `tea` via Bash.
## 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 |
|---|---|
| `/tea:issue` | issues as units of work — create, read, grep, validate, dependency graph. Offline. |
| `/tea:sync` | moving issues between the local store and Gitea — pull, push, comment. |
This skill covers everything else Gitea has: pulls, releases, milestones,
labels, repos, branches, actions, webhooks, notifications, times.
## Login: always write the placeholder, never a name (enforced)
Every `tea` invocation that touches Gitea MUST carry the login as the **literal
@@ -33,12 +46,11 @@ The pin takes effect immediately — no restart. Only `tea logins list` and
## How to use
1. Identify the entity in the request: issues, pulls, labels, milestones,
releases, times, repos, branches, actions, webhooks, comments,
notifications, etc.
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 with the placeholder login, e.g.
`tea issues list --login "$GITEA_LOGIN" --repo owner/repo --state open`.
`tea pulls list --login "$GITEA_LOGIN" --repo owner/repo --state open`.
(The hook rewrites `"$GITEA_LOGIN"` to the operator-pinned login.)
`tea` auto-detects owner/repo from `$PWD` inside a git repo; otherwise pass
@@ -46,117 +58,6 @@ The pin takes effect immediately — no restart. Only `tea logins list` and
per-project by the operator (see `/tea:auth`) and injected by the guard.
Config lives in `$XDG_CONFIG_HOME/tea`.
## Issues: work on local files, not on live `tea` calls
Never run `tea issues <n> -o json` or `tea api .../issues/<n>` to read an
issue — the full JSON payload (avatars, nested user objects, every comment
body) lands in your context whether you need it or not. Use the scripts in
`<skill-base-dir>/scripts/`. They pull issues into a flat, greppable cache
under `$PWD/tmp/issues/` and print only a compact index.
This is a **cache, not a mirror**: nothing tracks drift, nothing syncs back.
Refetch when you need current data; the `fetched:` field tells you the age.
| Script | Network | What it does |
|---|---|---|
| `issue_get.py <key…>` or `issue_get.py --milestone M \| --label L \| -q TEXT` | yes | fetch issue(s) → `tmp/issues/<n>.md` (+ `<n>.comments.md`, `tree-<slug>.md`) |
| `issue_list.py [--state] [--label] [--milestone] [-q TEXT]` | yes | discovery: one line per issue to stdout, writes nothing |
| `issue_push.py <draft…>\|--all [--keep] [--dry-run]` | yes | validate a draft, create labels, POST the issue, delete the draft |
| `issue_index.py` | no | rebuild `tmp/issues/INDEX.md` (auto after get/push) |
```
tmp/issues/INDEX.md table of everything cached — read this first
tmp/issues/42.md metadata block + `# Title` + body
tmp/issues/42.comments.md comments (only with --comments)
tmp/issues/tree-40.md dependency map (only with --deps)
tmp/issues/drafts/<slug>.md issues not yet created in Gitea
```
Key forms for `<key>`: `42`, `#42`, `owner/repo#42`, or a full issue URL. Repo
defaults to the current directory's git remote (add `--repo owner/repo`
outside one). No `--login` on any script call: they resolve the operator's pin
from `.claude/settings.local.json` themselves — same source as the tea-guard
hook. No pin → exit with a pointer to `/tea:auth`.
### Fetching a whole set: milestone, label, search
Do not loop `issue_get.py` over numbers to pull a group — pass the filter. The
list endpoint carries the issue bodies, so a milestone costs **one request per
50 issues**, not one per issue:
```bash
python3 <skill-base-dir>/scripts/issue_get.py --milestone 6 # id or title
python3 <skill-base-dir>/scripts/issue_get.py --milestone v0.2 --deps
python3 <skill-base-dir>/scripts/issue_get.py --label type/bug --label comp/hooks --state all
python3 <skill-base-dir>/scripts/issue_get.py -q sqlc --limit 20
```
Filters AND together; `--state` defaults to `open`; `--limit` defaults to 100.
Keys and filters are mutually exclusive. `--comments` stays single-issue —
loop over the numbers when a whole thread set is needed.
Two traps this handles for you:
- **Gitea silently ignores an unresolvable milestone filter** and returns the
whole backlog. The script resolves the milestone first (exits listing the
real ones if it does not exist) and re-checks every returned issue locally.
Never trust a raw `tea api ...issues?milestones=X` call for this.
- **Projects are not fetchable.** The projects API is not exposed (404 on
Gitea 1.26 for `repos/…/projects`, `orgs/…/projects`, `projects/{id}`).
Use milestones or labels; project columns live in the web UI only.
After a filtered fetch, `INDEX.md` carries a `milestone` column, and the cache
is greppable by it: `grep -l 'milestone: v0.2' tmp/issues/*.md`.
### Working a feature as one document
`--deps` walks the dependency graph **downwards** — the structured
`## Depends on` and `## Issues` sections plus Gitea's native dependencies.
Prose `#N` mentions are ignored on purpose, or the walk would drag in half the
backlog. Comments are not fetched during a walk (loop over the numbers if you
need them).
```bash
python3 <skill-base-dir>/scripts/issue_get.py 40 --deps # feature + children
```
Read `tree-40.md` once for the shape (a filtered fetch writes one forest,
`tree-<slug>.md`), then grep the files as one document:
```bash
grep -ln 'depends:.*#42' tmp/issues/*.md # who depends on #42 (upwards)
grep -l 'labels:.*type/bug' tmp/issues/*.md # all cached bugs
grep -A3 '## Acceptance criteria' tmp/issues/4*.md
grep -c '^- \[ \]' tmp/issues/42.md # open checkboxes
```
Metadata is written one field per line with inline lists (`labels: [a, b]`)
precisely so plain grep works without a parser.
### Creating issues: draft locally, push once
During planning write drafts to `tmp/issues/drafts/<slug>.md` — no network, no
`tea` call. A draft is the metadata block with `labels:` only, plus the
canonical body:
```markdown
---
labels: [type/task, tech/sql]
---
# Wire sqlc into the appclick repo layer
## Summary
...
```
When the plan is agreed, `issue_push.py` validates the format (exactly one
`type/*`, English title without a type prefix, `## Summary` / `## Spec` /
`## Acceptance criteria` present), creates missing labels with the right
colors and exclusivity, POSTs, prints the URL and **deletes the draft** — the
issue lives in Gitea now. `--keep` writes `tmp/issues/<n>.md` instead;
`--dry-run` validates without touching the network. Guided procedure:
`/tea:issue`.
## Index
- [tea CLI overview](references/tea/index.md) — global flags, common options, output formats
@@ -164,15 +65,14 @@ issue lives in Gitea now. `--keep` writes `tmp/issues/<n>.md` instead;
- [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
- [ISSUE FORMAT](references/issue-format.md) — canonical issue format: label
namespaces (`type/*`, `severity/*` exclusive; `tech/*`, `comp/*` free),
types `bug|task|refactor|test|feature|draft`, templates, dependencies,
title and language rules. MANDATORY whenever creating or editing an issue;
the `/tea:issue` skill is the guided procedure for it.
The canonical issue format moved to
[`../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 issues create`, `tea pulls create`, …)
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 `$EDITOR` fallback, or a
scope/confirm prompt waits on a TTY that doesn't exist. The harness eventually
@@ -182,30 +82,29 @@ kills the process (e.g. exit 144 = 128 + SIGURG on macOS).
fences / backticks / pipes / tables), bypass entity commands. Save the full
request payload to `$PWD/tmp/` first, then POST via `tea api`.
Issue creation is already wrapped: use `issue_push.py` (above) instead of
hand-rolling the JSON. The procedure below covers everything else — comments,
pulls, releases, and PATCHes to existing issues.
Issues and issue comments are already wrapped use `/tea:sync` rather than
hand-rolling their JSON. The procedure below covers everything else.
### Procedure
1. Ensure the target dir exists: `mkdir -p tmp/{kind}` where `{kind}` is
`comment`, `issue`, `pull`, `release`, etc.
`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/comment
cat > tmp/comment/issue-60.json <<'EOF'
{"body": "## Heading\n\nMulti-line markdown with `code`, | tables |, and ```fences```."}
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/comment/issue-60.json`.
`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 --login "$GITEA_LOGIN" \
-X POST -d @tmp/comment/issue-60.json \
repos/{owner}/{repo}/issues/60/comments
-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.
@@ -214,12 +113,12 @@ pulls, releases, and PATCHes to existing issues.
| Action | Method + endpoint |
|---|---|
| Comment on issue/PR | `POST repos/{owner}/{repo}/issues/{n}/comments` |
| Edit comment | `PATCH repos/{owner}/{repo}/issues/comments/{id}` |
| Create issue | `POST repos/{owner}/{repo}/issues` |
| Edit issue/PR body or title | `PATCH repos/{owner}/{repo}/issues/{n}` |
| 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" --login "$GITEA_LOGIN"`)
are still fine via entity commands. Always the placeholder, never a login name.