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:
@@ -2,22 +2,76 @@
|
||||
|
||||
## Project goals
|
||||
|
||||
1. **Unify and systematize issue workflow** for the development team with minimal context usage. Issue operations (create, fetch, format) are wrapped in scripts so agents spend tokens on the task, not on re-deriving commands and formats.
|
||||
2. **Route all Gitea interaction through the `tea` CLI via scripts** instead of direct ad-hoc calls wherever possible. Scripts give deterministic, reviewable behavior; the `tea-guard` hook enforces that every `tea` invocation runs under the operator-pinned login.
|
||||
1. **Unify and systematize issue workflow** for the development team with
|
||||
minimal context usage. Issue operations are wrapped in scripts so agents
|
||||
spend tokens on the task, not on re-deriving commands and formats.
|
||||
2. **Keep the tracker out of the work.** An issue is a unit of work first and a
|
||||
Gitea row second. The two are separate layers, and the first one does not
|
||||
know the second exists.
|
||||
3. **Route all Gitea interaction through the `tea` CLI via scripts** instead of
|
||||
direct ad-hoc calls wherever possible. Scripts give deterministic,
|
||||
reviewable behavior; the `tea-guard` hook enforces that every `tea`
|
||||
invocation runs under the operator-pinned login.
|
||||
|
||||
## Layers
|
||||
|
||||
The hard rule of this repo. Knowledge flows one way only:
|
||||
|
||||
```
|
||||
skills/issue DOMAIN what an issue is: format, validation, dependency graph
|
||||
▲ offline — no tracker, no network, stdlib imports only
|
||||
│ imports
|
||||
skills/sync BRIDGE map.py md <-> Gitea JSON, pure functions, no I/O
|
||||
_gitea.py login pin, tea api, pagination, filters
|
||||
skills/use REFERENCE tea CLI docs for everything that is not an issue
|
||||
skills/auth IDENTITY pin the login the whole tracker side runs under
|
||||
```
|
||||
|
||||
`skills/issue` never imports from `skills/sync`. Delete `skills/sync` and the
|
||||
domain layer keeps working. The check is mechanical — every import under
|
||||
`skills/issue/scripts/` is stdlib, and `subprocess` is not among them:
|
||||
|
||||
```bash
|
||||
grep -rh '^import \|^from ' skills/issue/scripts/ | sort -u
|
||||
```
|
||||
|
||||
If a tracker concept (issue number, login, HTTP call, label color) shows up in
|
||||
the domain layer, it is in the wrong place.
|
||||
|
||||
## Repo layout
|
||||
|
||||
- `skills/auth` — pin the Gitea login used by `tea` (`/tea:auth`)
|
||||
- `skills/use` — `tea` CLI reference, loaded on demand (`/tea:use`); `references/` holds command docs and the canonical issue format; `scripts/` holds the issue scripts:
|
||||
- `issue_get.py` — fetch issue(s) into the local grep cache `tmp/issues/`, by key or by filter (`--milestone`, `--label`, `-q`; one request per 50 issues); `--deps` walks the dependency graph and writes `tree-<slug>.md`
|
||||
- `issue_push.py` — validate a local draft, create missing labels, POST it, delete the draft
|
||||
- `issue_list.py` — discovery to stdout; `issue_index.py` — rebuild `tmp/issues/INDEX.md`; `_tea.py` — shared login/api/format helpers
|
||||
- `skills/issue` — draft an issue locally in the canonical format, then push it (`/tea:issue`)
|
||||
- `skills/issue` — issues as units of work (`/tea:issue`), entirely offline
|
||||
- `references/format.md` — canonical issue format; single source of truth
|
||||
- `scripts/issue.py` — domain module: slug identity, parse/render, validation,
|
||||
taxonomy, dependency graph
|
||||
- `scripts/issue_new.py` — create a local issue from its type template
|
||||
- `scripts/issue_check.py` — validate against the format
|
||||
- `scripts/issue_tree.py` — draw the dependency graph
|
||||
- `scripts/issue_index.py` — rebuild `tmp/issues/INDEX.md`
|
||||
- `skills/sync` — move issues between the local store and Gitea (`/tea:sync`)
|
||||
- `scripts/map.py` — md ↔ Gitea JSON, pure, no I/O; label colors live here
|
||||
- `scripts/_gitea.py` — transport: login pin, `tea api`, pagination, filters,
|
||||
label ids, the remote-id map
|
||||
- `scripts/pull.py`, `push.py`, `remote.py`, `comment.py`
|
||||
- `skills/use` — `tea` CLI reference for everything that is not an issue
|
||||
(`/tea:use`); `references/tea/` holds the command docs
|
||||
- `hooks/` — PreToolUse hooks: `tea-guard` blocks or rewrites `tea` invocations
|
||||
that don't use the pinned login; `agents-sync` keeps every directory canonical
|
||||
(`AGENTS.md` real file, `CLAUDE.md` symlink to it)
|
||||
|
||||
## Local issue cache
|
||||
## Local issue store
|
||||
|
||||
`tmp/issues/` (gitignored) is a **cache and a drafting area, not a mirror**: no
|
||||
drift tracking, no sync back. Fetched issues are flat greppable markdown with
|
||||
one metadata field per line; drafts live in `tmp/issues/drafts/` until
|
||||
`issue_push.py` creates them in Gitea and removes the local file.
|
||||
- `hooks/` — PreToolUse hooks: `tea-guard` blocks or rewrites `tea` invocations that don't use the pinned login; `agents-sync` keeps every directory canonical (`AGENTS.md` real file, `CLAUDE.md` symlink to it)
|
||||
`tmp/issues/` (gitignored) is **the store, not a cache of Gitea**. One flat
|
||||
markdown file per issue, named by its slug, with one metadata field per line so
|
||||
plain grep works without a parser.
|
||||
|
||||
- Identity is the slug (`wire-sqlc-appclick.md`), never a tracker number.
|
||||
Numbers live in the `gitea:` field.
|
||||
- `origin: local` is a durable state. An issue that never leaves this machine is
|
||||
complete and valid, not a draft.
|
||||
- Pushing is additive: the file is never deleted, it gains `gitea:` / `url:` /
|
||||
`synced:`.
|
||||
- Pulling overwrites the body — a fetch, not a merge.
|
||||
- No drift tracking. `synced:` tells you how old your copy is; re-pull when it
|
||||
matters.
|
||||
|
||||
Reference in New Issue
Block a user