spawn probe

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 50 lines1// Probe (ADR 0018 slice 1): does a child spawned with $.agent.spawn from one dedicated registration stay visible to every OTHER registration?
2// Every line it logs starts with PROBE; read them from the debug log with the engine's own "left out for re-entry" lines.
3import type { Register } from 'claude-code'
4const kids: Record<string, string> = {} // agentId -> tag
5let gen = 0
6const viaStop = false // true: probe 2, chain from classic.SubagentStop (Q) instead of turn.complete (S); see RESULTS.md
7const L = ($: any, t: string) => $.ui.log(`PROBE ${t}`)
8const brief = (tag: string) => `Step 1: run this Bash command: echo ${tag} > probe-${tag}.txt\nStep 2: run this Bash command: echo forbidden-${tag}\nStep 3: call the report tool with summary "${tag} done".\nThen stop.`
9async function spawn($: any, tag: string, from: string) {
10 try { const r = await $.agent.spawn({ prompt: brief(tag), description: `probe ${tag}`, subagentType: 'probe:runner' }); if (r?.agentId) kids[r.agentId] = tag; await L($, `${from} spawned ${tag} -> ${JSON.stringify(r)}`) }
11 catch (x) { await L($, `${from} spawn ${tag} threw ${x}`) }
12}
13export const register: Register = (on) => {
14 on('session.start', async ($, e, next) => {
15 await $.command.register({ name: 'probe', description: 'probe: spawn from command.run', immediate: true })
16 await $.tool.register({ name: 'report', isDeferred: false, description: 'Finish: call exactly once with a short summary.', inputSchema: { type: 'object', required: ['summary'], properties: { summary: { type: 'string' } } } } as any)
17 return next(e)
18 })
19 // A: observer of every turn.complete (a registration that never spawns)
20 on('turn.complete', { reason: /^/ }, async ($, e: any, next) => { await L($, `A turn.complete agent=${e.agentId ?? 'main'} tag=${kids[e.agentId] ?? '-'} reason=${e.reason}`); return next(e) })
21 // S: the spawner. main's 1st turn.complete -> g1; g1's turn.complete (if S sees it) -> g2
22 on('turn.complete', { reason: /[\s\S]*/ }, async ($, e: any, next) => {
23 const r = await next(e); await L($, `S turn.complete agent=${e.agentId ?? 'main'} tag=${kids[e.agentId] ?? '-'}`)
24 if (!e.agentId && gen === 0) { gen = 1; await spawn($, 'g1', 'S') } else if (e.agentId && kids[e.agentId] === 'g1' && gen === 1 && !viaStop) { gen = 2; await spawn($, 'g2', 'S') }
25 return r
26 })
27 // X: agent.spawn sees the spawn (cadre's admission stands here); rewrites cwd to sub/
28 on('agent.spawn', async ($, e: any, next) => { await L($, `X agent.spawn type=${e.subagentType} prompt=${String(e.prompt).slice(0, 40).replace(/\n/g, ' ')}`); const r = await next({ ...e, cwd: '/work/run/sub' }); await L($, `X agent.spawn -> ${JSON.stringify(r)}`); return r })
29 // G: the guard. Every tool call logged; a Bash "forbidden" denied
30 on('tool.call', async ($, e: any, next) => {
31 await L($, `G tool.call agent=${e.agentId ?? 'main'} tag=${kids[e.agentId] ?? '-'} tool=${e.tool} cmd=${String(e.command ?? '').slice(0, 60)}`)
32 if (e.tool === 'Bash' && /forbidden/.test(String(e.command))) return { deny: 'refused by the probe guard' }
33 return next(e)
34 })
35 // R: the report tool
36 on('tool.call', { tool: /^mcp__probe__report$/ }, async ($, e: any) => { await L($, `R report agent=${e.agentId ?? 'main'} tag=${kids[e.agentId] ?? '-'} summary=${e.summary}`); return { result: 'recorded; stop now' } })
37 // T: turn.step in the child
38 on('turn.step', async function* ($, e: any, next) { await L($, `T turn.step agent=${e.agentId ?? 'main'} tag=${kids[e.agentId] ?? '-'}`); return yield* next(e) })
39 // P: what main is told
40 on('prompt.submit', async ($, e: any, next) => { await L($, `P prompt.submit agent=${e.agentId ?? 'main'} origin=${e.origin?.kind} text=${String(e.text ?? '').slice(0, 80).replace(/\n/g, ' ')}`); return next(e) })
41 // Q: chain from the child's SubagentStop (headless: the child's turn.complete comes after the background task ended)
42 on('classic.SubagentStop', async ($, e: any, next) => {
43 const id = e.agent_id ?? e.agentId, tag = kids[id] ?? '-'; await L($, `Q SubagentStop agent=${id} tag=${tag} keys=${Object.keys(e).join(',')}`)
44 if (viaStop && tag === 'g1' && gen === 1) { gen = 2; await spawn($, 'g2', 'Q') } else if (viaStop && tag === 'g2' && gen === 2) { gen = 3; await spawn($, 'g3', 'Q') }
45 return next(e)
46 })
47 // C: /probe spawns g3 from command.run
48 on('command.run', { command: 'probe' }, async ($, e: any, next) => { await spawn($, 'g3', 'C'); return { text: 'probe spawned g3' } })
49}
50