feat: add the tea-runner execution agent

The skills carry meaning, the scripts carry work. Splitting the second
half onto a cheap model keeps the main session's context for the part
that needs judgement.

tea-runner is a Haiku subagent with Bash/Read/Grep/Glob/Skill and
nothing else. It loads /tea:sync or /tea:issue for the command table
rather than carrying its own copy, so the skills stay the single source
of truth for the script surface.

It executes and reports; it decides nothing. No Edit and no Write, so an
issue body is out of reach. No raw tea, no --force, no closing or
retitling, no pushing past the set it was handed, one retry maximum. A
failed validation, a missing type, an unpushed dependency come back as a
question in a `blocked:` line. The reply is a fixed receipt — commands
with ok/FAIL, touched paths, stderr verbatim — never a payload dump.

Knowledge still flows one way: nothing under skills/ knows the agent
exists, and deleting agents/ changes nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
naudachu
2026-08-10 15:02:34 +05:00
parent 9234d8004f
commit d8bd927f1d
5 changed files with 144 additions and 3 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
{ {
"name": "tea", "name": "tea",
"source": "./", "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."
} }
] ]
} }
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "tea", "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.", "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.0.0", "version": "2.1.0",
"author": { "author": {
"name": "naudachu" "name": "naudachu"
}, },
+9
View File
@@ -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 _gitea.py login pin, tea api, pagination, filters
skills/use REFERENCE tea CLI docs for everything that is not an issue 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/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 `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` - `scripts/pull.py`, `push.py`, `remote.py`, `comment.py`
- `skills/use``tea` CLI reference for everything that is not an issue - `skills/use``tea` CLI reference for everything that is not an issue
(`/tea:use`); `references/tea/` holds the command docs (`/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 - `hooks/` — PreToolUse hooks: `tea-guard` blocks or rewrites `tea` invocations
that don't use the pinned login; `agents-sync` keeps every directory canonical that don't use the pinned login; `agents-sync` keeps every directory canonical
(`AGENTS.md` real file, `CLAUDE.md` symlink to it) (`AGENTS.md` real file, `CLAUDE.md` symlink to it)
+24
View File
@@ -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: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: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: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 | | `tea-guard` hook | PreToolUse hook that blocks or rewrites every `tea` invocation |
## The layering ## The layering
@@ -22,6 +23,9 @@ skills/issue DOMAIN what an issue is: format, validation, dependency grap
▲ offline — no tracker, no network, stdlib only ▲ offline — no tracker, no network, stdlib only
│ imports │ imports
skills/sync BRIDGE md <-> Gitea JSON, then over the wire 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 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. `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 ## Project layout
``` ```
.claude-plugin/ .claude-plugin/
plugin.json plugin manifest plugin.json plugin manifest
marketplace.json marketplace catalog (makes `/plugin install` work) marketplace.json marketplace catalog (makes `/plugin install` work)
agents/
tea-runner.md subagent (Haiku) that executes the scripts
hooks/ hooks/
hooks.json registers the PreToolUse hook hooks.json registers the PreToolUse hook
tea-guard.sh the guard (Python 3, no deps) tea-guard.sh the guard (Python 3, no deps)
+108
View File
@@ -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 |