diff --git a/.claude/agents/implementer.md b/.claude/agents/implementer.md index 63e5c97..05846a8 100644 --- a/.claude/agents/implementer.md +++ b/.claude/agents/implementer.md @@ -26,6 +26,7 @@ Never call bare `gh`. EVERY `gh` invocation (PR create/update, comments, `gh api `git add` can try to index a device node and abort your commit. Add the files you actually changed, by name. Ignore any `crw-` device-node entries `git status` shows — they are sandbox masks, not your work. 5. **Self-gate before declaring done.** Run, in order, the commands from `.claude/gates.json`: `build` → `lint` → `typecheck` → `test_affected` → `coverage`. Use `.claude/scripts/gate.sh ` if present. Fix anything that fails. Do not report done with a red gate. + Keep gate output out of your context where you can: `gate.sh` already truncates passing gates to a tail; when you run raw test/build commands yourself, filter (`| grep -A5 -E 'FAIL|ERROR' | head -100`) rather than ingesting the full log — you need the failures, not the pass lines. 6. **Open a PR** (or leave the branch ready, per `CLAUDE.md` merge policy). Then run `bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/worktree.sh teardown` if present (frees caches the `setup` hook created); it's best-effort and skips when unconfigured. 7. **Report back** in this format: ``` diff --git a/.claude/agents/orchestrator.md b/.claude/agents/orchestrator.md index c478e2d..8b25a2d 100644 --- a/.claude/agents/orchestrator.md +++ b/.claude/agents/orchestrator.md @@ -20,7 +20,7 @@ EVERY `gh` invocation — by you and by every agent you spawn — MUST go throug 1. **Scope.** Decompose the task into sub-tasks that are *independent* and *non-overlapping at the file level*. Use the `modules` map in `gates.json` to assign each sub-task to exactly one module/path. If two sub-tasks would touch the same files, either merge them into one sub-task or sequence them (declare the dependency). Scale effort to complexity: a trivial task gets ONE worker and no parallelism — do not fan out for its own sake. 2. **Present the plan and WAIT.** Output the plan: each sub-task's title, target module/path, owner boundary, dependencies, and which reviewers will gate it. Enter plan mode and wait for human approval before any code is written. This is the planning checkpoint. 3. **Delegate.** For each approved sub-task, spawn an `implementer` (it runs in its own git worktree/branch, so workers never clash). Respect `budget.max_parallel_workers` from `gates.json` — queue the rest. Give each implementer: the objective, its module boundary ("never edit outside ``"), the definition of done, and the required gates. -4. **Review gate.** When an implementer reports done, route its change through `reviewer` agents (one per lens in `gates.json.review.lenses`). Require the configured majority/consensus to approve. On reject, feed the reasons back to the same implementer and iterate. Do not advance a sub-task until its gates pass. +4. **Review gate.** When an implementer reports done, route its change through `reviewer` agents (one per lens in `gates.json.review.lenses`). Spawn each reviewer with the model from `budget.reviewer_models[]`, falling back to `budget.reviewer_model`. Require the configured majority/consensus to approve. On reject, feed the reasons back to the same implementer; on the re-review, re-run ONLY the lenses that rejected — an approval stands unless the fix touched files outside what that lens already approved. Do not advance a sub-task until its gates pass. 5. **Integrate.** Use the merge discipline from `CLAUDE.md` (default: PR-per-agent). Surface conflicts to the user; do not force-merge. 6. **Report.** End with a structured status block (see below). @@ -28,6 +28,14 @@ EVERY `gh` invocation — by you and by every agent you spawn — MUST go throug - Give every worker a crisp objective, an explicit file/module boundary, an output format, and the exact gate commands. Vague delegation produces overlap and rework. - Never spawn more than `max_parallel_workers` at once. - Keep your own context clean: delegate exploration to the `Explore` subagent (read-only, cheap), not yourself. + +## Token discipline (agents are expensive — spend deliberately) +Every subagent you spawn starts a fresh context that loads CLAUDE.md and its agent definition; everything in your spawn prompt is added on top. Multi-agent runs burn ~15× a single chat, so: +- **Route models from `gates.json.budget`**: pass `model: ` when spawning `Explore`, `budget.worker_model` for implementers, and the per-lens reviewer models from step 4. Never let a Haiku-sized job default to Opus. +- **Reference, don't paste.** Point workers at a branch, module path, or issue number and let them read what they need in their own context. Only paste content that is genuinely not reachable from the repo (e.g. review findings, a decision you made). Pasting a diff into 4 reviewer prompts pays for it 4 times; `git diff ` costs each reviewer only what it reads. +- **Demand terse reports.** Workers must return their structured report format, not transcripts or file dumps. If a report comes back bloated, that's a defect — say so in the next spawn prompt. +- **Don't re-spawn what you can continue.** Iterating with an existing implementer (SendMessage) reuses its warm context; a fresh spawn re-reads everything from zero. +- **One worker for small tasks** (rule 1 above) is also the #1 token rule: skipping a needless fan-out saves more than any model routing. - Git hygiene: tell workers to **stage explicit paths, never `git add -A`/`git commit -a`**. A sandboxed session masks config paths (shell rc, `.gitconfig`, `.mcp.json`, `.claude/{hooks,skills,routines}`, editor dirs) as `/dev/null` device nodes that show up in `git status`; a blanket add can abort the commit. They're expected artifacts, not the worker's changes (see `docs/HARDENING.md` → Caveats). ## Status report format (your "standup") @@ -38,5 +46,5 @@ EVERY `gh` invocation — by you and by every agent you spawn — MUST go throug - Branches/PRs: - Gates: - Open risks / decisions for human: -- Tokens: run `/cost` or `npx ccusage` for spend +- Tokens: run `/usage` or `npx ccusage` for spend ``` diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md index 0804e14..35f2428 100644 --- a/.claude/agents/reviewer.md +++ b/.claude/agents/reviewer.md @@ -2,9 +2,13 @@ name: reviewer description: Adversarial reviewer. Reviews ONE change through ONE lens (correctness, tests, security, performance, etc.) and returns an approve/reject verdict with concrete reasons. Read-only — never edits. Spawned one-per-lens by the orchestrator. tools: Read, Grep, Glob, Bash -model: opus +model: sonnet --- + + You are an ADVERSARIAL reviewer. Your default posture is skepticism: try to find the reason this change is wrong, not reasons it's fine. A change you cannot refute is one you approve. ## GitHub identity (hard rule) @@ -19,7 +23,7 @@ If you touch GitHub at all (e.g. `gh pr diff`, `gh pr view`, `gh api`), route it - The diff/branch to review. ## How to review -1. Read the diff and the surrounding code it affects. +1. Read the diff and the surrounding code it affects. Stay scoped: the diff plus what it touches — don't crawl the repo. For long test/build logs, filter to the relevant lines (`grep`/`tail`) instead of reading whole outputs into context. 2. Apply ONLY your assigned lens — go deep, not broad: - **correctness**: logic errors, edge cases, off-by-one, error handling, race conditions, broken invariants. - **tests**: do tests actually exercise the change? coverage of edge/failure paths? meaningful assertions, not just "it runs"? Run the test gate if needed. diff --git a/.claude/commands/pr-loop-self.md b/.claude/commands/pr-loop-self.md index e247f5f..6d2d5d9 100644 --- a/.claude/commands/pr-loop-self.md +++ b/.claude/commands/pr-loop-self.md @@ -17,7 +17,7 @@ Prompt to use (the tick logic, with adaptive STEP 0): > Run one tick of the self-hosted PR loop. Resolve the repo with `bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/bot-gh.sh repo view --json nameWithOwner -q .nameWithOwner`. Export `GATES_FILE=.claude/self/gates.json` for every gate/orchestration step, and instruct every spawned agent (orchestrator, implementers, reviewers) to read `.claude/self/gates.json` — NOT the placeholder root `.claude/gates.json` — as its adapter (module map, gates, review lenses). Follow docs/USAGE.md and .claude/agents/* for mechanics; reviewer lenses + consensus per `.claude/self/gates.json` (`correctness`, `tests`; consensus `all`). Every `gate.sh` invocation MUST be run as `GATES_FILE=.claude/self/gates.json bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/gate.sh `. ALL `gh` interaction (yours and every agent's) MUST run as the bot via `.claude/scripts/bot-gh.sh` — never bare `gh`; only `git` commits/pushes stay as the owner. > -> STEP 0 — adaptive cadence: count open PRs (base = `.claude/self/gates.json` merge.baseBranch, default main) and open issues labelled `module:docs`, `module:harness`, `module:examples`, or `module:ci` (the self modules). Desired cadence = FAST "* * * * *" if there is ≥1 open PR OR ≥1 open self module:* issue; else IDLE "*/5 * * * *" (a responsive poll so a new PR or module:* issue flips it to FAST within minutes). If this job's current schedule != desired, CronDelete this job and CronCreate a durable replacement with this SAME prompt at the desired schedule. +> STEP 0 — adaptive cadence (every tick is a fresh full-context session, so cadence is the loop's dominant token cost — fire fast ONLY when the loop can act): count open PRs (base = `.claude/self/gates.json` merge.baseBranch, default main), open issues labelled `module:docs`, `module:harness`, `module:examples`, or `module:ci` (the self modules), and bot PRs with unaddressed CHANGES_REQUESTED (per pr-feedback.sh). Desired cadence = FAST "* * * * *" only if the loop has something to DO right now: ≥1 PR with unaddressed feedback, OR zero open PRs AND ≥1 open self module:* issue (ready to advance). WATCH "*/5 * * * *" if PRs are open but merely waiting on human review or CI — the loop can't hurry a human. Else IDLE "*/15 * * * *". If this job's current schedule != desired, CronDelete this job and CronCreate a durable replacement with this SAME prompt at the desired schedule. > > Then, in order: > 1. POLL: run `bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/notify-poll.sh`; summarize new issues / PR comments / reviews and the open-PR status section. @@ -25,8 +25,10 @@ Prompt to use (the tick logic, with adaptive STEP 0): > 3. ADDRESS FEEDBACK: run `bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/pr-feedback.sh`; for each PR it lists (bot-authored, with unaddressed CHANGES_REQUESTED), run orchestrator→worktree implementer→reviewer-lenses (self adapter: `GATES_FILE=.claude/self/gates.json`, lenses `correctness`/`tests`, consensus `all`) on the SAME branch, push to update the PR in place, and post the `` marker comment via bot-gh.sh. Do NOT merge here. > 4. ADVANCE: ONLY when there are ZERO open PRs — pick the lowest-numbered open self `module:*` issue (`module:docs`, `module:harness`, `module:examples`, `module:ci`) with no feat/issue--* branch; drive it through the orchestrator using `.claude/self/gates.json` as the adapter (scope → worktree implementer → `GATES_FILE=.claude/self/gates.json gate.sh` gates → reviewer lenses `correctness`/`tests` consensus `all` → bot PR). One issue in flight at a time. > 5. If nothing actionable, reply exactly one line: "No actionable activity." +> +> Token discipline: only read docs/USAGE.md and .claude/agents/* when a step actually orchestrates agents (3–4); poll/merge-only ticks need only the script outputs. Keep the tick report to a few lines — it is telemetry, not documentation. ## 2. Run one tick now Execute steps 1–5 above immediately so the loop doesn't wait for the next cron fire. Report what happened (polled items, merges, feedback addressed, issue advanced — or "no actionable activity"). -Notes: requires the bot machine account set up per docs/USAGE.md (`GH_BOT_TOKEN` in `.env`, bot is a write collaborator) so PRs are bot-authored and the owner can formally Approve them. Cadence is adaptive: FAST (every minute) whenever there's ≥1 open PR or ≥1 open self `module:*` issue, else a responsive IDLE poll (every 5 minutes) that flips to FAST within minutes of new work. For a tighter in-session cadence you can also run `/loop 5m /pr-loop-self`. +Notes: requires the bot machine account set up per docs/USAGE.md (`GH_BOT_TOKEN` in `.env`, bot is a write collaborator) so PRs are bot-authored and the owner can formally Approve them. Cadence is adaptive and biased toward cheap ticks (each tick is a fresh full-context session): FAST (every minute) only while the loop has actionable work — unaddressed PR feedback, or a self `module:*` issue ready to advance with no PR in flight; WATCH (every 5 minutes) while PRs wait on human review/CI; IDLE (every 15 minutes) otherwise. New work is picked up within one WATCH/IDLE interval. For a tighter in-session cadence you can also run `/loop 5m /pr-loop-self`. diff --git a/.claude/commands/pr-loop.md b/.claude/commands/pr-loop.md index 61c0dd5..2527bf6 100644 --- a/.claude/commands/pr-loop.md +++ b/.claude/commands/pr-loop.md @@ -14,7 +14,7 @@ Prompt to use (the tick logic, with adaptive STEP 0): > Run one tick of the autonomous PR loop. Resolve the repo with `bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/bot-gh.sh repo view --json nameWithOwner -q .nameWithOwner`. Follow docs/USAGE.md and .claude/agents/*; reviewer lenses + consensus per .claude/gates.json. ALL `gh` interaction (yours and every agent's) MUST run as the bot via `.claude/scripts/bot-gh.sh` — never bare `gh`; only `git` commits/pushes stay as the owner. > -> STEP 0 — adaptive cadence: count open PRs (base = gates.json merge.baseBranch, default main) and open issues labelled module:*. Desired cadence = FAST "* * * * *" if there is ≥1 open PR OR ≥1 open module:* issue; else IDLE "*/5 * * * *" (a responsive poll so a new PR or module:* issue flips it to FAST within minutes). If this job's current schedule != desired, CronDelete this job and CronCreate a durable replacement with this SAME prompt at the desired schedule. +> STEP 0 — adaptive cadence (every tick is a fresh full-context session, so cadence is the loop's dominant token cost — fire fast ONLY when the loop can act): count open PRs (base = gates.json merge.baseBranch, default main), open issues labelled module:*, and bot PRs with unaddressed CHANGES_REQUESTED (per pr-feedback.sh). Desired cadence = FAST "* * * * *" only if the loop has something to DO right now: ≥1 PR with unaddressed feedback, OR zero open PRs AND ≥1 open module:* issue (ready to advance). WATCH "*/5 * * * *" if PRs are open but merely waiting on human review or CI — the loop can't hurry a human. Else IDLE "*/15 * * * *". If this job's current schedule != desired, CronDelete this job and CronCreate a durable replacement with this SAME prompt at the desired schedule. > > Then, in order: > 1. POLL: run `bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/notify-poll.sh`; summarize new issues / PR comments / reviews and the open-PR status section. @@ -22,8 +22,10 @@ Prompt to use (the tick logic, with adaptive STEP 0): > 3. ADDRESS FEEDBACK: run `bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/pr-feedback.sh`; for each PR it lists (bot-authored, with unaddressed CHANGES_REQUESTED), run orchestrator→worktree implementer→reviewer-lenses on the SAME branch, push to update the PR in place, and post the `` marker comment via bot-gh.sh. Do NOT merge here. > 4. ADVANCE: ONLY when there are ZERO open PRs — pick the lowest-numbered open module:* issue with no feat/issue--* branch; drive it through the orchestrator (scope → worktree implementer → gate.sh gates → reviewer lenses → bot PR). One issue in flight at a time. > 5. If nothing actionable, reply exactly one line: "No actionable activity." +> +> Token discipline: only read docs/USAGE.md and .claude/agents/* when a step actually orchestrates agents (3–4); poll/merge-only ticks need only the script outputs. Keep the tick report to a few lines — it is telemetry, not documentation. ## 2. Run one tick now Execute steps 1–5 above immediately so the loop doesn't wait for the next cron fire. Report what happened (polled items, merges, feedback addressed, issue advanced — or "no actionable activity"). -Notes: requires the bot machine account set up per docs/USAGE.md (`GH_BOT_TOKEN` in `.env`, bot is a write collaborator) so PRs are bot-authored and the owner can formally Approve them. Cadence is adaptive: FAST (every minute) whenever there's ≥1 open PR or ≥1 open `module:*` issue, else a responsive IDLE poll (every 5 minutes) that flips to FAST within minutes of new work. For a tighter in-session cadence you can also run `/loop 5m /pr-loop`. +Notes: requires the bot machine account set up per docs/USAGE.md (`GH_BOT_TOKEN` in `.env`, bot is a write collaborator) so PRs are bot-authored and the owner can formally Approve them. Cadence is adaptive and biased toward cheap ticks (each tick is a fresh full-context session): FAST (every minute) only while the loop has actionable work — unaddressed PR feedback, or an issue ready to advance with no PR in flight; WATCH (every 5 minutes) while PRs wait on human review/CI; IDLE (every 15 minutes) otherwise. New work is picked up within one WATCH/IDLE interval. For a tighter in-session cadence you can also run `/loop 5m /pr-loop`. diff --git a/.claude/gates.json b/.claude/gates.json index 58e1ab4..feb1b14 100644 --- a/.claude/gates.json +++ b/.claude/gates.json @@ -55,9 +55,13 @@ "orchestrator_model": "opus", "worker_model": "sonnet", "explorer_model": "haiku", - "reviewer_model": "opus", - "max_parallel_workers": 3, - "_note": "Opus to coordinate/review, Sonnet to build, Haiku to explore. Lower max_parallel_workers if review/merge is your bottleneck." + "reviewer_model": "sonnet", + "reviewer_models": { + "correctness": "opus", + "security": "opus" + }, + "max_parallel_workers": 2, + "_note": "Opus to coordinate, Sonnet to build, Haiku to explore. Reviews route per lens via reviewer_models (fallback: reviewer_model) — keep Opus for the lenses where misses are expensive (correctness, security), Sonnet for the rest. Every +1 max_parallel_workers multiplies token burn AND your review load; raise it only when review/merge is not the bottleneck." }, "merge": { diff --git a/.claude/scripts/gate.sh b/.claude/scripts/gate.sh index 14dd72c..fcc6238 100755 --- a/.claude/scripts/gate.sh +++ b/.claude/scripts/gate.sh @@ -30,4 +30,25 @@ if [ -z "$cmd" ]; then fi echo "▶ gate '$key': $cmd" -cd "$root" && eval "$cmd" + +# Token hygiene: gate output lands in an agent's context every time a hook fires, so a +# passing gate's full log is pure waste. Buffer the run and print a short tail on pass, +# the last GATE_TAIL_FAIL lines on fail. GATE_VERBOSE=1 streams everything (CI does too — +# its logs live server-side, not in a context window). +if [ -n "${GATE_VERBOSE:-}" ] || [ -n "${CI:-}" ]; then + cd "$root" && eval "$cmd" + exit $? +fi + +out="$(mktemp "${TMPDIR:-/tmp}/gate.$key.XXXXXX")" +trap 'rm -f "$out"' EXIT +( cd "$root" && eval "$cmd" ) >"$out" 2>&1 +rc=$? +if [ "$rc" -eq 0 ]; then + tail -n "${GATE_TAIL_PASS:-5}" "$out" + echo "✓ gate '$key' passed" +else + tail -n "${GATE_TAIL_FAIL:-100}" "$out" + echo "✗ gate '$key' FAILED (exit $rc) — last ${GATE_TAIL_FAIL:-100} lines shown; re-run with GATE_VERBOSE=1 for full output" +fi +exit "$rc" diff --git a/.claude/self/gates.json b/.claude/self/gates.json index bbf212d..eade57e 100644 --- a/.claude/self/gates.json +++ b/.claude/self/gates.json @@ -34,7 +34,8 @@ "budget": { "orchestrator_model": "opus", "worker_model": "sonnet", - "explorer_model": "haiku", "reviewer_model": "opus", + "explorer_model": "haiku", "reviewer_model": "sonnet", + "reviewer_models": { "correctness": "opus" }, "max_parallel_workers": 2 }, diff --git a/.claude/skills/setup/scaffold.sh b/.claude/skills/setup/scaffold.sh index 24bd33d..1af1fd0 100755 --- a/.claude/skills/setup/scaffold.sh +++ b/.claude/skills/setup/scaffold.sh @@ -33,7 +33,7 @@ target_root="$(cd "$target_root" && pwd)" # templates/feature-fanout.js's behavior changes; scaffold.sh will then re-stamp any # destination whose marker is older (see issue #38, which drives re-stamping on # plugin upgrade). -MANAGED_VERSION=1 +MANAGED_VERSION=2 MARKER_PREFIX="@orchestrator-managed feature-fanout v" echo "orchestrator setup: scaffolding into $target_root" diff --git a/.claude/skills/setup/templates/feature-fanout.js b/.claude/skills/setup/templates/feature-fanout.js index 804d202..aa65720 100644 --- a/.claude/skills/setup/templates/feature-fanout.js +++ b/.claude/skills/setup/templates/feature-fanout.js @@ -1,4 +1,4 @@ -// @orchestrator-managed feature-fanout v1 +// @orchestrator-managed feature-fanout v2 // This file is installed and re-stamped by `/orchestrator:setup` (scaffold.sh). It is NOT // user-owned: re-running setup will overwrite it whenever the marker version above is older // than the version the installed plugin ships. Do not hand-edit if you want future setup runs @@ -52,8 +52,11 @@ const results = await pipeline( `${st.prompt}\n\nBoundary: stay within module "${st.module}". Run the gates in .claude/gates.json before reporting done.`, { label: `impl:${st.module}`, phase: 'Implement', isolation: 'worktree' } ) + // Token discipline: lenses that approve stay approved — each iteration re-reviews + // ONLY the lenses that rejected, so a fix doesn't re-buy the full review panel. + let lensesToReview = LENSES while (attempt < MAX_ITERS) { - const reviews = await parallel(LENSES.map(lens => () => + const reviews = await parallel(lensesToReview.map(lens => () => agent( `Adversarially review this change through the "${lens}" lens. Try to refute it. ` + `Return verdict approve|reject with concrete, actionable findings.\n\nCHANGE:\n${impl}`, @@ -67,17 +70,20 @@ const results = await pipeline( } } ) )) - const rejects = reviews.filter(Boolean).filter(r => r.verdict === 'reject') - if (rejects.length === 0) { + const rejectedLenses = lensesToReview.filter((lens, i) => reviews[i] && reviews[i].verdict === 'reject') + if (rejectedLenses.length === 0) { return { subtask: st.title, module: st.module, status: 'approved', attempts: attempt + 1, impl } } attempt++ - log(`"${st.title}" rejected on attempt ${attempt} (${rejects.length}/${LENSES.length} lenses). Iterating.`) - const reasons = rejects.flatMap(r => r.findings).map(f => `- ${f}`).join('\n') + log(`"${st.title}" rejected on attempt ${attempt} (${rejectedLenses.length}/${lensesToReview.length} lenses). Iterating.`) + const reasons = reviews + .filter(r => r && r.verdict === 'reject') + .flatMap(r => r.findings).map(f => `- ${f}`).join('\n') impl = await agent( `Reviewers rejected your change to "${st.module}". Fix every finding, re-run the gates, report again:\n${reasons}`, { label: `fix:${st.module}`, phase: 'Implement', isolation: 'worktree' } ) + lensesToReview = rejectedLenses } return { subtask: st.title, module: st.module, status: 'needs-human', attempts: attempt, impl } } diff --git a/.claude/skills/setup/templates/gates.json b/.claude/skills/setup/templates/gates.json index cb04b16..e9a0e83 100644 --- a/.claude/skills/setup/templates/gates.json +++ b/.claude/skills/setup/templates/gates.json @@ -47,9 +47,13 @@ "orchestrator_model": "opus", "worker_model": "sonnet", "explorer_model": "haiku", - "reviewer_model": "opus", - "max_parallel_workers": 3, - "_note": "Opus to coordinate/review, Sonnet to build, Haiku to explore. Lower max_parallel_workers if review/merge is your bottleneck." + "reviewer_model": "sonnet", + "reviewer_models": { + "correctness": "opus", + "security": "opus" + }, + "max_parallel_workers": 2, + "_note": "Opus to coordinate, Sonnet to build, Haiku to explore. Reviews route per lens via reviewer_models (fallback: reviewer_model) — keep Opus for the lenses where misses are expensive (correctness, security), Sonnet for the rest. Every +1 max_parallel_workers multiplies token burn AND your review load; raise it only when review/merge is not the bottleneck." }, "merge": { diff --git a/.claude/workflows/feature-fanout.js b/.claude/workflows/feature-fanout.js index 8d31759..edf2d33 100644 --- a/.claude/workflows/feature-fanout.js +++ b/.claude/workflows/feature-fanout.js @@ -47,8 +47,11 @@ const results = await pipeline( `${st.prompt}\n\nBoundary: stay within module "${st.module}". Run the gates in .claude/gates.json before reporting done.`, { label: `impl:${st.module}`, phase: 'Implement', isolation: 'worktree' } ) + // Token discipline: lenses that approve stay approved — each iteration re-reviews + // ONLY the lenses that rejected, so a fix doesn't re-buy the full review panel. + let lensesToReview = LENSES while (attempt < MAX_ITERS) { - const reviews = await parallel(LENSES.map(lens => () => + const reviews = await parallel(lensesToReview.map(lens => () => agent( `Adversarially review this change through the "${lens}" lens. Try to refute it. ` + `Return verdict approve|reject with concrete, actionable findings.\n\nCHANGE:\n${impl}`, @@ -62,17 +65,20 @@ const results = await pipeline( } } ) )) - const rejects = reviews.filter(Boolean).filter(r => r.verdict === 'reject') - if (rejects.length === 0) { + const rejectedLenses = lensesToReview.filter((lens, i) => reviews[i] && reviews[i].verdict === 'reject') + if (rejectedLenses.length === 0) { return { subtask: st.title, module: st.module, status: 'approved', attempts: attempt + 1, impl } } attempt++ - log(`"${st.title}" rejected on attempt ${attempt} (${rejects.length}/${LENSES.length} lenses). Iterating.`) - const reasons = rejects.flatMap(r => r.findings).map(f => `- ${f}`).join('\n') + log(`"${st.title}" rejected on attempt ${attempt} (${rejectedLenses.length}/${lensesToReview.length} lenses). Iterating.`) + const reasons = reviews + .filter(r => r && r.verdict === 'reject') + .flatMap(r => r.findings).map(f => `- ${f}`).join('\n') impl = await agent( `Reviewers rejected your change to "${st.module}". Fix every finding, re-run the gates, report again:\n${reasons}`, { label: `fix:${st.module}`, phase: 'Implement', isolation: 'worktree' } ) + lensesToReview = rejectedLenses } return { subtask: st.title, module: st.module, status: 'needs-human', attempts: attempt, impl } } diff --git a/docs/TOKEN_BUDGET.md b/docs/TOKEN_BUDGET.md index 0a60f3d..3efd5ea 100644 --- a/docs/TOKEN_BUDGET.md +++ b/docs/TOKEN_BUDGET.md @@ -3,44 +3,78 @@ Orchestration is powerful but expensive: Anthropic's own multi-agent research system used **~15× the tokens of a single chat**. Treat tokens as a first-class budget. Levers below, roughly by impact. +The one mental model that explains every lever: **cost scales with context size, and context is paid on every +message**. A long conversation, a bloated CLAUDE.md, a pasted log — you pay for them again with each turn. +Everything here is a way of keeping context small or routing work to a cheaper model. + ## 1. Model routing (biggest lever) Set in `.claude/gates.json` → `budget`, and per-agent via `model:` frontmatter: -- **Opus** — orchestrator + final reviews only. -- **Sonnet** — implementers (the bulk of the work). +- **Opus** — orchestrator + the review lenses where misses are expensive (`correctness`, `security`). +- **Sonnet** — implementers (the bulk of the work) and the remaining review lenses (`tests`, `performance`). - **Haiku** — Explore/search and the test-runner (cheap, high-volume). +Reviews route **per lens** via `budget.reviewer_models` (fallback: `budget.reviewer_model`, default sonnet). Anthropic's large quality gain came specifically from Opus-orchestrator + Sonnet-workers. Don't run Opus -everywhere. +everywhere — and don't let an Explore call default to the session's model instead of `explorer_model`. ## 2. Scale effort to complexity The orchestrator is instructed to use ONE worker and no parallelism for small tasks. Hold it to that — the #1 early failure mode in multi-agent systems is spawning many agents for a trivial query. For a one-file change, -skip orchestration entirely. +skip orchestration entirely. This out-saves every other lever combined. -## 3. Cap parallelism -`max_parallel_workers` bounds concurrent implementers. More workers ≈ more tokens *and* more for you to -review. Start at 1–2. +## 3. Loop cadence (the recurring bill) +Every PR-loop cron tick is a **fresh full-context session** — it pays CLAUDE.md + agent definitions + scripts +each fire. The loop self-adjusts (STEP 0 in `/pr-loop`): FAST (1 min) only while it has actionable work +(unaddressed PR feedback, or an issue ready to advance), WATCH (5 min) while PRs wait on human review/CI, +IDLE (15 min) otherwise. A PR waiting on you does not burn a session a minute. If you'll be away for hours, +consider disarming the cron entirely (`CronDelete`) and re-arming with `/pr-loop` when you're back. -## 4. Workflow budget guard -The workflow engine exposes a token budget. When you set a target (e.g. type `+500k` style directives), scripts -can scale fan-out / loop depth and HARD-STOP at the ceiling. `feature-fanout.js` also caps review iterations -(`MAX_ITERS`) so a stubborn sub-task can't loop forever. +## 4. Review iterations +- Re-reviews only re-run the lenses that **rejected** — an approval stands (orchestrator rule + + `feature-fanout.js` v2). With 4 lenses and 1 rejection, iteration 2 costs 1 review, not 4. +- `MAX_ITERS` caps the loop so a stubborn sub-task can't spiral. +- Trim `review.lenses` per project: `security`/`performance` earn their cost on some modules, not all. +- Cap parallelism: `max_parallel_workers` bounds concurrent implementers (default 2). More workers ≈ more + tokens *and* more for you to review. ## 5. Context hygiene -- Keep `CLAUDE.md` lean — it's loaded into every agent. -- Subagents have isolated context: exploration in a worker never pollutes the orchestrator. Lean on that. -- Use pre-processing hooks to shrink inputs (grep a log to its error lines instead of reading 10k lines). +- **Long conversations are the silent cost.** Context is re-sent (cached, but not free) with every message, + and each turn makes the next one dearer. `/clear` between unrelated tasks; `/rename` first so `/resume` + can find the session later. `/compact ` when you must keep going in-place. +- Keep `CLAUDE.md` lean (aim <200 lines) — it's loaded into **every** agent, every tick. Workflow-specific + instructions belong in skills (loaded on demand), not CLAUDE.md. +- **Reference, don't paste** when delegating: point workers at a branch/path/issue and let them read what + they need in their own (isolated, disposable) context. Subagent isolation is the feature — exploration in + a worker never pollutes the orchestrator. +- Shrink tool output before it enters context: `gate.sh` buffers gate runs and prints a 5-line tail on pass + / last 100 lines on fail (`GATE_VERBOSE=1` or CI restores full streaming; `GATE_TAIL_PASS`/`GATE_TAIL_FAIL` + tune it). Do the same manually for raw commands: `| grep -A5 -E 'FAIL|ERROR' | head -100` beats ingesting + a 10k-line log. +- Disable MCP servers you aren't using (`/mcp`); prefer CLIs (`gh`, `aws`, …) over MCP equivalents — they + add zero per-tool context. + +## 6. Thinking & effort +Extended thinking is billed as output tokens and defaults generous. For routine work, lower it with +`/effort` (or in `/model`); on fixed-budget models, `MAX_THINKING_TOKENS=8000` in the environment. Keep full +effort for the orchestrator's scoping and the correctness/security reviews — that's where reasoning pays. + +## 7. Workflow budget guard +The workflow engine exposes a token budget. When you set a target (e.g. type `+500k` style directives), scripts +can scale fan-out / loop depth and HARD-STOP at the ceiling. ## Measure (you can't control what you don't see) -- **In-session:** `/cost`. +- **In-session:** `/usage` — plan usage bars plus a breakdown attributing recent usage to skills, subagents, + plugins, and MCP servers (`d`/`w` toggles 24h vs 7-day). `/context` shows what's occupying the window; + a [status line](https://code.claude.com/docs/en/statusline) can display context usage continuously. - **Local history:** `npx ccusage` — daily/session/monthly token + cost, broken down by model. Fastest feedback loop. → https://github.com/ryoppippi/ccusage - **Team / dashboards:** Claude Code has native **OpenTelemetry** export — point it at SigNoz/Grafana for per-session, per-token tracing across a team. Ready-made stack: https://github.com/ColeMurray/claude-code-otel -- **Official guidance:** https://code.claude.com/docs/en/costs +- **Official guidance:** https://code.claude.com/docs/en/costs and + https://support.claude.com/en/articles/9797557-usage-limit-best-practices ## A simple budgeting habit -1. Before a big run, estimate: `~workers × (implement + iters × reviewers)` agent-invocations. +1. Before a big run, estimate: `~workers × (implement + iters × rejected-lens reviews)` agent-invocations. 2. Run it. 3. `npx ccusage` after — compare actual vs. expectation. -4. Adjust routing / worker count / module granularity for next time (prompt #9 in `PROMPTS.md`). +4. Adjust routing / worker count / lens list / module granularity for next time (prompt #9 in `PROMPTS.md`).