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
+60 -22
View File
@@ -7,11 +7,28 @@ A Claude Code plugin that gives Claude a reference for the `tea` CLI and enforce
| Piece | What it does |
|---|---|
| `/tea:auth` skill | Prompts you to pick a Gitea login and pins it to the project |
| `/tea:use` skill | Tea CLI reference — loads command docs on demand |
| `/tea:issue` skill | Drafts issues locally in a canonical format (typed labels, fixed sections), then pushes them |
| Issue scripts | Fetch issues into a flat, greppable local cache (`tmp/issues/`), walk dependency trees, push drafts |
| `/tea:issue` skill | Issues as units of work — create, read, grep, validate, walk the dependency graph. Entirely offline |
| `/tea:sync` skill | Moves issues between the local store and Gitea — pull, push, comment |
| `/tea:use` skill | Tea CLI reference for everything that is not an issue — loads command docs on demand |
| `tea-guard` hook | PreToolUse hook that blocks or rewrites every `tea` invocation |
## The layering
An issue is a unit of work first and a Gitea row second. Those are two layers,
and knowledge flows one way:
```
skills/issue DOMAIN what an issue is: format, validation, dependency graph
▲ offline — no tracker, no network, stdlib only
│ imports
skills/sync BRIDGE md <-> Gitea JSON, then over the wire
```
Delete `skills/sync` and the domain layer keeps working — issues that live only
on your machine are first-class, not drafts waiting to be uploaded. That is the
point of the split: you can plan, write, validate, and track work without a
tracker, and publish only what you choose to.
## Prerequisites
- **Claude Code** — CLI, desktop app, or IDE extension
@@ -41,7 +58,7 @@ This is a Claude Code plugin — install it through the plugin marketplace, not
/plugin install tea@tea
```
The skills (`/tea:auth`, `/tea:use`, `/tea:issue`) and the `tea-guard` hook load immediately. Use `/plugin` to enable, disable, or update it later.
The skills (`/tea:auth`, `/tea:issue`, `/tea:sync`, `/tea:use`) and the `tea-guard` hook load immediately. Use `/plugin` to enable, disable, or update it later.
> The marketplace registration is written to `extraKnownMarketplaces` and the plugin to `enabledPlugins` in your settings automatically — you don't edit those by hand. There is **no** top-level `"plugins"` settings key; if you've added one from older instructions, remove it.
@@ -53,7 +70,9 @@ Run `/tea:auth` once per project. Claude will list your available Gitea logins a
/tea:auth
```
After that, use `/tea:use` to look up commands, or just ask Claude to do something with Gitea and it will load the reference automatically.
After that, just ask Claude to do something with issues or Gitea — it loads the
right skill automatically. `/tea:auth` is only needed for the tracker side;
`/tea:issue` works without any login at all.
## How the login guard works
@@ -79,23 +98,42 @@ hooks/
tea-guard.sh the guard (Python 3, no deps)
skills/
auth/SKILL.md /tea:auth skill
use/SKILL.md /tea:use skill
use/references/tea/ tea CLI reference docs
use/references/issue-format.md canonical issue format (types, templates)
use/scripts/ issue scripts (Python 3, no deps):
issue_get.py fetch issues into tmp/issues/, --deps walks the graph
issue_push.py validate a local draft, create labels, POST, drop the draft
issue_list.py discovery listing to stdout
issue_index.py rebuild tmp/issues/INDEX.md (no network)
_tea.py shared login / api / on-disk-format helpers
issue/SKILL.md /tea:issue skill
issue/ /tea:issue — the domain layer, offline
SKILL.md
references/format.md canonical issue format (identity, types, templates)
scripts/ Python 3, stdlib only, no network:
issue.py domain module: slug identity, parse/render,
validation, taxonomy, dependency graph
issue_new.py create a local issue from its type template
issue_check.py validate against the format
issue_tree.py draw the dependency graph
issue_index.py rebuild tmp/issues/INDEX.md
sync/ /tea:sync — the bridge to Gitea
SKILL.md
scripts/
map.py md <-> Gitea JSON, pure functions, no I/O
_gitea.py transport: login pin, tea api, pagination, filters
pull.py Gitea -> tmp/issues/
push.py tmp/issues/ -> Gitea (additive; never deletes)
remote.py discovery listing to stdout
comment.py post or edit a comment
use/ /tea:use — tea CLI reference (non-issue entities)
SKILL.md
references/tea/ command docs
```
## Local issue cache
## Local issue store
The scripts keep issues in `tmp/issues/` (gitignore it) as flat markdown with
one metadata field per line — so `grep -l 'labels:.*type/bug' tmp/issues/*.md`
works without a parser. It is a **cache and a drafting area, not a mirror**:
nothing tracks drift and nothing syncs back. Drafts written during planning
live in `tmp/issues/drafts/` and are deleted once `issue_push.py` creates them
in Gitea.
Issues live in `tmp/issues/` (gitignore it) as flat markdown with one metadata
field per line — so `grep -l 'labels:.*type/bug' tmp/issues/*.md` works without
a parser.
It is **the store, not a cache of Gitea**:
- Identity is a slug (`wire-sqlc-appclick.md`), never a tracker number. Numbers
live in a `gitea:` field.
- `origin: local` is a durable state. An issue that never leaves your machine is
complete and valid.
- Pushing is additive — the file gains `gitea:` / `url:` / `synced:` and stays
put. Pulling overwrites the body: a fetch, not a merge.
- Nothing tracks drift. `synced:` tells you how old your copy is.