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>
3.6 KiB
3.6 KiB
AGENTS.md
Project goals
- 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.
- 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.
- Route all Gitea interaction through the
teaCLI via scripts instead of direct ad-hoc calls wherever possible. Scripts give deterministic, reviewable behavior; thetea-guardhook enforces that everyteainvocation 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:
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 bytea(/tea:auth)skills/issue— issues as units of work (/tea:issue), entirely offlinereferences/format.md— canonical issue format; single source of truthscripts/issue.py— domain module: slug identity, parse/render, validation, taxonomy, dependency graphscripts/issue_new.py— create a local issue from its type templatescripts/issue_check.py— validate against the formatscripts/issue_tree.py— draw the dependency graphscripts/issue_index.py— rebuildtmp/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 herescripts/_gitea.py— transport: login pin,tea api, pagination, filters, label ids, the remote-id mapscripts/pull.py,push.py,remote.py,comment.py
skills/use—teaCLI reference for everything that is not an issue (/tea:use);references/tea/holds the command docshooks/— PreToolUse hooks:tea-guardblocks or rewritesteainvocations that don't use the pinned login;agents-synckeeps every directory canonical (AGENTS.mdreal file,CLAUDE.mdsymlink to it)
Local issue store
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 thegitea:field. origin: localis 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.