# tea — Claude Code plugin for the Gitea CLI A Claude Code plugin that gives Claude a reference for the `tea` CLI and enforces a hard rule: every `tea` command runs under the login **the operator chose**, never one Claude picked. ## What it ships | Piece | What it does | |---|---| | `/tea:auth` skill | Prompts you to pick a Gitea login and pins it to the project | | `/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-runner` agent | Subagent on Haiku that runs the scripts and reports back a receipt — the mechanical half, off your main context | | `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 ▲ │ calls tea-runner EXECUTION runs the scripts, reports a receipt — no opinions ``` 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 - **Python 3** — required by the `tea-guard` hook (`python3` must be on `$PATH`) - **`tea`** — Gitea's official CLI. Install with `brew install tea` (macOS) or from [gitea.com/gitea/tea/releases](https://gitea.com/gitea/tea/releases) - At least one login configured: `tea logins add` (interactive — run it in a terminal, not via Claude) ## Installation This is a Claude Code plugin — install it through the plugin marketplace, not by hand-editing `settings.json`. 1. Register this repo as a marketplace: ``` /plugin marketplace add https://git.noodles.cam/claude-skills/tea.git ``` Already have a local clone? Point at the directory instead: ``` /plugin marketplace add /path/to/tea ``` 2. Install the plugin: ``` /plugin install tea@tea ``` 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. ## First use Run `/tea:auth` once per project. Claude will list your available Gitea logins and ask you to pick one. The choice is written to `.claude/settings.local.json` and takes effect immediately — no restart needed. ``` /tea:auth ``` 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 Every `tea` invocation Claude writes must carry the literal placeholder `--login "$GITEA_LOGIN"`. The `tea-guard` hook intercepts the Bash call before it runs, looks up the pinned login from `.claude/settings.local.json`, and rewrites the command to use it. Claude is **blocked** from: - running `tea` without `--login` at all - naming a login itself (e.g. `--login myaccount`) - using any variable other than `$GITEA_LOGIN` This prevents silent fallback to the machine's default login (often a personal account) when working in a project that belongs to a different identity. `tea logins list` and `tea --version / --help` are exempt — they don't touch Gitea data. ## The tea-runner agent The skills carry meaning; the scripts carry work. `tea-runner` is a subagent on Haiku that does the second half in its own context and hands back a receipt — what ran, what it touched, what failed, verbatim. Delegate a **batch**: pull a milestone and rebuild the index, push the three issues you just wrote, bootstrap the label set, post a comment from a file you prepared. Spawning it for a single `pull.py 42` costs more than running the command yourself; the saving is in the loop, the retry, and reading somebody else's stderr. It cannot decide anything. No `Edit`, no `Write`, no `--force`, no closing or retitling, no raw `tea`, no pushing beyond the set it was handed. A missing type, a failed validation, an unpushed dependency come back as a question, not as a guess. The `tea-guard` hook applies to it exactly as it does to the main session — the pinned login is enforced on every call it makes. ## Project layout ``` .claude-plugin/ plugin.json plugin manifest marketplace.json marketplace catalog (makes `/plugin install` work) agents/ tea-runner.md subagent (Haiku) that executes the scripts hooks/ hooks.json registers the PreToolUse hook tea-guard.sh the guard (Python 3, no deps) skills/ auth/SKILL.md /tea:auth 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 store 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.