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 +1 -1
- package/CHANGELOG.md +23 -0
- package/README.md +5 -3
- package/connectors/echo.json +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +7 -5
- package/src/cli.js +12 -7
- package/src/lib/config.js +2 -1
- package/src/setup.js +45 -7
- package/src/workflow/dashboard.js +99 -6
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 #
|
|
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
|
|
280
|
-
|
|
281
|
-
|
|
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/connectors/echo.json
CHANGED
|
@@ -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
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
|
|
267
|
-
|
|
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
|
|
357
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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…)
|
|
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:
|
|
381
|
-
detail: `enabled pools: ${
|
|
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
|
-
|
|
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
|
|
318
|
-
|
|
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,
|
|
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
|
-
:
|
|
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 ?
|
|
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(
|
|
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
|
-
|
|
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) {
|