Starts breadcrumbs' manual-test testers the moment you press Start, without going through the main agent.

Lets breadcrumbs start a manual-test tester the moment you press Start tester, even while Claude is mid-turn.
A Claude Code mod can't answer tool calls from an agent it spawned itself: that agent skips every hook of the mod that spawned it. So breadcrumbs used to ask the main agent to spawn each tester, and you waited until Claude was free. This mod does the spawning instead. Breadcrumbs leaves a request under ~/.agents/notes/.launch/<session>/, this mod picks it up within a second, and breadcrumbs answers the tester's tools as usual. The tester's final report still goes to the main session.
It starts only breadcrumbs:tester, and nothing else. Breadcrumbs falls back to asking Claude if this mod isn't running.
/plugin install breadcrumbs-launcher@claude-modshooks/register.ts 80 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// Breadcrumbs writes spawn requests here and checks `ready` to know this mod runs; see LAUNCH_DIR in its hooks/tests.ts.
4const LAUNCH_DIR = '.agents/notes/.launch'
5const TESTER = 'breadcrumbs:tester'
6const POLL_MS = 1000
7const HEARTBEAT_MS = 30_000
8const STALE_MS = 7 * 86_400_000
9
10type $ = EngineInterface
11type Request = { subagentType?: unknown; description?: unknown; prompt?: unknown }
12
13let dir: string | null = null
14
15const root = async ($: $) => `${(await $.env.get('HOME')) ?? '~'}/${LAUNCH_DIR}`
16
17const stamp = async ($: $) => {
18 if (dir) await $.fs.write(`${dir}/ready`, String(await $.clock.now()))
19}
20
21// Folders of sessions that ended long ago, which nothing else removes.
22const tidy = async ($: $, now: number) => {
23 let sessions
24 try {
25 sessions = await $.fs.list(await root($))
26 } catch {
27 return
28 }
29 for (const s of sessions) {
30 const path = `${await root($)}/${s.name}`
31 if (s.kind !== 'dir' || path === dir) continue
32 const at = await $.fs.stat(`${path}/ready`).then(r => r.mtimeMs, () => 0)
33 if (now - at > STALE_MS) await $.process.run(['rm', '-rf', path])
34 }
35}
36
37// Runs from this mod's own timer: a spawn breadcrumbs causes, even through a command, skips every breadcrumbs hook, so the tester's tools would go unanswered.
38const poll = async ($: $) => {
39 if (!dir) return
40 let entries
41 try {
42 entries = await $.fs.list(dir)
43 } catch {
44 return
45 }
46 for (const entry of entries) {
47 if (entry.kind !== 'file' || !entry.name.endsWith('.json')) continue
48 const path = `${dir}/${entry.name}`
49 let request: Request
50 try {
51 request = JSON.parse(await $.fs.read(path)) as Request
52 } catch {
53 continue
54 }
55 // Taken before spawning, so breadcrumbs' fallback never starts the same tester twice.
56 await $.process.run(['rm', '-f', path])
57 if (request.subagentType !== TESTER || typeof request.prompt !== 'string') continue
58 const description = typeof request.description === 'string' ? request.description : 'Manual test'
59 try {
60 await $.agent.spawn({ subagentType: TESTER, prompt: request.prompt, description })
61 } catch (error) {
62 $.ui.log(`breadcrumbs-launcher: could not start "${description}" (${error instanceof Error ? error.message : String(error)})`)
63 }
64 }
65}
66
67export const register: Register = on => {
68 on('session.start', async ($, e, next) => {
69 const started = await next(e)
70 dir = `${await root($)}/${await $.session.id()}`
71 await stamp($)
72 $.clock.every(POLL_MS, () => poll($))
73 $.clock.every(HEARTBEAT_MS, () => stamp($))
74 const now = await $.clock.now()
75 $.clock.after(10_000, () => tidy($, now))
76
77 return started
78 })
79}
80