Probe: answers dispatch-pilot's reads of the board, the decision log and the skill-profile record with fixed scenes, so its band, footer and rationale pane can…

A probe for looking at dispatch-pilot's screens (the band, the footer, the rationale pane) with no model or decision request: it answers dispatch-pilot's $.state reads of board, decisionLog and skillProfiles with fixed scenes. Turns are not run; the screens draw what the scene holds.
It lives under docs/research/ so claude --plugin-dir . never loads it with the repo's mods. The scenes follow dispatch-pilot/types/index.d.ts as of 0.3.1: when the contract changes, update the scene that shows the change.
/dpscene <name>, or DP_SCENE=<name> at launch)| Scene | What it shows |
|---|---|
agents (default) | A main turn with dispatched agents: done, running, failed, a long and a Chinese name; mid-turn re-decisions kept, raised, held and blocked; a forced raise; a find_skill failure in the event stream; skill profiles done with one failure |
writing / stopped | agents, with the skill-profile line 生成中 2/5, or stopped by an API error |
workflow | A Workflow's agents: done, running, and a call not started yet (queued) |
notrouted | The main agent and a dispatched agent not routed, with the failure behind each |
idle | A finished turn: the band's one line, with 可试 /x |
many | 14 agents: more than the digit keys, rows past the band's height |
herdr pane split <your pane> --direction down (fullscreen iTerm2: a 194 x 32 pane, where the rationale pane docks on the right at about 76 columns; split it right again for ~97 columns, where the pane goes inline above the prompt).herdr pane run <new pane> "command claude --plugin-dir ./dispatch-pilot --plugin-dir ./docs/research/board-scenes". Never herdr pane run into a pane where Claude is running: it types the text in as a prompt.herdr pane send-text <pane> "/dpscene workflow" and herdr pane send-keys <pane> enter; /dp opens the pane. There is no pagedown: scroll a focused pane with repeated down.python3 docs/research/board-scenes/capture.py herdr <pane> <out_dir> <suffix> <scene>... switches scenes and saves each screen as .txt and .ansi (herdr pane read --format ansi). capture.py tmux <session> ... does the same in a detached tmux new -x 180 -y 50 (tmux quantizes the colours).dispatch-pilot/.claude-plugin/types/ back from the main checkout if you loaded a worktree's mod: loading it regenerated the types from that session, and tsc fails on mcp__dispatch-pilot__find_skill (scripts/check.sh copies them only when they are missing).Write what you saw, with the Claude Code version and the widths, into dispatch-pilot's DEVELOPMENT.md (「已实测的引擎行为」) when it is a fact about the engine.
hooks/scenes.ts 161 lines1// Probe for looking at dispatch-pilot's screens: answers its reads of `board`,
2// `decisionLog` and `skillProfiles` with fixed scenes (times relative to now),
3// so the real band, footer and rationale pane draw a busy turn without any
4// model or decision request. `/dpscene <name>` picks one (README.md lists them);
5// DP_SCENE=<name> at launch picks the first.
6//
7// The scenes follow dispatch-pilot's PluginState contract (dispatch-pilot/types/index.d.ts)
8// as of 0.3.1; a field the contract gains is absent here until a scene needs it.
9
10import type { Register } from 'claude-code'
11
12type Node = Record<string, unknown>
13type Entry = Record<string, unknown>
14type Scene = { board: { turn: number; starts: { turn: number; at: number }[]; changes?: unknown[]; notes?: unknown[]; nodes: Node[] }; log: Entry[]; profiles?: Record<string, unknown> }
15
16const levels = ['low', 'medium', 'high', 'xhigh', 'max'] as const
17const probs = (...p: number[]) => Object.fromEntries(levels.map((level, i) => [level, p[i] ?? 0]))
18
19/** The steps pickEffort records: the most likely level, the max gate, the round-up. */
20const pick = (top: string, p: number, gate: number, up: { above: string | null; p: number; applied: boolean; level: string }) => [
21 { rule: 'top', applied: true, level: top, p, tie: false },
22 { rule: 'max-gate', applied: false, level: top, p: gate, thetaMax: 0.5 },
23 { rule: 'round-up', applied: up.applied, level: up.level, above: up.above, p: up.p, threshold: 0.3, blockedByMax: false, thetaMax: 0.5 },
24]
25
26function agent(turn: number, id: string, name: string, model: string, effort: string | undefined, state: string, t0: number, extra: Node = {}): Node {
27 return { turn, id, kind: 'agent', name, type: 'general-purpose', model, ...(effort === undefined ? {} : { effort }), state, t0, routed: true, ...extra }
28}
29
30const TIMEOUT = { why: 'jev:1500 毫秒内没有回答', failure: { backend: 'jev', kind: 'timeout', detail: 'no answer in 1500 ms' } }
31
32function scenes(now: number): Record<string, Scene> {
33 const s = (seconds: number) => now - seconds * 1000
34 // A main turn with dispatched agents: mid-turn re-decisions (same, up, held, blocked), a forced raise, long names.
35 const agents: Scene = {
36 board: {
37 turn: 7,
38 starts: [{ turn: 6, at: s(400) }, { turn: 7, at: s(96) }],
39 changes: [
40 { turn: 7, id: 'main', at: 40.2, after: 29, from: { model: 'opus', effort: 'high' }, to: { model: 'opus', effort: 'xhigh' } },
41 { turn: 7, id: 'a4', at: 71, after: 30, from: { model: 'sonnet', effort: 'high' }, to: { model: 'sonnet', effort: 'xhigh' } },
42 ],
43 notes: [{ turn: 7, id: 'main', feature: 'find-skill', at: 80, after: 30, kind: 'failed', why: 'jev:1500 毫秒内没有回答' }],
44 nodes: [
45 { turn: 7, id: 'main', kind: 'main', name: '主 agent', type: 'main', model: 'opus', effort: 'xhigh', state: 'running', t0: 0, routed: true, decision: 23, midturn: { steps: 9, judged: 4, changed: 1 } },
46 agent(7, 'a1', 'Research mod UI surfaces', 'sonnet', 'medium', 'done', 6, { dur: 41, decision: 25 }),
47 agent(7, 'a2', 'Grep dispatch-pilot hooks', 'haiku', undefined, 'done', 9, { dur: 7, decision: 26 }),
48 agent(7, 'a4', 'Write migration tests for the board, then run them against every scene the prototype had', 'sonnet', 'xhigh', 'running', 48, { decision: 28, counts: { failed: 2, blocked: 1, raised: 1 } }),
49 agent(7, 'a5', 'Probe Desktop renderer', 'sonnet', 'high', 'failed', 52, { dur: 20, why: '出错' }),
50 agent(7, 'a6', '起草发布说明(中文名字很长很长的一个 agent)', 'fable', 'medium', 'running', 63),
51 ],
52 },
53 log: [
54 { n: 21, turn: 6, at: 0, feature: 'main-effort', agent: 'main', tone: 'ok', outcome: 'effort high', subject: '"目前状态栏那个三角形感叹号为什么一直在"', reason: '概率 low 0.00, medium 0.10, high 0.62, xhigh 0.24, max 0.04;置信度 0.40', effort: 'high', probs: probs(0, 0.1, 0.62, 0.24, 0.04), conf: 0.4, trace: pick('high', 0.62, 0.04, { above: 'xhigh', p: 0.24, applied: false, level: 'high' }) },
55 { n: 22, turn: 6, at: 3, feature: 'find-skill', agent: 'main', tone: 'ok', outcome: '找到 research', subject: '"research the status line"', reason: '第一段 research 0.80,都不合适 0.20;fits research 0.91', skills: { suggest: [{ name: 'research', relevance: 0.91 }], try: [] } },
56 { n: 23, turn: 7, at: 0, feature: 'main-effort', agent: 'main', tone: 'ok', outcome: 'effort xhigh', subject: '"把看板做出来,带依据面板和时间色带"', reason: '概率 low 0.00, medium 0.05, high 0.56, xhigh 0.35, max 0.04;置信度 0.25', effort: 'xhigh', probs: probs(0, 0.05, 0.56, 0.35, 0.04), conf: 0.25, trace: pick('high', 0.56, 0.04, { above: 'xhigh', p: 0.35, applied: true, level: 'xhigh' }) },
57 { n: 24, turn: 7, at: 0.4, feature: 'skills', agent: 'main', tone: 'ok', outcome: '推荐 research', subject: '"把看板做出来,带依据面板和时间色带"', reason: '第一段 research 0.70,plugin-authoring 0.22;相关度 0.70 起推荐', skills: { suggest: [{ name: 'research', relevance: 0.86 }, { name: 'plugin-authoring', relevance: 0.8 }], try: [{ name: 'grill-me', relevance: 0.81 }] } },
58 { n: 25, turn: 7, at: 6, feature: 'dispatched-agents', agent: 'a1', tone: 'ok', outcome: 'sonnet medium', subject: '"Research mod UI surfaces"(general-purpose)', reason: '已决定,不用主 agent 指定的 opus;选 sonnet,置信度 0.62;effort 概率 low 0.55, medium 0.38, high 0.05, xhigh 0.02, max 0.00', model: 'sonnet', effort: 'medium', conf: 0.62, probs: probs(0.55, 0.38, 0.05, 0.02, 0), trace: [...pick('low', 0.55, 0, { above: 'medium', p: 0.38, applied: true, level: 'medium' }), { rule: 'model-floor', applied: false, level: 'medium', from: 'medium', model: 'sonnet', floor: 'medium' }] },
59 { n: 26, turn: 7, at: 9, feature: 'dispatched-agents', agent: 'a2', tone: 'ok', outcome: 'haiku', subject: '"Grep dispatch-pilot hooks"(Explore)', reason: '已决定;选 haiku,置信度 0.71', model: 'haiku', conf: 0.71 },
60 { n: 27, turn: 7, at: 20, feature: 'midturn-effort', agent: 'main', tone: 'info', outcome: 'effort high(不变)', subject: '第 3 步(每 3 步)', reason: '置信度 0.80;同一档', conf: 0.8, mid: { current: 'high', picked: 'high', result: 'high' }, trace: [...pick('high', 0.7, 0, { above: 'xhigh', p: 0.2, applied: false, level: 'high' }), { rule: 'suggest', applied: true, picked: 'high', current: 'high', direction: 'same' }] },
61 { n: 28, turn: 7, at: 48, feature: 'dispatched-agents', agent: 'a4', tone: 'ok', outcome: 'sonnet high', subject: '"Write migration tests"(general-purpose)', reason: '已决定;选 sonnet,置信度 0.52', model: 'sonnet', effort: 'high', conf: 0.52, probs: probs(0.02, 0.2, 0.58, 0.18, 0.02), trace: [...pick('high', 0.58, 0.02, { above: 'xhigh', p: 0.18, applied: false, level: 'high' }), { rule: 'model-floor', applied: false, level: 'high', from: 'high', model: 'sonnet', floor: 'medium' }] },
62 { n: 29, turn: 7, at: 40, feature: 'midturn-effort', agent: 'main', tone: 'ok', outcome: 'effort xhigh(原来 high)', subject: '第 6 步(派出了 agent)', reason: '置信度 0.62;升档', conf: 0.62, mid: { current: 'high', picked: 'xhigh', result: 'xhigh', threshold: 0.3 }, trace: [...pick('xhigh', 0.6, 0, { above: 'max', p: 0.1, applied: false, level: 'xhigh' }), { rule: 'suggest', applied: true, picked: 'xhigh', current: 'high', direction: 'up' }, { rule: 'theta-up', applied: true, confidence: 0.62, threshold: 0.3 }] },
63 { n: 30, turn: 7, at: 70, feature: 'escalation', agent: 'a4', tone: 'warn', outcome: 'effort xhigh(原来 high)', subject: 'agent "Write migration tests",第 5 步(2 次工具调用失败)', reason: '强制升一档;不是预期内(概率 0.05,thetaExpected 0.25)', forced: { kind: 'effort', from: 'high', to: 'xhigh', floor: 'xhigh' }, counts: { failed: 2, blocked: 1, raised: 1 }, trace: [{ rule: 'forced-raise', applied: true, level: 'xhigh', from: 'high', mode: 'one-level' }, ...pick('high', 0.5, 0, { above: 'xhigh', p: 0.25, applied: false, level: 'high' }), { rule: 'theta-up', applied: true, confidence: 0.7, threshold: 0.3 }, { rule: 'higher-of', applied: false, level: 'xhigh', from: 'xhigh' }] },
64 { n: 31, turn: 7, at: 75, feature: 'midturn-effort', agent: 'main', tone: 'info', outcome: 'effort xhigh(不变)', subject: '第 8 步(每 3 步)', reason: '防抖:2 步前刚升档(holdSteps 5)', conf: 0.9, mid: { current: 'xhigh', picked: 'medium', result: 'xhigh', held: '防抖:2 步前刚升档(holdSteps 5)', remaining: 3 }, trace: [{ rule: 'suggest', applied: true, picked: 'medium', current: 'xhigh', direction: 'down' }, { rule: 'hold', applied: true, sinceRaise: 2, holdSteps: 5, remaining: 3 }] },
65 { n: 32, turn: 7, at: 90, feature: 'midturn-effort', agent: 'main', tone: 'info', outcome: 'effort xhigh(不变)', subject: '第 11 步(每 3 步)', reason: 'high 把握不够(降档要 thetaDown 0.55)', conf: 0.42, mid: { current: 'xhigh', picked: 'high', result: 'xhigh', threshold: 0.55 }, trace: [{ rule: 'suggest', applied: true, picked: 'high', current: 'xhigh', direction: 'down' }, { rule: 'hold', applied: false, sinceRaise: 5, holdSteps: 5, remaining: 0 }, { rule: 'theta-down', applied: false, confidence: 0.42, threshold: 0.55 }] },
66 ],
67 profiles: { phase: 'done', turn: 1, model: 'haiku', kept: 52, planned: 4, written: 3, failed: 1, deferred: 0, failures: [{ name: 'anthropic-skills:google-workspace', reason: '回答不是画像' }] },
68 }
69 return {
70 agents,
71 // The skill-profile line while profiles are written, and once the writing stopped.
72 writing: { ...agents, profiles: { phase: 'writing', turn: 1, model: 'haiku', kept: 52, planned: 5, written: 2, failed: 0, deferred: 0, failures: [] } },
73 stopped: { ...agents, profiles: { phase: 'stopped', turn: 1, model: 'haiku', kept: 0, planned: 4, written: 1, failed: 0, deferred: 2, stop: { reason: 'api-error', detail: 'an API error, HTTP 529 overloaded' }, failures: [] } },
74 // A Workflow's agents: done, running, and a call that has not started (queued, id with `#`).
75 workflow: {
76 board: {
77 turn: 8,
78 starts: [{ turn: 7, at: s(300) }, { turn: 8, at: s(50) }],
79 nodes: [
80 { turn: 8, id: 'main', kind: 'main', name: '主 agent', type: 'main', model: 'opus', effort: 'high', state: 'running', t0: 0, routed: true, decision: 33 },
81 ...['types', 'docs', 'probes', 'desktop', 'terminal'].map((name, i) => ({ turn: 8, id: `wa${i}`, kind: 'wf', name: `sweep:${name}`, type: 'workflow', model: i === 2 ? 'haiku' : 'sonnet', ...(i === 2 ? {} : { effort: 'high' }), state: 'done', t0: 12 + i * 0.3, dur: 11 + i * 4, routed: true, workflow: { id: 'toolu_wf', name: 'mod-ui-research' } })),
82 { turn: 8, id: 'wa5', kind: 'wf', name: 'verify:cross-check', type: 'workflow', model: 'opus', effort: 'xhigh', state: 'running', t0: 40, routed: true, workflow: { id: 'toolu_wf', name: 'mod-ui-research' } },
83 { turn: 8, id: 'wa6', kind: 'wf', name: 'verify:claims', type: 'workflow', model: 'opus', effort: 'high', state: 'running', t0: 44, routed: true, workflow: { id: 'toolu_wf', name: 'mod-ui-research' } },
84 { turn: 8, id: 'toolu_wf#7', kind: 'wf', name: 'report:summary', type: 'workflow', state: 'queued', t0: 3, routed: true, workflow: { id: 'toolu_wf', name: 'mod-ui-research' } },
85 ],
86 },
87 log: [
88 { n: 33, turn: 8, at: 0, feature: 'main-effort', agent: 'main', tone: 'ok', outcome: 'effort high', subject: '"调研各个画面"', reason: '', effort: 'high', trace: pick('high', 0.61, 0, { above: 'xhigh', p: 0.2, applied: false, level: 'high' }) },
89 ...Array.from({ length: 8 }, (_, i) => ({ n: 34 + i, turn: 8, at: 3, feature: 'workflow-agents', agent: `toolu_wf#${i}`, tone: 'ok', outcome: i === 2 ? 'haiku' : i >= 5 ? 'opus high' : 'sonnet high', subject: '"x"(Workflow mod-ui-research)', reason: '', model: i === 2 ? 'haiku' : i >= 5 ? 'opus' : 'sonnet', ...(i === 2 ? {} : { effort: 'high' }) })),
90 ],
91 },
92 // Not routed, with the failure behind it: the main agent and one dispatched agent.
93 notrouted: {
94 board: {
95 turn: 9,
96 starts: [{ turn: 8, at: s(200) }, { turn: 9, at: s(23) }],
97 nodes: [
98 { turn: 9, id: 'main', kind: 'main', name: '主 agent', type: 'main', model: 'opus', effort: 'xhigh', state: 'running', t0: 0, routed: false, ...TIMEOUT },
99 agent(9, 'a1', 'Grep dispatch-pilot hooks', 'haiku', undefined, 'done', 5, { dur: 7, decision: 26 }),
100 agent(9, 'a2', 'Check marketplace manifest', 'opus', 'xhigh', 'running', 8, { routed: false, why: 'jev:出错(状态码 500)', failure: { backend: 'jev', kind: 'http', detail: 'HTTP 500', status: 500 } }),
101 ],
102 },
103 log: agents.log,
104 },
105 // A finished turn: the band folds into its one idle line, with 可试 /x.
106 idle: {
107 board: {
108 turn: 10,
109 starts: [{ turn: 9, at: s(500) }, { turn: 10, at: s(130) }],
110 nodes: [
111 { turn: 10, id: 'main', kind: 'main', name: '主 agent', type: 'main', model: 'opus', effort: 'xhigh', state: 'done', t0: 0, dur: 118, routed: true, decision: 50 },
112 agent(10, 'a1', 'Research', 'sonnet', 'medium', 'done', 6, { dur: 41 }),
113 agent(10, 'a2', 'Grep', 'haiku', undefined, 'done', 9, { dur: 7 }),
114 agent(10, 'a3', 'Review', 'opus', 'xhigh', 'failed', 31, { dur: 50, why: '出错' }),
115 ],
116 },
117 log: [
118 { n: 50, turn: 10, at: 0, feature: 'main-effort', agent: 'main', tone: 'ok', outcome: 'effort xhigh', subject: '"..."', reason: '', effort: 'xhigh', trace: pick('high', 0.44, 0, { above: 'xhigh', p: 0.35, applied: true, level: 'xhigh' }) },
119 { n: 51, turn: 10, at: 0.4, feature: 'skills', agent: 'main', tone: 'ok', outcome: '没有推荐 skill', subject: '"..."', reason: '', skills: { suggest: [], try: [{ name: 'grill-me', relevance: 0.81 }] } },
120 ],
121 },
122 // A wide fan-out: more agents than digit keys, rows past the band's height.
123 many: {
124 board: {
125 turn: 11,
126 starts: [{ turn: 10, at: s(600) }, { turn: 11, at: s(140) }],
127 nodes: [
128 { turn: 11, id: 'main', kind: 'main', name: '主 agent', type: 'main', model: 'sonnet', effort: 'medium', state: 'running', t0: 0, routed: true },
129 ...Array.from({ length: 14 }, (_, i) => agent(11, `m${i}`, `fan-out task ${i + 1}`, i % 3 === 0 ? 'haiku' : 'sonnet', i % 3 === 0 ? undefined : i % 2 === 0 ? 'low' : 'medium', i < 6 ? 'done' : 'running', 4 + i * 6, i < 6 ? { dur: 20 + i } : {})),
130 ],
131 },
132 log: [],
133 },
134 }
135}
136
137/** The scene shown: the person's last `/dpscene`. A hot reload starts over at DP_SCENE (or `agents`). */
138let scene = 'agents'
139
140export const register: Register = (on) => {
141 on('session.start', { cwd: /(?:)/ }, async ($, e, next) => {
142 const result = await next(e)
143 scene = (await $.env.get('DP_SCENE').catch(() => undefined)) ?? scene
144 await $.command.register({ name: 'dpscene', description: 'probe: pick the fake board scene', argumentHint: 'agents|writing|stopped|workflow|notrouted|idle|many', immediate: true }).catch(() => undefined)
145 return result
146 })
147 on('command.run', { command: 'dpscene' }, async ($, e) => {
148 const want = e.args.trim()
149 if (want !== '') scene = want
150 $.ui.invalidate('ui.render')
151 return { text: `scene ${scene}` }
152 })
153 const of = async (now: Promise<number>) => {
154 const all = scenes(await now)
155 return all[scene] ?? all.agents
156 }
157 on('state.get', { plugin: 'dispatch-pilot', key: 'board' }, async ($) => ({ value: { value: (await of($.clock.now()))?.board as never, version: 1 } }))
158 on('state.get', { plugin: 'dispatch-pilot', key: 'decisionLog' }, async ($) => ({ value: { value: (await of($.clock.now()))?.log as never, version: 1 } }))
159 on('state.get', { plugin: 'dispatch-pilot', key: 'skillProfiles' }, async ($) => ({ value: { value: (await of($.clock.now()))?.profiles as never, version: 1 } }))
160}
161