# AGENTS.md ## Project goals 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 ▲ │ calls agents/ EXECUTION tea-runner: runs the scripts, reports a receipt ``` `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/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, body checkboxes - `scripts/issue_new.py` — create a local issue from its type template - `scripts/issue_check.py` — validate against the format - `scripts/issue_ac.py` — list the body's checkboxes; tick one by number or substring, changing exactly one character of the file - `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 - `agents/tea-runner.md` — subagent on Haiku that executes the scripts and returns a compact receipt. Delegate batches (bulk pull, push a named set, bootstrap labels, rebuild the index), never the thinking: it has no `Edit` and no `Write`, may not `--force`, and may not decide what an issue says. Delegating a single call costs more than running it inline — the win is the loop, the retry, and the error triage. - `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) - `tests/` — stdlib `unittest`, no pytest and no third-party deps: the scripts under test may not have dependencies, so neither may their tests. Scripts are imported via `sys.path.insert` (`skills/*/scripts/` are not packages), and fixtures are built in a `tempfile.TemporaryDirectory()` — never in `tmp/`. ```bash python3 -m unittest discover -s tests -v ``` ## 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 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.