SLOPSHOPPER

board-scenes

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…

newcommand
★ 2v0.0.1no licenseupdated 2026-10-06alexcz-a11y/claude-mods/docs/research/board-scenes
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · board-scenes
› fix the failing auth test and add an audit log call ⏺ 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 › /dpscene ⎿ board-scenes: scene agents ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

board-scenes

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.

Scenes (/dpscene <name>, or DP_SCENE=<name> at launch)

SceneWhat 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 / stoppedagents, with the skill-profile line 生成中 2/5, or stopped by an API error
workflowA Workflow's agents: done, running, and a call not started yet (queued)
notroutedThe main agent and a dispatched agent not routed, with the failure behind each
idleA finished turn: the band's one line, with 可试 /x
many14 agents: more than the digit keys, rows past the band's height

Looking at it (iTerm2 + herdr)

  1. Open a fresh shell pane next to yours: 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).
  2. In that shell pane only: 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.
  3. Drive it with 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.
  4. After a resize, wait 5 to 12 seconds before reading: Claude follows a herdr resize slowly, passing through ~25 and ~52 columns.
  5. 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).
  6. Close the panes you opened. Then copy 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.

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