bullswarm 0.10.4 → 0.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -37,7 +37,7 @@ content. Published as `bullswarm` on npm.
37
37
  ## Development
38
38
 
39
39
  ```bash
40
- npm test # 256 tests, no network needed (meters read from cache)
40
+ npm test # 261 tests, no network needed (meters read from cache)
41
41
  node bin/bullswarm.js doctor --json # readiness report
42
42
  node bin/bullswarm.js workflow list # discover workflows
43
43
  node bin/bullswarm.js workflow runs # ongoing workflow instances
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # bullswarm changelog
2
2
 
3
+ ## 0.10.7 — clearer orchestration overview
4
+
5
+ - Replaced the orchestrator trace dump with a summary-first view showing what
6
+ it is doing now, the latest decision and reason, the next action, worker
7
+ progress, and the last three semantic events.
8
+ - Added `v` progressive disclosure for provider sessions, checkpoint prompts,
9
+ per-turn usage, full decisions, and artifact paths, keeping audit detail
10
+ available without making it the default human experience.
11
+
12
+ ## 0.10.6 — safe setup and boolean flags
13
+
14
+ - Marked the deterministic `echo` connector as a test fixture and excluded it
15
+ from automatic setup, routing suggestions, connector readiness, and delegate
16
+ readiness. Existing installations receive a one-time migration that disables
17
+ an accidentally enabled fixture while preserving later explicit choices.
18
+ - Interactive setup labels test fixtures and defaults them to disabled;
19
+ `doctor` now requires at least one real enabled delegate instead of treating
20
+ canned fixture output as offload capability.
21
+ - Made top-level boolean flags explicit so options such as `--no-caller`,
22
+ `--force`, and `--dry-run` cannot swallow a following positional task.
23
+ - Thanks to @kwunlokng for reporting both defect classes and supplying focused
24
+ reproductions in #1, #2, and #3.
25
+
3
26
  ## 0.10.4 — durable interactive workflow viewer
4
27
 
5
28
  - Added `workflow runs result <id> [--json]`, a stable
package/README.md CHANGED
@@ -276,9 +276,11 @@ bullswarm workflow watch <shortId> --once # one current/terminal snapshot
276
276
  autonomous goal its left navigation stacks a compact Orchestrator panel above
277
277
  the Phases panel; internal planner turns never appear as workers or phases.
278
278
  Select Orchestration and press Enter, or press `o`
279
- anywhere, to inspect the logical orchestration conversation: provider session,
280
- checkpoint turns and decisions, recent semantic actions, usage, prompt, and
281
- artifacts. Status marks are consistent throughout the tree: `○` not started,
279
+ anywhere, to open a summary-first orchestration overview: what it is doing now,
280
+ its latest decision in plain language, why it chose that path, what happens
281
+ next, progress, and the last three semantic actions. Press `v` to toggle the
282
+ technical view with provider session, every checkpoint turn, usage, prompt,
283
+ and artifact paths. Status marks are consistent throughout the tree: `○` not started,
282
284
  an animated Braille spinner for active work, `⧖` waiting, `✓` finished, and
283
285
  `✗` failed or interrupted. The non-emoji `⧖` avoids the inconsistent cell
284
286
  width of `⌛` across terminal fonts. It watches ongoing runs from disk and supports `j`/`k` or arrow-key selection, Enter for
@@ -23,6 +23,6 @@
23
23
  "pricingSource": "local deterministic fixture",
24
24
  "pricingUpdatedAt": "2026-08-27"
25
25
  }],
26
- "flags": { "stealth": false },
26
+ "flags": { "stealth": false, "testFixture": true },
27
27
  "$comment-timeout": "No implicit wall-clock timeout; callers may opt in with --timeout or workflow step.timeoutSec"
28
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bullswarm",
3
- "version": "0.10.4",
3
+ "version": "0.10.7",
4
4
  "description": "Route work across coding-agent CLI subscriptions — paced by live quota meters, verified by content, never trusting exit codes.",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -263,8 +263,10 @@ cannot be guaranteed. Restore eligibility with `strategy include-model`.
263
263
 
264
264
  In the human TUI, the autonomous orchestrator has a compact selectable panel
265
265
  stacked above the phase tree. Select it and press Enter, or press
266
- `o`, to inspect its durable provider session, checkpoint turns and decisions,
267
- recent semantic actions, usage, prompt, and artifacts. Esc returns to phases.
266
+ `o`, to see a summary-first overview of its current role, latest decision,
267
+ reason, next action, progress, and recent semantic activity. Press `v` for the
268
+ durable provider session, checkpoint prompts and turns, usage, and artifact
269
+ paths; `v` returns to the overview and Esc returns to phases.
268
270
  The shared state marks are `○` not started, animated Braille spinner active,
269
271
  `⧖` waiting, `✓` finished, and `✗` failed or interrupted.
270
272
 
@@ -353,8 +355,8 @@ work*: "In 200 words: (1) the top 3 risks, (2) cited file paths,
353
355
  You do NOT need to set up bullswarm. Every verb self-initializes:
354
356
 
355
357
  - Missing `~/.bullswarm/`? Created on first call.
356
- - No enabled pools? `autoSetup` enables every installed agent CLI plus
357
- the deterministic `echo` pool (so offload works on a fresh machine).
358
+ - No enabled pools? `autoSetup` enables every installed real agent CLI. The
359
+ deterministic `echo` connector is a test fixture and is never auto-enabled.
358
360
  - `bullswarm doctor --json` returns a readiness report; non-zero exit
359
361
  only on a real problem. The first call to `doctor` self-heals.
360
362
 
@@ -376,7 +378,7 @@ of exact commands to fix anything missing.
376
378
  ### "Audit every file in this directory"
377
379
 
378
380
  A discover-based fanout requires the discover delegate to return a bare
379
- JSON array in its output file. The built-in `echo` pool is a verifier
381
+ JSON array in its output file. The built-in `echo` pool is a disabled test
380
382
  fixture and deliberately returns prose, so use a real provider for this
381
383
  variant:
382
384
 
package/src/cli.js CHANGED
@@ -29,14 +29,18 @@ export function getBullswarmDir() {
29
29
  // call getBullswarmDir() so BULLSWARM_HOME is honored at invocation time.
30
30
  export const BULLSWARM_DIR = getBullswarmDir();
31
31
 
32
- function parseArgs(argv) {
32
+ const BOOLEAN_FLAGS = new Set([
33
+ 'json', 'force', 'no-caller', 'yes', 'strategy', 'integrate', 'dry-run',
34
+ ]);
35
+
36
+ export function parseArgs(argv) {
33
37
  const args = {};
34
38
  const rest = [];
35
39
  for (let i = 0; i < argv.length; i++) {
36
40
  if (argv[i].startsWith('--')) {
37
41
  const eq = argv[i].indexOf('=');
38
42
  const key = argv[i].slice(2, eq > 0 ? eq : undefined);
39
- if (key === 'json') args.json = true;
43
+ if (BOOLEAN_FLAGS.has(key)) args[key] = true;
40
44
  else if (eq > 0) args[key] = argv[i].slice(eq + 1);
41
45
  else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) args[key] = argv[++i];
42
46
  else args[key] = true;
@@ -355,12 +359,12 @@ async function cmdDoctor(opts) {
355
359
  }
356
360
 
357
361
  const discovered = discoverConnectors();
358
- const found = discovered.filter((d) => d.discovered && !d.broken);
362
+ const found = discovered.filter((d) => d.discovered && !d.broken && !d.testFixture);
359
363
  checks.push({
360
364
  id: 'connectors',
361
365
  ok: found.length > 0,
362
366
  detail: `${found.length} agent CLI(s) found: ${found.map((d) => d.name).join(', ') || '(none)'}`,
363
- fix: found.length ? null : 'install at least one agent CLI (codex, grok, opencode…) — echo pool still works',
367
+ fix: found.length ? null : 'install at least one agent CLI (codex, grok, opencode…)',
364
368
  });
365
369
 
366
370
  try {
@@ -369,6 +373,7 @@ async function cmdDoctor(opts) {
369
373
  });
370
374
  const live = pools.filter((p) => p.meterSource === 'live' || p.meterSource === 'cache');
371
375
  const enabled = pools.filter((p) => p.enabled);
376
+ const delegates = enabled.filter((p) => !p.testFixture);
372
377
  checks.push({
373
378
  id: 'meters',
374
379
  ok: enabled.length > 0,
@@ -377,9 +382,9 @@ async function cmdDoctor(opts) {
377
382
  });
378
383
  checks.push({
379
384
  id: 'offload-capable',
380
- ok: enabled.some((p) => p.name !== 'echo') || enabled.length > 0,
381
- detail: `enabled pools: ${enabled.map((p) => p.name).join(', ') || 'none'}`,
382
- fix: null,
385
+ ok: delegates.length > 0,
386
+ detail: `enabled delegate pools: ${delegates.map((p) => p.name).join(', ') || 'none'}`,
387
+ fix: delegates.length ? null : 'install an agent CLI and run bullswarm setup --yes',
383
388
  });
384
389
  } catch (err) {
385
390
  checks.push({ id: 'meters', ok: false, detail: err.message, fix: 'check network / re-run' });
package/src/lib/config.js CHANGED
@@ -45,7 +45,8 @@ export function buildPools(bullswarmDir, now = Date.now(), readings = {}) {
45
45
  const pool = {
46
46
  name,
47
47
  connector: conn,
48
- enabled: ps.enabled !== false,
48
+ testFixture: conn.flags?.testFixture === true,
49
+ enabled: conn.flags?.testFixture === true ? ps.enabled === true : ps.enabled !== false,
49
50
  costRank: conn.costRank ?? 5,
50
51
  lanes: conn.lanes,
51
52
  capabilities: conn.capabilities ?? [],
package/src/setup.js CHANGED
@@ -97,6 +97,7 @@ export function discoverConnectors() {
97
97
  meter: conn.meter?.type ?? 'none',
98
98
  costRank: conn.costRank,
99
99
  lanes: conn.lanes,
100
+ testFixture: conn.flags?.testFixture === true,
100
101
  });
101
102
  }
102
103
  return found;
@@ -187,6 +188,10 @@ export function upgradeConnectorMetadata(bullswarmDir) {
187
188
  installed.model = packaged.model;
188
189
  changed = true;
189
190
  }
191
+ if (packaged.flags?.testFixture === true && installed.flags?.testFixture !== true) {
192
+ installed.flags = { ...(installed.flags ?? {}), testFixture: true };
193
+ changed = true;
194
+ }
190
195
  if (installed.eventStream == null && packaged.eventStream != null) {
191
196
  installed.eventStream = packaged.eventStream;
192
197
  // Older packaged connectors used stdout extraction. Once JSONL flags
@@ -218,6 +223,34 @@ export function upgradeConnectorMetadata(bullswarmDir) {
218
223
  return upgraded;
219
224
  }
220
225
 
226
+ // One-time safety migration for installations created before connectors could
227
+ // identify deterministic test fixtures. Explicit choices made after this
228
+ // migration remain respected.
229
+ export function migrateTestFixturePools(bullswarmDir) {
230
+ const state = loadState(bullswarmDir);
231
+ state.config ??= {};
232
+ if (state.config.testFixturesMigrated === true) return [];
233
+
234
+ const connectorsDir = join(bullswarmDir, 'connectors');
235
+ const disabled = [];
236
+ if (existsSync(connectorsDir)) {
237
+ for (const file of readdirSync(connectorsDir)) {
238
+ if (!file.endsWith('.json') || file.startsWith('_')) continue;
239
+ try {
240
+ const connector = JSON.parse(readFileSync(join(connectorsDir, file), 'utf8'));
241
+ if (connector.flags?.testFixture !== true) continue;
242
+ state.pools ??= {};
243
+ state.pools[connector.name] ??= {};
244
+ if (state.pools[connector.name].enabled !== false) disabled.push(connector.name);
245
+ state.pools[connector.name].enabled = false;
246
+ } catch { /* connector repair owns malformed files */ }
247
+ }
248
+ }
249
+ state.config.testFixturesMigrated = true;
250
+ saveState(bullswarmDir, state);
251
+ return disabled;
252
+ }
253
+
221
254
  // --- auto-setup ---------------------------------------------------------------
222
255
  // Zero-touch initialization: enable every discovered pool, write config,
223
256
  // never prompt. Used by `setup --yes`, by any verb on first use, and by
@@ -229,13 +262,12 @@ export function autoSetup(bullswarmDir, { reason = 'auto' } = {}) {
229
262
  const discovered = discoverConnectors();
230
263
  const repaired = repairConnectors(bullswarmDir);
231
264
 
232
- const usable = discovered.filter((d) => !d.broken && d.discovered);
233
- // Always include the deterministic echo pool so offloading works even on
234
- // a machine with zero other agent CLIs installed.
265
+ const usable = discovered.filter((d) => !d.broken && d.discovered && !d.testFixture);
235
266
  const enabled = new Set(usable.map((d) => d.name));
236
- enabled.add('echo');
237
267
 
238
268
  state.pools ??= {};
269
+ state.config ??= {};
270
+ state.config.testFixturesMigrated = true;
239
271
  for (const d of discovered.filter((x) => !x.broken)) {
240
272
  state.pools[d.name] ??= {};
241
273
  state.pools[d.name].enabled = enabled.has(d.name);
@@ -275,6 +307,7 @@ export function isConfigured(bullswarmDir) {
275
307
  export function ensureSetup(bullswarmDir) {
276
308
  if (isConfigured(bullswarmDir)) {
277
309
  upgradeConnectorMetadata(bullswarmDir);
310
+ migrateTestFixturePools(bullswarmDir);
278
311
  return null;
279
312
  }
280
313
  return autoSetup(bullswarmDir, { reason: 'first-use' });
@@ -306,7 +339,7 @@ export async function runWizard(bullswarmDir, opts = {}) {
306
339
  ? 'quota: unmetered'
307
340
  : `quota: ${d.meter} window (burn rate: learning)`;
308
341
  console.log(
309
- ` ${d.name.padEnd(14)} ${d.discovered ? 'found' : 'not found'} ${meter}`,
342
+ ` ${d.name.padEnd(14)} ${d.discovered ? 'found' : 'not found'} ${meter}${d.testFixture ? ' TEST FIXTURE' : ''}`,
310
343
  );
311
344
  }
312
345
  console.log('');
@@ -314,8 +347,11 @@ export async function runWizard(bullswarmDir, opts = {}) {
314
347
  // 2. Toggle pools
315
348
  const enabled = [];
316
349
  for (const d of discovered.filter((x) => !x.broken && x.discovered)) {
317
- const ans = (await rl.question(`enable ${d.name}? [Y/n] `)).trim().toLowerCase();
318
- if (ans !== 'n') enabled.push(d.name);
350
+ const prompt = d.testFixture
351
+ ? `enable ${d.name} test fixture? [y/N] `
352
+ : `enable ${d.name}? [Y/n] `;
353
+ const ans = (await rl.question(prompt)).trim().toLowerCase();
354
+ if (d.testFixture ? (ans === 'y' || ans === 'yes') : ans !== 'n') enabled.push(d.name);
319
355
  }
320
356
 
321
357
  if (enabled.length === 0) {
@@ -332,6 +368,8 @@ export async function runWizard(bullswarmDir, opts = {}) {
332
368
  mkdirSync(join(bullswarmDir, 'connectors'), { recursive: true });
333
369
  const repaired = repairConnectors(bullswarmDir);
334
370
  state.pools ??= {};
371
+ state.config ??= {};
372
+ state.config.testFixturesMigrated = true;
335
373
  for (const d of discovered.filter((x) => !x.broken)) {
336
374
  state.pools[d.name] ??= {};
337
375
  state.pools[d.name].enabled = enabled.includes(d.name);
@@ -345,7 +345,8 @@ export function workflowPanelModel(row, {
345
345
  export function renderWorkflowTui(row, {
346
346
  width = 120, height = 36, focus = 0, phaseIndex = null, agentIndex = null,
347
347
  detailScroll = 0, message = null, confirmCancel = false,
348
- controlSelected = false, orchestratorDetail = false, spinnerFrame = 0,
348
+ controlSelected = false, orchestratorDetail = false, orchestratorVerbose = false,
349
+ spinnerFrame = 0,
349
350
  } = {}) {
350
351
  width = Math.max(38, Number(width) || 120);
351
352
  height = Math.max(18, Number(height) || 36);
@@ -367,10 +368,12 @@ export function renderWorkflowTui(row, {
367
368
  ];
368
369
  const footer = confirmCancel
369
370
  ? ' Stop this workflow? y confirm · n/Esc keep running'
370
- : ` ↑/↓ select · Enter drill in · Esc back · o orchestrator · c stop · q detach`;
371
+ : orchestratorDetail
372
+ ? ` ↑/↓ scroll · v ${orchestratorVerbose ? 'overview' : 'technical details'} · Esc back · c stop · q detach`
373
+ : ` ↑/↓ select · Enter drill in · Esc back · o orchestrator · c stop · q detach`;
371
374
  const rawMessageLine = message
372
375
  ? ` ${truncate(message, width - 2)}`
373
- : ` ${orchestratorDetail ? 'Orchestrator thread' : focus === 0 ? 'Phases' : focus === 1 ? 'Agents' : 'Agent activity'} · r refresh · workflow continues after detach`;
376
+ : ` ${orchestratorDetail ? `Orchestrator ${orchestratorVerbose ? 'technical details' : 'overview'}` : focus === 0 ? 'Phases' : focus === 1 ? 'Agents' : 'Agent activity'} · r refresh · workflow continues after detach`;
374
377
  const messageLine = truncate(rawMessageLine, width);
375
378
  const bodyHeight = Math.max(10, height - header.length - 3);
376
379
 
@@ -409,6 +412,7 @@ export function renderWorkflowTui(row, {
409
412
  model,
410
413
  Math.max(20, (orchestratorDetail ? width : rightWidth) - 4),
411
414
  spinnerFrame,
415
+ { verbose: orchestratorVerbose },
412
416
  );
413
417
  const detail = orchestratorDetail
414
418
  ? orchestrationLines
@@ -444,7 +448,7 @@ export function renderWorkflowTui(row, {
444
448
 
445
449
  let body;
446
450
  if (orchestratorDetail) {
447
- body = renderPanel('Orchestrator thread · control plane', visibleDetail, width, bodyHeight);
451
+ body = renderPanel(`Orchestrator · ${orchestratorVerbose ? 'technical details' : 'overview'}`, visibleDetail, width, bodyHeight);
448
452
  } else if (narrow) {
449
453
  const mobile = focus === 0
450
454
  ? {
@@ -495,7 +499,7 @@ function joinPanels(left, right) {
495
499
  return left.map((line, index) => `${line}${right[index] ?? ''}`);
496
500
  }
497
501
 
498
- function orchestratorDetailLines(model, width, spinnerFrame) {
502
+ function orchestratorDetailLines(model, width, spinnerFrame, { verbose = false } = {}) {
499
503
  const { orchestrator, state } = model;
500
504
  if (!orchestrator.autonomous) {
501
505
  return wrapLines(['This workflow has no autonomous orchestrator thread.'], width);
@@ -505,10 +509,57 @@ function orchestratorDetailLines(model, width, spinnerFrame) {
505
509
  const liveActions = active?.lastActions ?? latest?.lastActions ?? [];
506
510
  const totalActions = active?.actionCount ?? latest?.actionCount ?? liveActions.length;
507
511
  const conversations = Object.entries(state.orchestration?.conversations ?? {});
512
+ const decisions = state.decisions ?? [];
513
+ const latestDecision = decisions.at(-1);
514
+ const workerAttempts = (state.attempts ?? []).filter((attempt) => attempt.actionId !== orchestrator.actionId);
515
+ const completedWorkers = workerAttempts.filter((attempt) => TERMINAL_ACTIONS.has(attempt.status)).length;
516
+ const activeWorkers = Object.values(state.activeAgents ?? {})
517
+ .filter((agent) => agent.stepId !== orchestrator.actionId);
518
+ const nextActions = latestDecision?.actions?.map((action) => action.id).filter(Boolean) ?? [];
519
+ const stateLabel = active
520
+ ? 'Choosing the next smallest useful action'
521
+ : activeWorkers.length
522
+ ? `Waiting for ${activeWorkers.length} worker${activeWorkers.length === 1 ? '' : 's'} to finish`
523
+ : state.finishedAt
524
+ ? 'Workflow finished'
525
+ : 'Reviewing completed evidence';
508
526
  const lines = [
509
527
  `${statusIcon(active ? 'running' : latest?.status ?? orchestrator.status, spinnerFrame)} ${orchestrator.status} · ${orchestrator.pool} · ${orchestrator.model}`,
510
- `Logical thread · ${orchestrator.attempts.length} checkpoint turn${orchestrator.attempts.length === 1 ? '' : 's'}`,
528
+ '',
529
+ `Now · ${stateLabel}`,
530
+ `Progress · ${completedWorkers}/${workerAttempts.length} worker attempts finished · ${orchestrator.attempts.length} planning checkpoint${orchestrator.attempts.length === 1 ? '' : 's'}`,
511
531
  ];
532
+ if (latestDecision) {
533
+ lines.push(`Latest decision · ${decisionLabel(latestDecision.decision)}`);
534
+ if (latestDecision.reason) lines.push(`Why · ${sentencePreview(latestDecision.reason)}`);
535
+ lines.push(`Next · ${nextActions.length ? nextActions.join(', ') : latestDecision.decision === 'complete' ? 'Return the verified result' : 'Wait for current work, then reassess'}`);
536
+ } else {
537
+ lines.push('Latest decision · Planning has not completed its first checkpoint yet');
538
+ }
539
+ if (active?.stall?.status === 'suspected_stalled') {
540
+ lines.push(`Attention · No new evidence for ${active.stall.silentForSec}s; the agent has not been auto-killed`);
541
+ }
542
+
543
+ if (!verbose) {
544
+ if (state.finishedAt) {
545
+ const verified = state.outcome?.verified === true;
546
+ lines.push('', `Result · ${verified ? 'Verified delivery is ready' : state.status === 'completed_with_concerns' ? 'Best useful delivery is ready with concerns' : state.status === 'blocked' ? 'No useful delivery could be completed' : 'Workflow is terminal'}`);
547
+ const concernCount = state.outcome?.concerns?.length ?? 0;
548
+ if (concernCount) lines.push(`Concerns · ${concernCount} recorded in the result envelope`);
549
+ }
550
+ lines.push('', 'Recent activity');
551
+ if (!liveActions.length) lines.push('· waiting for semantic action events');
552
+ const firstVisibleActionNumber = Math.max(1, totalActions - liveActions.length + 1);
553
+ liveActions.slice(-3).forEach((action, index, visible) => {
554
+ const number = Math.max(firstVisibleActionNumber, totalActions - visible.length + 1) + index;
555
+ lines.push(`#${number} ${statusIcon(action.status, spinnerFrame)} ${friendlyActionKind(action.kind)}${action.summary ? ` · ${friendlyActionSummary(action)}` : ''}`);
556
+ });
557
+ lines.push('', 'Press v for checkpoint prompts, sessions, usage, and artifact paths.');
558
+ return wrapLines(lines, width);
559
+ }
560
+
561
+ lines.push('', 'Technical thread');
562
+ lines.push(`Logical thread · ${orchestrator.attempts.length} checkpoint turn${orchestrator.attempts.length === 1 ? '' : 's'}`);
512
563
  for (const [pool, thread] of conversations) {
513
564
  lines.push(`Session · ${pool} · ${thread.sessionId ?? '—'}${thread.started ? ' · resumable' : ' · pending first turn'}`);
514
565
  }
@@ -551,6 +602,38 @@ function orchestratorDetailLines(model, width, spinnerFrame) {
551
602
  return wrapLines(lines, width);
552
603
  }
553
604
 
605
+ function decisionLabel(decision) {
606
+ return ({
607
+ needs_more_work: 'Continue with bounded work',
608
+ complete: 'Finish and deliver',
609
+ stop: 'Stop with the best useful outcome',
610
+ })[decision] ?? String(decision ?? 'Pending');
611
+ }
612
+
613
+ function sentencePreview(value, maxChars = 420) {
614
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
615
+ const sentences = text.match(/.*?[.!?](?:\s|$)/g)?.slice(0, 2).map((part) => part.trim()).join(' ') ?? text;
616
+ return truncate(sentences || text, maxChars);
617
+ }
618
+
619
+ function friendlyActionKind(kind) {
620
+ return ({
621
+ read_file: 'Read file',
622
+ write_file: 'Write file',
623
+ response: 'Response',
624
+ tool: 'Tool',
625
+ bash: 'Command',
626
+ })[kind] ?? String(kind ?? 'Action').replaceAll('_', ' ');
627
+ }
628
+
629
+ function friendlyActionSummary(action) {
630
+ const summary = String(action.summary ?? '').replace(/\s+/g, ' ').trim();
631
+ if (action.kind === 'response' && /workflow\.decision|"decision"|needs_more_work/.test(summary)) {
632
+ return 'Planner decision recorded';
633
+ }
634
+ return truncate(summary, 180);
635
+ }
636
+
554
637
  function agentDetailLines(model, width, spinnerFrame) {
555
638
  const agent = model.selectedAgent;
556
639
  if (!agent) {
@@ -751,6 +834,7 @@ export async function runDashboard(bullswarmDir, {
751
834
  confirmCancel: false,
752
835
  controlSelected: false,
753
836
  orchestratorDetail: false,
837
+ orchestratorVerbose: false,
754
838
  spinnerFrame: 0,
755
839
  };
756
840
  const paint = () => {
@@ -866,6 +950,7 @@ export async function runDashboard(bullswarmDir, {
866
950
  if (key === '\u001b' || key === 'b') {
867
951
  if (ui.orchestratorDetail) {
868
952
  ui.orchestratorDetail = false;
953
+ ui.orchestratorVerbose = false;
869
954
  ui.focus = 0;
870
955
  ui.controlSelected = true;
871
956
  ui.detailScroll = 0;
@@ -891,6 +976,7 @@ export async function runDashboard(bullswarmDir, {
891
976
  const model = workflowPanelModel(row, { phaseIndex: ui.phaseIndex, agentIndex: ui.agentIndex });
892
977
  if (model.orchestrator.autonomous) {
893
978
  ui.orchestratorDetail = true;
979
+ ui.orchestratorVerbose = false;
894
980
  ui.controlSelected = true;
895
981
  ui.detailScroll = 0;
896
982
  message = null;
@@ -900,6 +986,12 @@ export async function runDashboard(bullswarmDir, {
900
986
  if (key === '1' && detail) { ui.focus = 0; return paint(); }
901
987
  if (key === '2' && detail) { ui.focus = 1; return paint(); }
902
988
  if (key === '3' && detail) { ui.focus = 2; return paint(); }
989
+ if (key === 'v' && ui.orchestratorDetail) {
990
+ ui.orchestratorVerbose = !ui.orchestratorVerbose;
991
+ ui.detailScroll = 0;
992
+ message = null;
993
+ return paint();
994
+ }
903
995
  if (key === '\r' || key === '\n') {
904
996
  if (!detail) {
905
997
  selectedRunId = rows[selected]?.runId ?? selectedRunId;
@@ -909,6 +1001,7 @@ export async function runDashboard(bullswarmDir, {
909
1001
  } else if (ui.focus === 0) {
910
1002
  if (ui.controlSelected) {
911
1003
  ui.orchestratorDetail = true;
1004
+ ui.orchestratorVerbose = false;
912
1005
  ui.detailScroll = 0;
913
1006
  } else ui.focus = 1;
914
1007
  } else if (ui.focus === 1) {