SLOPSHOPPER

probe

spawn probe

newguardcommandprompttoolagents
★ 1v0.0.1no licenseupdated 2026-10-13it-dainb/cadre/harness/probes/spawn
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · probe
› fix the failing auth test and add an audit log call ● probe: PROBE P prompt.submit agent=main origin=composer text=fix the failing auth test and add an audit log call ● probe: PROBE G tool.call agent=main tag=- tool=Read cmd= ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /probe ⎿ probe: probe spawned g3 ● probe: PROBE G tool.call agent=main tag=- tool=Grep cmd= ● probe: PROBE G tool.call agent=main tag=- tool=Edit cmd= ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

cadre

A Claude Code plugin that makes Claude plan, test, review and merge like a disciplined team, enforced in code, not prompts.

  • You approve in a dialog. Claude drafts a plan; nothing runs until you choose Approve in Claude Code's own question dialog.
  • Tests first. A separate tester writes a failing test, then a worker makes it pass.
  • Isolated and reviewed. Every task gets its own git worktree and an independent review before it merges.
  • A live board in your terminal. See every task, step and agent without leaving Claude Code.
  • Works offline. Tasks live in beads (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 │
 └────────────────┘  ┃  ┗━━━━━━━━━━━━━━━━┛  ┃  └────────────────┘

Quick start

claude plugin marketplace add it-dainb/cadre
claude plugin install cadre@cadre

Then, inside Claude Code:

  1. /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.
  2. Start work: /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:
  3. ask: a question or investigation. An explorer answers it; nothing changes.
  4. lite: one small, clear change. One task, drafted for you; choose Approve once and Claude runs it to the end: worker → review → merge (a bug a test can reproduce gets the failing test first).
  5. full: a feature, several tasks, or a public API change. The full plan with scenarios and contracts. Before it plans, Claude sends one explorer through the code and scores how clear the goal is (outcome, scope, acceptance, constraints). A vague goal gets at most 3 questions in one round, naming what the explorer found ("I found Hero, Navbar and Timeline in components/: which one?"); a clear one gets none. Under yolo it asks nothing and the dialog lists what it assumed. /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.
  1. After you approve, it runs by itself. Two modes:
  2. auto (/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.
  3. yolo (/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.

How it works

plan → your approval → tester (test goes red) → worker (goes green)
     → independent reviewer → merge into the epic branch
     → epic lands on dev → you release
WhoDoes
YouApprove plans, resolve flags, cut releases. Every one is a dialog answer.
Main ClaudePlans and manages. It never edits code and has no shell.
Worker agentsTester writes the test, worker writes the code, reviewer checks it. Each works in its own worktree.
cadre itselfOwns git (workers cannot commit or push), runs the checks, enforces every step, never pushes.

More: the loop in detail.

Commands

Type thisWhat it does
/cadre setupOne-time machine setup. Details
/cadreOne line: where things stand and what to type next; opens the pane.
/cadre initRe-run the repo setup on purpose (a first /cadre <goal> does it for you).
/cadre doctorRead-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 continuePick up a plan an earlier session left (also 0 on the band).
/cadre open · /cadre statusOpen the board pane · print the board as text.
SettingsPress 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.

Stopping and continuing

  • Stop the run: press 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.
  • Quit, crash, or 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.
  • What a quit can lose: only work that was not on disk yet (the agent's unwritten next step). Every file it had written is kept.
  • Two sessions on one repo: a session that is still running keeps its plan and its tasks; /cadre continue elsewhere says so and takes nothing.
  • Usage limit: the run stops with the time it resets (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.
  • Your checkout is never moved. If you have 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.
  • An agent can always end. Once an agent has handed in its work, cadre ends its turn itself (no tokens); one that repeats the same call 20 times is stopped and its task asks you. limits.silentRuns (runs without a report before a task blocks) and limits.startFails (failed starts before the run stops) are in Settings.

Requirements

  • Claude Code 2.1.295 (tested; newer usually works and only warns)
  • git
  • bd ≥ 1.3.0. /cadre setup installs it.
  • Linux: bwrap (bubblewrap) and socat for sandboxed checks. macOS: setup tells you what is needed.

Troubleshooting

  • Run /cadre doctor first. It lists every failing check and how to fix it. The full report is in the pane's Doctor tab.
  • "Ready when you are": cadre isn't set up in this repo yet; /cadre <goal> sets it up and plans the goal.
  • Ubuntu 24.04+ blocks bwrap (AppArmor). cadre never runs root commands for you. It writes a fix script to ~/.cache/cadre/fix/. Read it, then run the line Claude shows you, e.g. sudo sh ~/.cache/cadre/fix/bwrap-apparmor.sh.
  • Known limits: docs/limitations.md.

Philosophy

  • Enforced by code. Hooks deny what the rules forbid. A prompt can be ignored; a denial cannot.
  • Main plans, never edits. The agent that talks to you cannot touch your source.
  • Computed beats claimed. Tests pass because cadre ran them, not because an agent said so.
  • Local first. Your tasks and git history stay on your machine. cadre never pushes.
  • Calm UI, zero token cost. The board is drawn by the plugin. It adds nothing to Claude's context.

Read more: DESIGN · UI design · decisions (ADRs) · UI dev loop · test harness · module contracts

Development

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).

Status

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.

Source 1 files
hooks/register.ts 50 lines
1// 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