bullswarm 0.10.6 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
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
+
3
12
  ## 0.10.6 — safe setup and boolean flags
4
13
 
5
14
  - Marked the deterministic `echo` connector as a test fixture and excluded it
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bullswarm",
3
- "version": "0.10.6",
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
 
@@ -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) {