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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user