harness-only scripted model: answers every model request from fixtures

A Claude Code plugin that makes Claude plan, test, review and merge like a disciplined team, enforced in code, not prompts.
bd) on your machine. The store is in .cadre/store (not committed); to look at it by hand: BEADS_DIR=.cadre/store bd list. Older projects keep .beads; /cadre doctor offers to move it.[RUN] cadre · p3 auth-login · strict · contained ✓ · ctx 34% · cache 87%
✓a1 ✓a4 ✓a7 │ ▶a2 ✓a3 │ ⏸a6 ◌a5 4/7 done · ▶1 ⏸1 ◌1
[1] Epic [7] Plan [8] Doctor · p3 · epic/p3-auth-login · 4/7 done
┌────────────────┐ ┏━━━━━━━━━━━━━━━━┓ ┌────────────────┐
│a1 feat [OK]✓│ ┃a2 feat [RUN]▶┃ │a6 feat [WAIT]⏸│
│Add login │━━┳━▶┃Session refresh ┃──┬─▶│Remember me │
│●●●●●● merged │ ┃ ┃●●◉○○○ implement┃ ┃ │○○○○○○ waits a2 │
└────────────────┘ ┃ ┗━━━━━━━━━━━━━━━━┛ ┃ └────────────────┘
claude plugin marketplace add it-dainb/cadre
claude plugin install cadre@cadre
Then, inside Claude Code:
/cadre setup: once per machine. Claude checks your tools and fixes what it can, in the open, and asks your default mode and limits; you confirm the settings in a dialog./cadre <goal in plain words>, e.g. /cadre fix the off-by-one in src/page.ts. In a repo cadre hasn't seen, one dialog asks to set it up first: Claude prepares the task store, your build and test commands and a changelog in the open, you confirm the safety settings, and then the plan comes up with its mission for one approval. Claude picks a lane and says why:/cadre deep <goal> talks it through instead: one question per round at the weakest point, with the scores shown, until it is clear, 8 rounds pass, or you say "enough". Settings → Planning. The lane shows in the approval dialog, with an option to change it; type something instead to ask Claude for a change. Breaking, security, public contract changes and plans of 3+ tasks are always full./cadre <goal>): Claude carries the plan until it lands on dev and asks you only at the critical points: a change to the agreed spec, a question only you can answer, an integration that fails twice, a protected branch moved outside cadre, and the release./cadre yolo <goal>): after the approval nothing is asked. Claude decides spec changes and blocked questions itself and each decision is recorded (Activity, Doctor) for you to review. It stops, never releases, when the plan lands on dev. To make yolo your default, set Drive → mode to yolo in Settings (9); a repo's committed config can only force auto. Both stop by themselves when an agent moves a protected branch, the sandbox settings change, or a bound is hit (100 turns, $20, 240 minutes, or 3 turns without progress; Settings → Drive). The band shows auto · turn 12/100 · $1.20 · 0: stop; 0 stops and resumes./cadre alone opens the pane. Those forms (/cadre, /cadre <goal>, /cadre yolo <goal>, /cadre deep <goal>, setup, init, doctor, continue) are all you type; every decision is answered in a dialog. It opens once by itself; after that 1 (on the band or in the pane) opens it again, and the pane lists everything that needs you.
plan → your approval → tester (test goes red) → worker (goes green)
→ independent reviewer → merge into the epic branch
→ epic lands on dev → you release
| Who | Does |
|---|---|
| You | Approve plans, resolve flags, cut releases. Every one is a dialog answer. |
| Main Claude | Plans and manages. It never edits code and has no shell. |
| Worker agents | Tester writes the test, worker writes the code, reviewer checks it. Each works in its own worktree. |
| cadre itself | Owns git (workers cannot commit or push), runs the checks, enforces every step, never pushes. |
More: the loop in detail.
| Type this | What it does |
|---|---|
/cadre setup | One-time machine setup. Details |
/cadre | One line: where things stand and what to type next; opens the pane. |
/cadre init | Re-run the repo setup on purpose (a first /cadre <goal> does it for you). |
/cadre doctor | Read-only health check. Safe any time. |
/cadre <goal> · /cadre yolo <goal> | Start work; after approval it runs to dev, asking at the critical points (auto) or not at all (yolo). |
/cadre deep <goal> | Talk the goal through first (one question per round, scores shown), then the plan comes up as usual. |
/cadre continue | Pick up a plan an earlier session left (also 0 on the band). |
/cadre open · /cadre status | Open the board pane · print the board as text. |
| Settings | Press 9 in the pane: every setting, where its value comes from, and buttons to change it. |
Every decision is a dialog (1 opens it). The typed fallbacks, config keys and the extension protocol are in docs/commands.md.
0 on the band (0: stop); 0: resume starts it again with a fresh budget./clear or compaction: nothing to do. Running agents keep working and the run goes on.claude --continue later: the new session shows Unfinished plan … · /cadre continue to pick it up and 0: continue on the band. Nothing restarts by itself. /cadre continue (or 0) re-arms the run in its mode (auto or yolo) and respawns each task that was in flight. Each new agent starts from a checkpoint commit of the old one's worktree, with the diff and the last failing check, so nothing is redone. No waiting for old claims to expire./cadre continue elsewhere says so and takes nothing.usage limit until HH:MM UTC on the band) and goes on by itself then; nothing is charged to the task. 0: resume goes on at once.dev (or main) checked out when work lands, cadre leaves your branch, index and files alone and asks in one dialog: Land brings it into your checkout, keeping your commits and your uncommitted edits (an edit to the same file refuses, and the dialog says so). Switch to another branch and it lands by itself. A release waits until it is in.limits.silentRuns (runs without a report before a task blocks) and limits.startFails (failed starts before the run stops) are in Settings.gitbd ≥ 1.3.0. /cadre setup installs it.bwrap (bubblewrap) and socat for sandboxed checks. macOS: setup tells you what is needed./cadre doctor first. It lists every failing check and how to fix it. The full report is in the pane's Doctor tab./cadre <goal> sets it up and plans the goal.~/.cache/cadre/fix/. Read it, then run the line Claude shows you, e.g. sudo sh ~/.cache/cadre/fix/bwrap-apparmor.sh.Read more: DESIGN · UI design · decisions (ADRs) · UI dev loop · test harness · module contracts
You need the Claude Code 2.1.295 binary (not whatever claude is on PATH).
CC=<path to claude 2.1.295>
node scripts/budget.mjs # size budget
$CC plugin validate --strict . && $CC plugin test .
$CC --plugin-dir . # try it in the repo you want to manage
node --experimental-transform-types scripts/ui-dev.mjs # the pane in your terminal, hot reload
Live runs go through Docker: harness/run.sh (see harness/README.md).
Version 0.14.0. Pre-1.0, pinned to Claude Code 2.1.295 (agent exits tested up to 2.1.299). License: not chosen yet.
hooks/register.ts 118 lines1// Harness-only scripted model (S0): answers every model request at turn.step (TM-04) from the fixtures below, never calling next, so no request
2// reaches the API (PROXY=1's effort.jsonl stays empty). Each answer is a function of the loop's own transcript ($.session.messages), never of a
3// counter: a resumed or nudged agent gets the answer its transcript calls for. Loaded with a second --plugin-dir AFTER cadre's, so it sits inner to
4// cadre in the user tier (load order): cadre's turn.step sees each scripted response come up from beneath (its cost ledger records `model: scripted`).
5// Every line it logs starts with SCRIPTED (debug log). `/s0 <end|hb|wedge>` is the forced-exit / TaskStop probe (RESULTS.md (c), (d)).
6// The script is chosen per cell by the repo's `.git/scripted.json` ({"script": …}, written by a harness/fixtures/scripted overlay), default `lite`:
7// lite: main plans one lite task; full: a full-lane task (tester, worker, reviewer); loop: lite, the worker calls the same tool forever (the repeat-call breaker, then forced exit);
8// limit: lite, the first worker's first step goes to the proxy, which answers a usage limit (stub-proxy.mjs); slow: lite, the worker waits 45 s first.
9import type { Register } from 'claude-code'
10
11type Row = { role: 'user' | 'assistant'; text: string; toolUses?: { tool: string; input?: any }[]; toolResults?: { text?: string; isError?: boolean }[] }
12type Act = { text?: string; tool?: string; input?: unknown }
13const U = { input_tokens: 7, output_tokens: 3, cache_read_input_tokens: 0, cache_creation_input_tokens: 0, model: 'scripted' } // tokensOf = 7 + 5*3 = 22 per step
14const L = ($: any, t: string) => $.ui.log(`SCRIPTED ${t}`)
15const id = () => `toolu_s0${crypto.randomUUID().replace(/-/g, '').slice(0, 20)}`
16const isText = (r: Row) => r.role === 'user' && !r.toolResults?.length && !!r.text
17const since = (rows: Row[]) => { let i = rows.length - 1; while (i > 0 && !isText(rows[i])) i--; return rows.slice(i) } // from the last prompt/brief/nudge on
18const done = (rows: Row[]) => rows.filter(r => r.role === 'assistant').length
19const END = 'Done. Stopping here.'
20let script = 'lite', injected = false
21const cwds: Record<string, string> = {} // agentId -> the folder cadre spawned it in (agent.spawn): the tester writes there by its full path
22
23// ---- fixtures: titleCase(s) on the `single` repo, lite lane ----
24const GOAL = /Goal from the user/
25function main(rows: Row[]): Act {
26 const g = rows.map(r => isText(r) && GOAL.test(r.text)).lastIndexOf(true)
27 if (g < 0) return { text: 'OK.' }
28 const after = rows.slice(g + 1), calls = after.flatMap(r => (r.role === 'assistant' ? r.toolUses ?? [] : []))
29 const has = (f: (c: { tool: string; input?: any }) => boolean) => calls.some(f)
30 const kind = (k: string) => has(c => c.tool === 'TaskCreate' && c.input?.metadata?.kind === k)
31 const ai = after.findIndex(r => r.role === 'assistant' && r.toolUses?.some(c => c.tool === 'Agent'))
32 const answered = ai >= 0 && after.slice(ai + 1).some(r => isText(r) || r.toolResults?.some(t => t.text && !/launched/i.test(t.text)))
33 if (after.filter(r => r.role === 'assistant').length > 8) return { text: 'Stopping: approve the plan in the dialog.' }
34 if (ai < 0) return { tool: 'Agent', input: { description: 'Explore the library', subagent_type: 'Explore', prompt: 'What in the project does adding titleCase(s) touch: where string helpers live, how they are exported, where the tests are?' } }
35 if (!answered) return { text: 'Waiting for the explorer.' }
36 if (!kind('clarity')) return { tool: 'TaskCreate', input: { subject: 'clarity', description: 'Clarity of the goal after exploring.', metadata: { kind: 'clarity', outcome: 1, scope: 1, acceptance: 1, constraints: 1 } } }
37 const mission = /There is no mission yet/.test(rows[g].text) ? { mission: 'Goal: add titleCase(s) to the library\nConstraints: none\nNon-goals: none' } : {}
38 if (script === 'full') {
39 if (!kind('plan')) return { tool: 'TaskCreate', input: { subject: 'Add titleCase(s) to the library', description: 'A new public function titleCase(s) with node:test tests.', metadata: { kind: 'plan', lane: 'full', reason: 'a new public function', ...mission } } }
40 if (!has(c => c.tool === 'TaskCreate' && !c.input?.metadata?.kind)) return { tool: 'TaskCreate', input: { subject: 'titleCase(s) with node:test tests', description: 'Create src/titleCase.mjs exporting titleCase: uppercase the first letter of each whitespace-separated word, leave the rest unchanged.',
41 metadata: { type: 'feature', package: '.', paths: ['src/titleCase.mjs', 'test/titleCase.test.mjs'], scenarios: [`S1: Given 'hello big world' When titleCase is called Then it returns 'Hello Big World'`],
42 changelog: { category: 'Added', entry: 'titleCase(s) capitalises the first letter of each word' }, interface: { symbols: [{ name: 'titleCase', kind: 'function', params: ['s: string'], returns: 'string', status: 'new' }] } } } }
43 return { text: 'The plan is drafted: approve it in the dialog (key 1 opens it).' }
44 }
45 if (!kind('plan')) {
46 return { tool: 'TaskCreate', input: { subject: 'Add titleCase(s) with tests', description: 'Add titleCase(s) as a named export in src/titleCase.mjs, in the style of src/slug.mjs, with node:test tests in test/titleCase.test.mjs.',
47 metadata: { kind: 'plan', lane: 'lite', reason: 'one small function and its test', type: 'feature', changelog: { category: 'Added', entry: 'titleCase(s) capitalises the first letter of each word' }, ...mission } } }
48 }
49 return { text: 'The plan is drafted: approve it in the dialog (key 1 opens it).' }
50}
51const TEST = `import { test } from 'node:test'
52import assert from 'node:assert/strict'
53import { titleCase } from '../src/titleCase.mjs'
54test('S1: titleCase capitalises each word', () => assert.equal(titleCase('hello big world'), 'Hello Big World'))
55`
56// the test file only when there is none (full lane: the tester's is locked)
57const FILES = String.raw`mkdir -p src test && cat > src/titleCase.mjs <<'EOF'
58export const titleCase = s => s.replace(/(^|\s)(\S)/g, (_, a, b) => a + b.toUpperCase())
59EOF
60test -f test/titleCase.test.mjs || cat > test/titleCase.test.mjs <<'EOF'
61${TEST}EOF`
62const taskOf = (brief: string) => brief.match(/TASK-([\w-]+\.\d+)/)?.[1] ?? brief.match(/\b([a-z][\w]*-[a-z0-9]+\.\d+)\b/)?.[1]
63const WAIT = { tool: 'Bash', input: { command: 'node -e "setTimeout(() => {}, 45000)"', description: 'Wait' } }
64const ROLES: Record<string, (k: number, brief: string, cwd: string) => Act> = {
65 worker: (k, b) => script === 'loop' ? { tool: 'mcp__cadre__run_check', input: { name: 'test', task: taskOf(b) } } : (script === 'slow' ? [WAIT] : []).concat([{ tool: 'Bash', input: { command: FILES, description: 'Write titleCase and its test' } }, { tool: 'mcp__cadre__run_check', input: { name: 'test', task: taskOf(b) } },
66 { tool: 'mcp__cadre__report', input: { schemaVersion: 1, role: 'worker', outcome: 'done', summary: 'Added titleCase(s) in src/titleCase.mjs with a node:test test; npm test passes.', risks: [] } }])[k] ?? { text: END },
67 tester: (k, _b, cwd) => [{ tool: 'Write', input: { file_path: `${cwd}/test/titleCase.test.mjs`, content: TEST } },
68 { tool: 'mcp__cadre__report', input: { schemaVersion: 1, role: 'tester', outcome: 'done', summary: 'One test per scenario in test/titleCase.test.mjs.', tests: [{ path: 'test/titleCase.test.mjs', name: 'S1: titleCase capitalises each word', scenario: 'S1' }], run: { command: ['node', '--test', 'test/titleCase.test.mjs'] } } }][k] ?? { text: END },
69 reviewer: (k, b) => [{ tool: 'mcp__cadre__run_check', input: { name: 'test', task: taskOf(b) } },
70 { tool: 'mcp__cadre__report', input: { schemaVersion: 1, role: 'reviewer', verdict: 'pass', summary: 'The commit adds titleCase and its test only; npm test passes.', findings: [], coverage: script === 'full' ? [{ scenario: 'S1', test: 'S1: titleCase capitalises each word', ok: true }] : [], contractStatus: script === 'full' ? [{ name: 'titleCase', ok: true }] : [] } }][k] ?? { text: END },
71 explorer: k => [{ tool: 'mcp__cadre__report', input: { schemaVersion: 1, role: 'explorer', answer: 'One string helper, slug, in src/slug.mjs as a named ESM export; tests in test/ use node:test; npm test runs node --test.', citations: [{ path: 'src/slug.mjs', line: 1 }, { path: 'package.json', line: 1 }] } }][k] ?? { text: END },
72}
73function agent(rows: Row[], cwd: string): [string, Act] {
74 const brief = rows.find(isText)?.text ?? '', probe = brief.match(/S0-PROBE tag=(\w+)/)?.[1]
75 const k = done(since(rows)) // auto mode: a role's text + end_turn is re-asked 3 times for SubagentHandback, then the run ends (RESULTS.md (c))
76 if (done(rows) > (script === 'loop' ? 40 : 15)) return ['cap', { text: END }] // loop: past the breaker (20 in a row)
77 if (probe === 'end') return [probe, { text: END }]
78 if (probe === 'hb') return [probe, /No such tool available: SubagentHandback/.test(JSON.stringify(rows.at(-1))) ? { text: END } : { text: END, tool: 'SubagentHandback', input: { message: END } }] // the tool exists in auto mode only
79 if (probe === 'wedge') return [probe, k === 0 ? { tool: 'Bash', input: { command: 'node -e "setTimeout(() => {}, 600000)"', description: 'Wait' } } : { text: END }]
80 const role = brief.match(/\b(Worker|Reviewer|Explorer|Tester|Planner) brief/)?.[1]?.toLowerCase() ?? 'unknown'
81 return [role, ROLES[role]?.(k, brief, cwd) ?? { text: END }]
82}
83
84async function* answer(e: any, a: Act): AsyncGenerator<any, any, any> {
85 let i = 0
86 if (a.text) yield { kind: 'text', index: i++, text: a.text }
87 if (a.tool) { yield { kind: 'tool', index: i, id: id(), name: a.tool }; yield { kind: 'input', index: i, json: JSON.stringify(a.input ?? {}) } }
88 const stopReason = a.tool ? 'tool_use' : 'end_turn'
89 yield { kind: 'stop', stopReason, usage: U }
90 return { turnId: e.turnId, index: e.index, answer: a.text ?? '', toolUses: a.tool ? [{ name: a.tool, input: a.input ?? {} }] : [], stopReason, usage: U }
91}
92
93export const register: Register = (on) => {
94 on('session.start', async ($: any, e, next) => {
95 try { script = JSON.parse(await $.fs.read('/work/run/.git/scripted.json')).script ?? script } catch {}
96 await L($, `script ${script}`); await $.command.register({ name: 's0', description: 'scripted probe: /s0 end|hb|wedge', immediate: true }); return next(e)
97 })
98 on('agent.spawn', async ($: any, e: any, next) => { const r = await next(e); if (r?.agentId) cwds[r.agentId] = e.cwd ?? '/work/run'; return r })
99 on('turn.step', async function* ($: any, e: any, next) {
100 const rows = ((await $.session.messages(e.agentId ? { agentId: e.agentId } : {})) ?? []) as Row[]
101 const [who, a] = e.agentId ? agent(rows, cwds[e.agentId] ?? '/work/run') : ['main', main(rows)]
102 if (script === 'limit' && who === 'worker' && !injected) { injected = true; await L($, `step agent=${e.agentId} who=worker index=${e.index} -> next (usage limit from the proxy)`); return yield* next(e) }
103 await L($, `step agent=${e.agentId ?? 'main'} who=${who} index=${e.index} msgs=${e.messageCount} effort=${e.effort ?? '-'} -> ${a.tool ? `${a.text ? 'text+' : ''}tool ${a.tool}` : 'text end_turn'}`)
104 return yield* answer(e, a)
105 })
106 on('turn.complete', async ($: any, e: any, next) => { await L($, `turn.complete agent=${e.agentId ?? 'main'} reason=${e.reason} usage=${e.usage?.model ?? '-'}`); return next(e) })
107 // (c)/(d) probe: spawned from this command frame, so this plugin's turn.step (another registration) still sees the children's steps (G6)
108 on('command.run', { command: 's0' }, async ($: any, e: any) => {
109 const tag = String(e.args ?? '').trim() || 'end'
110 const r = await $.agent.spawn({ prompt: `S0-PROBE tag=${tag}. Do the task you are given.`, description: `s0 ${tag}`, subagentType: 'general-purpose' })
111 await L($, `spawned ${tag} -> ${JSON.stringify(r)}`)
112 if (tag === 'wedge' && r?.agentId) $.clock.after(20_000, async () => { // a timer frame, as the backstop's: no hook is calling
113 try { await L($, `TaskStop ${r.agentId} -> ${JSON.stringify(await $.tool.call({ tool: 'TaskStop', task_id: r.agentId }))}`) } catch (x) { await L($, `TaskStop ${r.agentId} threw ${x}`) }
114 })
115 return { text: `s0 ${tag} spawned` }
116 })
117}
118