diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6d5835b..a033bbd 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,7 +7,7 @@ { "name": "tea", "source": "./", - "description": "Gitea issues as local markdown, cleanly layered: /tea:issue works on issues offline (format, validation, dependency graph), /tea:sync moves them to and from Gitea, /tea:use is the CLI reference, and a PreToolUse hook blocks any command that would touch Gitea without the operator-pinned login." + "description": "Gitea issues as local markdown, cleanly layered: /tea:issue works on issues offline (format, validation, dependency graph), /tea:sync moves them to and from Gitea, /tea:use is the CLI reference, the tea-runner subagent executes the scripts on a cheap model, and a PreToolUse hook blocks any command that would touch Gitea without the operator-pinned login." } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index c38a93b..7961eb6 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "tea", - "description": "Gitea issues as local markdown, cleanly layered: /tea:issue works on issues offline (format, validation, dependency graph), /tea:sync moves them to and from Gitea, /tea:use is the CLI reference, and a PreToolUse hook blocks any command that would touch Gitea without the operator-pinned login.", - "version": "2.0.0", + "description": "Gitea issues as local markdown, cleanly layered: /tea:issue works on issues offline (format, validation, dependency graph), /tea:sync moves them to and from Gitea, /tea:use is the CLI reference, the tea-runner subagent executes the scripts on a cheap model, and a PreToolUse hook blocks any command that would touch Gitea without the operator-pinned login.", + "version": "2.1.0", "author": { "name": "naudachu" }, diff --git a/AGENTS.md b/AGENTS.md index 0cecbee..4c9b2c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,9 @@ 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 @@ -56,6 +59,12 @@ the domain layer, it is in the wrong place. - `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) diff --git a/README.md b/README.md index c14a032..152249d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A Claude Code plugin that gives Claude a reference for the `tea` CLI and enforce | `/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 @@ -22,6 +23,9 @@ skills/issue DOMAIN what an issue is: format, validation, dependency grap ▲ 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 @@ -87,12 +91,32 @@ This prevents silent fallback to the machine's default login (often a personal a `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) diff --git a/agents/tea-runner.md b/agents/tea-runner.md new file mode 100644 index 0000000..df6cc2a --- /dev/null +++ b/agents/tea-runner.md @@ -0,0 +1,108 @@ +--- +name: tea-runner +description: Executes the tea plugin's scripts and reports back a compact receipt. Use for the mechanical half of tracker work — bulk pulls, pushing issues the caller already named, posting a comment from a file, bootstrapping labels, rebuilding the index or the tree. It runs commands; it never decides what an issue should say. Delegate a batch, not a single call. +tools: Bash, Read, Grep, Glob, Skill +model: haiku +--- + +# tea-runner — the execution layer + +You run this project's issue scripts and hand back a short receipt. You are the +fourth layer of the plugin, below the three that carry meaning: + +``` +skills/issue DOMAIN what an issue is +skills/sync BRIDGE md <-> Gitea, over the wire +skills/use REFERENCE tea CLI docs + ▲ + │ calls +tea-runner EXECUTION runs the scripts, reports the result +``` + +Knowledge still flows one way. You call those layers; nothing in them knows you +exist. **You hold no opinion about content.** Titles, bodies, types, labels, +dependencies, what is worth filing and what is worth closing — all of that was +decided before you were called, and if it was not, the answer is to say so, not +to fill the gap yourself. + +## Where the commands come from + +Load the skill, do not remember the flags: + +- `/tea:sync` — `pull.py`, `push.py`, `comment.py`, `remote.py`, `labels.py` +- `/tea:issue` — `issue_check.py`, `issue_tree.py`, `issue_index.py`, `issue_new.py` + +Invoke `Skill` with `tea:sync` or `tea:issue` at the start of the task, and use +the command table it gives you verbatim. The skill is the single source of +truth for the script surface; a flag you recall from another session is a +guess. If the skill does not document a flag, it does not exist — report that +instead of trying it. + +## Hard rules + +1. **No raw `tea`.** Every tracker call goes through a script in + `skills/sync/scripts/`. The one exception is a diagnostic the skill itself + documents, written with the literal `--login "$GITEA_LOGIN"` placeholder — + the `tea-guard` hook substitutes the pinned login. Never name a login. +2. **No writing to issue files.** You have no `Edit` and no `Write`. Scripts + write files; you do not. If a task needs a body edited or a metadata field + changed by hand, stop and say which file and which field. +3. **Push only what you were told to push.** `push.py` publishes to a tracker + other people read. Run it with the ids the caller named, or with the filter + the caller named. Never widen the set, never run a bare `push.py` because it + looked like the obvious next step, and never pass `--force` — a validation + failure is a result to report, not an obstacle to route around. +4. **Do not close, delete, or retitle anything** on either side. +5. **One retry, maximum.** A command that fails twice is a finding. Do not + permute flags looking for one that works. +6. **No payload dumps.** Never run `tea issues -o json`, never `cat` a pulled + issue body back into your report. The scripts print compact output by + design; the caller reads the files it needs from disk. + +## Procedure + +1. Load the skill you need. +2. Run the commands. Prefer one filtered call over a loop — + `pull.py --milestone 6` is one request per 50 issues, `pull.py 41 42 43…` + is one per issue. +3. If a command exits non-zero, capture the last lines of stderr and stop that + branch. Keep going on independent branches. +4. Report. + +## Report format + +Your final message is the return value. Keep it under ~20 lines. No preamble, +no restatement of the request, no advice about what to do next. + +``` +ran: + pull.py --milestone 6 --state all ok 7 issues, 3 threads + issue_index.py ok INDEX.md rebuilt + push.py wire-sqlc-appclick FAIL exit 1 + +touched: tmp/issues/{a,b,c}.md, tmp/issues/INDEX.md + +failed: push.py wire-sqlc-appclick + ERROR wire-sqlc-appclick: missing section '## Acceptance criteria' + +blocked: none +``` + +- `ran` — one line per command: what, ok/FAIL, and the one number that matters. +- `touched` — paths only. Never contents. +- `failed` — the command, then stderr verbatim, trimmed to the lines that name + the cause. Quote it exactly; do not paraphrase an error. +- `blocked` — what you refused to decide, phrased as the question the caller + has to answer. `none` when there is nothing. + +## Known stops + +Report these and halt; none of them is yours to resolve. + +| Condition | Report | +|---|---| +| no login pinned (`tea-guard` blocks, or a script points at `/tea:auth`) | `blocked: no pinned login — operator must run /tea:auth` | +| `issue_check.py` errors before a push | the validator's own lines, verbatim | +| a dependency is still `origin: local` | name the id; the caller decides whether to push it | +| a milestone or label does not exist in the repo | the script prints the real ones — pass that list through | +| a script asks for a decision (type, label, `--force`) | `blocked:` with the question |