bullswarm 0.16.0 → 0.18.0

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,102 @@
1
1
  # bullswarm changelog
2
2
 
3
+ ## 0.18.0 — exact routes, cheaper plans, clearer results
4
+
5
+ - `workflow goal` can now guarantee an exact planner model and a separate exact
6
+ worker route with `--orchestrator-model`, `--worker-pool`, and
7
+ `--worker-model`. The worker lock is runtime-owned and propagates through the
8
+ scout, ordinary actions, fan-out items, verification repairs, reverification,
9
+ and extraction helpers; unsupported or excluded models fail closed instead
10
+ of silently substituting another model.
11
+ - An action-bearing planner `proceed` is normalized to the schema-equivalent
12
+ `needs_more_work` program before validation. This removes a redundant
13
+ correction turn without changing the proposed graph or weakening any safety
14
+ check (the prior real run spent four frontier planner turns correcting this
15
+ exact representation mismatch).
16
+ - `workflow runs result` now selects the latest successful verifier that
17
+ transitively covers the delivery, so a final suite verifier depending on
18
+ unit verifiers is surfaced ahead of a narrower direct unit check.
19
+ - The same result envelope now adds a backward-compatible `deliveries[]`
20
+ frontier for parallel multi-worker outcomes while preserving the singular
21
+ `delivery` field for existing callers.
22
+ - The autonomous planner now batches cheap homogeneous edits instead of paying
23
+ for a worker and unit verifier per tiny file; substantial independent units
24
+ still fan out and retain focused verification before the final suite.
25
+ - Narrow SSH and phone terminals now open on a full-width workflow timeline;
26
+ `t` toggles between that overview and the phase browser without affecting the
27
+ existing Enter/Esc agent drill-down.
28
+ - Goal-level `--orchestrator <pool>` is now a preference with immediate
29
+ fallback when that pool is quota-gated, ineligible, or unavailable. Exact
30
+ provider testing moves to `--strict-orchestrator <pool>`. Quota waits now
31
+ refresh the durable runner heartbeat at least every 30 seconds, preventing a
32
+ live waiting run from being falsely reconciled as interrupted and making
33
+ cooperative cancellation responsive during long meter-poll intervals.
34
+
35
+ - The runtime now owns the acceptance bar for every verify and re-verify. Each
36
+ verifier's instructions end with a fixed "Acceptance standard (runtime-owned;
37
+ it overrides any stricter rule in the instructions above)": `ok:false` means
38
+ the work is unusable — its acceptance command fails, a required deliverable
39
+ is missing, or the answer is nonsense — and everything else (style, scope,
40
+ cosmetic mismatches, process rules the goal never stated such as append-only,
41
+ files changed by other actions in the shared tree) goes in `concerns` under
42
+ `ok:true`. A re-verify rejects only when the work is still unusable or the
43
+ repair broke the acceptance checks. Direction from the user after `8ebi8a`:
44
+ "unless it is completely nonsense or unable to finish I don't see a reason to
45
+ reject so easily".
46
+ - Repair prompts carry a runtime-owned shared-tree rule: edit only the files
47
+ the reviewed work owns; a concern about other files is not the repair's to
48
+ resolve; never revert, checkout or delete other actions' changes. Earned on
49
+ `8ebi8a`: `verify-docs` rejected on a repo-wide `git diff --stat` scope check
50
+ while siblings were writing, and its repair reverted five `src/` files it did
51
+ not own to satisfy the concern.
52
+ - Planner contract: rule 2 requires exactly one owner per file, including any
53
+ existing test the change breaks (the `workflow-adaptive.test.js:206` gap for
54
+ the fourth time); rule 7 restates the lenient bar above; the validator line
55
+ now says ids are unique across the whole run, finished and failed actions
56
+ included (turn 2 of `8ebi8a` re-proposed the blocked id `verify-suite` and
57
+ spent a 97 s correction turn on it).
58
+ - Goal-4 rerun on `7724da1` (`8ebi8a`, rule 7 + PR #5): 42 min 03 s, three
59
+ planner turns (775 s, 31 %), parallelism 1.34, 23 dispatches (20 on
60
+ `kaihk/gpt-5.6-luna`), three repair rounds each rejected on re-verify for
61
+ reasons the prompts caused, tail of five actions blocked, recovery program
62
+ auto-completed, 315/315, existing tests +179/−1. Goal-4 line:
63
+ 44 → 72 → 37 → 25 → 36 → 42 min.
64
+
65
+ ## 0.17.0 — the timeline tells the execution story
66
+
67
+ - Workflow timeline (PR #5) hardened after a 16-agent adversarial review against
68
+ real run state (23 findings, 21 confirmed): worker rows now name their phase
69
+ (`├─✓ [Verify] verify-impl`) because concurrent phases interleave in time
70
+ order and the tree glyph alone hung a row under the wrong phase; a phase whose
71
+ actions never started (a blocked tail) is shown as `[Phase: X] blocked`
72
+ instead of vanishing; the header line is truncated so widths down to 20
73
+ columns really hold; PgUp now scrolls the timeline to earlier rows (it was a
74
+ dead key at the newest view) and scroll state resets when the pane changes;
75
+ below 100 columns the footer and status line no longer advertise a timeline
76
+ the narrow layout does not render. Confirmed minors left open are listed on
77
+ PR #5.
78
+ - Reworked the autonomous workflow TUI around a human-readable execution story:
79
+ the existing Workflow Planner and phase sidebar now sits beside a timestamped
80
+ timeline of completed preflight, planner, phase, and worker milestones; active
81
+ workers and the waiting/running planner are isolated in a Live section with
82
+ their latest normalized action and stream heartbeat, and future work stays in
83
+ a distinct Next section. `v` keeps raw action-ledger and event evidence one key
84
+ away without mixing it into the default view.
85
+ - Rule 7: a verify checks the goal's own acceptance criteria and never adds a
86
+ process rule the goal does not state (append-only, existing tests
87
+ untouched); when the implementation changes what an existing assertion
88
+ pins, a worker must own updating it. Earned three times on goal 4 (attempt
89
+ 3, `r2vu9i`, `euh622`): the planner wrote "EXTEND BY APPENDING only" into
90
+ the test worker and its verify while the goal said "do NOT modify existing
91
+ tests except to extend them" and item 5 forced `programFeatures` to grow,
92
+ so the assertion at `workflow-adaptive.test.js:206` had no owner, the
93
+ re-verify rejected the mandated extension, and a planner turn recovered.
94
+ - Goal-4 rerun on v0.16.0 (`euh622`): 36 min 00 s, four stage phases in the TUI
95
+ (implement, tests, verify, report) instead of sixteen one-action rows, 22/24
96
+ dispatches on `kaihk/gpt-5.6-luna`, auto-completed, 319/319; two planner
97
+ turns because of the false rejection above (planner turn 2: "an append-only
98
+ rule that the goal itself makes unsatisfiable").
99
+
3
100
  ## 0.16.0 — the planner sets the width; a re-verify judges the repair
4
101
 
5
102
  - A re-verify after a repair round now receives the concerns it raised and the
package/README.md CHANGED
@@ -188,8 +188,23 @@ Resume a process-interrupted autonomous run from its persisted workflow:
188
188
  bullswarm workflow goal --resume <shortId> --json
189
189
  ```
190
190
 
191
- `--orchestrator <pool>` exists for controlled testing; ordinary use should
192
- leave selection on `auto`. `--max-agents` and `--max-workflow-seconds` are
191
+ `--orchestrator <pool>` expresses a preference and immediately falls back to
192
+ another eligible pool if that provider is quota-gated or unavailable. Ordinary
193
+ use can leave selection on `auto`. For controlled provider QA only,
194
+ `--strict-orchestrator <pool>` requires that exact pool and may wait for its
195
+ quota window. Controlled comparisons can additionally pin the exact planner
196
+ and worker routes without changing global strategy:
197
+
198
+ ```bash
199
+ bullswarm workflow goal "Implement and verify the change" --cwd . \
200
+ --strict-orchestrator codex --orchestrator-model gpt-5.6-sol \
201
+ --worker-pool opencode2 --worker-model kaihk/gpt-5.6-luna
202
+ ```
203
+
204
+ The worker lock covers the scout, ordinary runs, fan-out items, repairs,
205
+ re-verification, and runtime extraction helpers. A pool that cannot guarantee
206
+ the requested model is ineligible rather than silently substituting another
207
+ model. `--max-agents` and `--max-workflow-seconds` are
193
208
  advisory planning targets; `--max-expansion-rounds` is also an advisory
194
209
  convergence target. Hard structural safeguards are adjusted with
195
210
  `--max-actions` and `--max-items-per-expansion`.
@@ -258,8 +273,9 @@ Values accept ISO timestamps, local `YYYY-MM-DD` dates, `today`, `yesterday`,
258
273
  After a workflow reaches a terminal state, agents should consume
259
274
  `workflow runs result <id> --json` instead of probing `state.json`, task files,
260
275
  or provider-specific output. The versioned `bullswarm.workflow.result.v1`
261
- envelope identifies the final delivery artifact and its matching verification
262
- verdict, and includes progress, step logs, tokens, and an explicitly
276
+ envelope retains the primary `delivery`, adds a `deliveries[]` frontier when
277
+ parallel workers jointly form the outcome, and identifies their strongest
278
+ matching verification verdict. It also includes progress, step logs, tokens, and an explicitly
263
279
  complete-or-partial tool-call total. `runs show` remains the low-level debugging
264
280
  surface.
265
281
  Goal launch output includes an `instructions` handoff with four named paths:
@@ -289,21 +305,27 @@ bullswarm workflow watch <shortId> --verbose # detailed agent/action view
289
305
  ```
290
306
 
291
307
  `workflow tui` is the interactive, Claude-style `/workflows` view. For an
292
- autonomous goal its left navigation stacks a compact Orchestrator panel above
293
- the Phases panel; internal planner turns never appear as workers or phases.
294
- Select Orchestration and press Enter, or press `o`
295
- anywhere, to open a summary-first orchestration overview: what it is doing now,
308
+ autonomous goal its left navigation stacks a compact Workflow Planner panel
309
+ above the Phases panel; internal planner turns never appear as workers or phases.
310
+ The default desktop main panel is a timestamped workflow timeline: completed
311
+ preflight, planner-checkpoint, phase-transition, and worker-result events stay
312
+ above a live section containing the waiting/running Workflow Planner and workers,
313
+ each with its latest semantic action and stream heartbeat. Planned work is kept
314
+ in a separate Next section so it cannot be mistaken for execution evidence.
315
+ Select Workflow Planner and press Enter, or press `o`
316
+ anywhere, to open a summary-first planner overview: what it is doing now,
296
317
  its latest decision in plain language, why it chose that path, what happens
297
- next, progress, and the last three semantic actions. Press `v` to toggle the
298
- technical view with provider session, every checkpoint turn, usage, prompt,
299
- and artifact paths. Status marks are consistent throughout the tree: `○` not started,
318
+ next, progress, and the last three semantic actions. Press `v` from the timeline
319
+ for workflow technical state, or from Workflow Planner for provider session,
320
+ every checkpoint turn, usage, prompt, and artifact paths. Status marks are consistent throughout the tree: `○` not started,
300
321
  an animated Braille spinner for active work, `⧖` waiting, `✓` finished, and
301
322
  `✗` failed or interrupted. The non-emoji `⧖` avoids the inconsistent cell
302
323
  width of `⌛` across terminal fonts. It watches ongoing runs from disk and supports `j`/`k` or arrow-key selection, Enter for
303
324
  details, Esc to go back, `c` to request a confirmed cooperative stop, `r` to
304
325
  refresh, and `q` to detach. Its responsive drill-down fits both desktop and
305
326
  mobile SSH terminals without squeezing phase, agent, and activity into three
306
- narrow columns.
327
+ narrow columns. Below 100 columns it opens on a full-width timeline; press `t`
328
+ to toggle Timeline and Phases, then use Enter/Esc for agents and activity.
307
329
 
308
330
  ```bash
309
331
  bullswarm workflow tui
@@ -431,7 +453,7 @@ appending anything. It executes ready actions, observes their durable results,
431
453
  and calls the planner again. `events.jsonl`, `state.json`, the TUI, and JSON
432
454
  inspection expose the same plan, actions, attempts, decisions, budgets, and
433
455
  artifacts. See `workflows/adaptive-code-review.json` for a complete example.
434
- Planner actions cannot set `pool`, `addDir`, or `taskFile`. If those need to be
456
+ Planner actions cannot set `pool`, `model`, `addDir`, or `taskFile`. If those need to be
435
457
  fixed by the initiator, declare them under the `decide` step's `actionDefaults`;
436
458
  otherwise eligible capable pools are ranked by live quota surplus.
437
459
 
@@ -307,3 +307,85 @@ is met: each round fixed a defect the deliverable needed fixed. Prediction befor
307
307
  rounds and the 10-min recovery turn vanish, wall ≈ 30 min"; observed 25 min 13 s with one planner turn.
308
308
 
309
309
  Cost: 22 of 23 dispatches on the unmetered opencode2 seat; Claude quota spent on one 247 s planner turn.
310
+
311
+ ## Goal-4 rerun on v0.16.0 (phase = stage) — `euh622` (wf-mtej85ws-18a3c0), 15:25:14 → 16:01:14 Z — **36 min 00 s, auto-completed; one planner-authored false rejection cost the recovery turn**
312
+
313
+ Runtime `4bfd7f4` = released v0.16.0 (rule 3 "a phase is a pipeline stage … never one per action"); workers pinned to
314
+ `opencode2`/`kaihk/gpt-5.6-luna` (cleared after); orchestrator `claude-code`/`claude-opus-5`; fresh fixture `g4-bs-v5`.
315
+
316
+ | metric | `r2vu9i` | `bizp4s` | **`euh622`** |
317
+ | --- | ---: | ---: | ---: |
318
+ | wall | 36 min 58 s | 25 min 13 s | 36 min 00 s (2 157 s) |
319
+ | planner turns / plannerSec | 2 / 648 s | 1 / 247 s | 2 / 728 s (34 %) — 462 s + 266 s |
320
+ | dispatches | 26 | 23 | 24 (22 luna + 2 opus) |
321
+ | max concurrent / parallelism | 4 / 1.55 | 4 / 1.77 | 4 / 1.5 |
322
+ | phases in the TUI | 19 one-action rows | 16 one-action rows | **4 stages** (implement 2, tests 3, verify 6 + repairs, report 2) + 2 recovery phases |
323
+ | repairs (rounds / repaired ok / re-verify rejected) | 4 / 2 / 2 | 3 / 2 / 1 | 4 / 2 / 2 |
324
+ | tests after | 314 | 319 | **319/319**; existing tests +116/−1 (the mandated `:206` extension) and +153/−0 |
325
+
326
+ What the phase change did: the planner wrote `implement` (impl ∥ docs), `tests` (three test writers), `verify` (six
327
+ verifies), `report` — the layout asked for, with no scheduling change (impl ∥ docs started together; three test writers
328
+ and verify-impl started the second impl landed; verify-docs ran while impl was still running). Width was one docs
329
+ worker (three files merged; off the critical path) but three test writers — comparable to `bizp4s`.
330
+
331
+ The four repair rounds:
332
+ - `verify-impl` r1: fan-out items dispatched through plain `dispatch()`, so `stepTemplate.outputSchema` was never applied
333
+ — real. Re-verify rejected: the repair tested `step.outputSchema` instead of `itemStep.outputSchema` — the listed
334
+ concern still unresolved, exactly the rejection the re-verify rule permits. r2 repaired; re-verify ok.
335
+ - `verify-test-runtime` r1: the retry case did not assert the `errors` payload of `action.output_schema_retry` — real.
336
+ - `verify-test-refs` r1: the acceptance command failed on the pre-existing assertion at `workflow-adaptive.test.js:206`
337
+ (`programFeatures` pinned to three entries) because goal item 5 mandates adding `outputSchema` to it. The repair
338
+ extended the assertion (+116/−1). Re-verify rejected BECAUSE an existing assertion changed — the planner had written
339
+ "EXTEND BY APPENDING new test cases only" into `test-refs` and "shows APPENDED cases only" into `verify-test-refs`,
340
+ and "Do NOT modify existing tests" into `impl`, while the goal says "do NOT modify existing tests except to extend
341
+ them". No worker owned the assertion; the verifier treated its prompt's rule as an unresolved concern. `verify-suite`,
342
+ `report`, `verify-report` blocked → planner turn 2 (266 s), whose reason is exact: "verify-test-refs ended ok:false
343
+ on an append-only rule that the goal itself makes unsatisfiable — goal item 5 mandates adding 'outputSchema' to
344
+ programFeatures". Recovery program `verify-suite-full` → `final-report` → `verify-final-report`, auto-completed.
345
+ Third occurrence of this shape (attempt 3, `r2vu9i`, here); `bizp4s` avoided it only because its implementation
346
+ happened to keep the old assertion true.
347
+
348
+ Cost of the false rejection: the 266 s planner turn plus the serialised tail ≈ 5–6 min; the rest of the gap to
349
+ `bizp4s` is variance (planner turn 1 462 s vs 247 s on the same contract; `impl` 445 s vs 377 s).
350
+
351
+ Fix committed after the run, unreleased (`71960ae`): rule 7 — "A verify checks the goal's own acceptance criteria …
352
+ never add a process rule the goal does not state (append-only, tests untouched); when the implementation changes what
353
+ an existing assertion pins, a worker must own updating it." Proof pending a rerun on that commit.
354
+
355
+ ## Run `8ebi8a` — runtime `7724da1` (rule 7 `71960ae` + PR #5 merge), luna pinned, fixture g4-bs-v6
356
+
357
+ Launched 2026-08-29 16:42 Z as the live proof of rule 7. Result: **42 min 03 s**, worse than `euh622` (36 min) and
358
+ `bizp4s` (25 min). Measured (`bs-g4-v6-metrics.json`): 3 planner turns / 775 s (31 % of wall; turn 2 430 s, correction
359
+ turn 97 s, turn 1 ≈ 248 s derived), parallelism 1.34, max 3 concurrent, 23 dispatches (20 workers on
360
+ `kaihk/gpt-5.6-luna`, 3 planner turns on claude-code/opus), 3 repair rounds — every re-verify rejected — 1 validator
361
+ correction, auto-completed by `program-completion`, `npm test` 315/315, existing tests +179/−1 (the mandated `:206`
362
+ extension, finally done by a named action `fix-pinned-test`).
363
+
364
+ The three rejections were each legitimate under the re-verify rule of `9af8fdf`; the defect was in the prompts the
365
+ planner wrote, and rule 7 did not stop it:
366
+ - `verify-impl` r1: real concern. Repair 1 removed `outputSchema` from `programFeatures` so the OLD assertion at
367
+ `workflow-adaptive.test.js:206` would pass — because `impl` was told "do NOT modify existing tests", `tests-runtime`
368
+ (owner of that file) was never told to extend `:206`, and `verify-impl` expected `impl` to have done it. Re-verify
369
+ rejected (a regression: item 5 mandates the entry). r2 re-added it; the old assertion failed again; rejected.
370
+ Turn 2's reason names it: "my round-1 prompt asked for it". Nobody owned the assertion — the fourth run with this gap.
371
+ - `verify-docs` r1: its prompt said "only those three doc files were changed by this worker (`git diff --stat`)"; the
372
+ repo-wide diff showed `impl`'s files, so it rejected on scope. The repair, told "Do not touch src/", still reverted
373
+ five `src/` files to make `git diff --name-only` show three files (its report: "git diff --name-only reports exactly
374
+ the three requested documentation files… 299 tests"). Re-verify rejected on the missing implementation.
375
+ - Tail blocked: `verify-tests-schema`, `verify-tests-runtime`, `verify-suite`, `report`, `verify-report` depended on
376
+ `verify-impl` (a verdict, chosen so repairs would not edit the same files) → `failed_terminal` → planner turn 2,
377
+ which re-proposed the blocked id `verify-suite` → validator rejection → 97 s correction → recovery program
378
+ `restore-src` → `fix-pinned-test` → `verify-src` / `verify-tests` → `verify-full-suite` → `final-report` →
379
+ `verify-final-report`, all ok.
380
+
381
+ Where the extra time went (vs `bizp4s`): ≈ 17 min in the two failed verify loops, the blocked tail, turn 2 and the
382
+ correction; `impl` 493 s vs 377 s is variance.
383
+
384
+ Conclusion and fix (unreleased, committed after the run): three runs in a row failed on a different planner-authored
385
+ constraint the goal never stated (append-only → contradictory ownership → repo-wide scope check), so contract text
386
+ alone is whack-a-mole. The runtime now owns the bar: every verify/re-verify instruction ends with a fixed acceptance
387
+ standard (ok:false = unusable; everything else is a concern under ok:true; other actions' files are never this unit's
388
+ defect), every repair prompt says to edit only the reviewed work's files and never revert others' changes, rule 2
389
+ requires one owner per file including an existing test the change breaks, and the validator line states run-wide id
390
+ uniqueness. Direction from the user: "unless it is completely nonsense or unable to finish I don't see a reason to
391
+ reject so easily". Claim to test on the next rerun: none of the three `8ebi8a` rejection reasons can produce ok:false.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bullswarm",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
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
@@ -130,8 +130,15 @@ terminal-owned execution.
130
130
  The detached runner does not depend on the initiating CLI remaining alive.
131
131
  Resume a process-interrupted run from its persisted definition with
132
132
  `bullswarm workflow goal --resume <shortId> --json`. Leave orchestrator
133
- selection automatic in normal use; `--orchestrator=<pool>` is for controlled
134
- provider QA. `SIGTERM`/`SIGINT` cooperatively terminate the active delegate and
133
+ selection automatic in normal use. `--orchestrator=<pool>` is a preference
134
+ that falls back when the pool is quota-gated or unavailable;
135
+ `--strict-orchestrator=<pool>` is the exact-provider control for QA and may
136
+ wait for that pool's quota window. For a controlled model comparison, add
137
+ `--orchestrator-model=<model>`, `--worker-pool=<pool>`, and
138
+ `--worker-model=<model>`. The worker constraints cover scout, runs, fan-out
139
+ items, repairs, re-verification, and extraction helpers; a pool that cannot
140
+ guarantee the model is excluded rather than silently substituting it.
141
+ `SIGTERM`/`SIGINT` cooperatively terminate the active delegate and
135
142
  persist `interrupted`; later workflow commands also reconcile dead or stale
136
143
  owners into that explicit resumable state.
137
144
 
@@ -223,8 +230,9 @@ ISO timestamps, local dates, today/yesterday/tomorrow/now, or durations such as
223
230
  normal ongoing-only scope.
224
231
 
225
232
  When the run is terminal, use `workflow runs result <id> --json` as the
226
- handoff contract. Its versioned result envelope points to the selected delivery
227
- artifact, the dependent verification verdict, progress, and usage. Do not guess
233
+ handoff contract. Its versioned result envelope keeps one primary `delivery`,
234
+ adds every jointly delivered parallel artifact under `deliveries[]`, and points
235
+ to the strongest dependent verification verdict, progress, and usage. Do not guess
228
236
  the output schema by scraping task files or assume the last provider response is
229
237
  the deliverable; `runs show` is for low-level debugging.
230
238
 
@@ -277,12 +285,18 @@ supports explicit model selection, Bullswarm pins an allowed model in the same
277
285
  effort tier; otherwise that pool is excluded because its implicit default
278
286
  cannot be guaranteed. Restore eligibility with `strategy include-model`.
279
287
 
280
- In the human TUI, the autonomous orchestrator has a compact selectable panel
281
- stacked above the phase tree. Select it and press Enter, or press
288
+ In the human TUI, the autonomous orchestrator is presented as Workflow Planner
289
+ in a compact selectable panel stacked above the phase tree. The default desktop
290
+ view pairs that unchanged navigation with a timestamped workflow timeline:
291
+ finished events stay above a live Planner/worker section with each participant's
292
+ latest semantic action, while unexecuted work stays in a separate Next section.
293
+ Select Workflow Planner and press Enter, or press
282
294
  `o`, to see a summary-first overview of its current role, latest decision,
283
295
  reason, next action, progress, and recent semantic activity. Press `v` for the
284
296
  durable provider session, checkpoint prompts and turns, usage, and artifact
285
297
  paths; `v` returns to the overview and Esc returns to phases.
298
+ Below 100 columns the TUI opens on a full-width timeline; `t` toggles Timeline
299
+ and Phases, and Enter/Esc continues through agents and activity.
286
300
  The shared state marks are `○` not started, animated Braille spinner active,
287
301
  `⧖` waiting, `✓` finished, and `✗` failed or interrupted.
288
302
 
@@ -400,7 +414,7 @@ orchestrator for the rest of the run and tries one other eligible pool
400
414
  `completed_with_concerns` with a ready best-effort artifact when useful work
401
415
  exists, or `blocked` when it does not; neither hides failed verification.
402
416
  Use `workflows/adaptive-code-review.json` as the starting template.
403
- Planner proposals cannot choose `pool`, `addDir`, or `taskFile`. Those fields
417
+ Planner proposals cannot choose `pool`, `model`, `addDir`, or `taskFile`. Those fields
404
418
  are runtime-owned. An initiator may constrain them with a decide step's
405
419
  `actionDefaults`; absent a pinned default, normal capability and quota routing
406
420
  selects the worker.
package/src/help.js CHANGED
@@ -581,7 +581,11 @@ const workflowGoalText = rich({
581
581
  { flag: '--watch', desc: 'immediately follow low-noise progress until terminal; only valid for a new human-readable independent launch — cannot combine with --detach, --foreground, --json, --resume, or --request', default: 'off' },
582
582
  { flag: '--foreground', desc: 'keep execution attached to this terminal instead of detaching', default: 'off (detaches into a background process)' },
583
583
  { flag: '--json', desc: 'print the launch/report document as JSON', default: 'human-readable launch instructions' },
584
- { flag: '--orchestrator <pool|auto>', desc: 'pin the orchestrator pool for a new goal, or override it when combined with --resume', default: 'auto (capability- and quota-based selection)' },
584
+ { flag: '--orchestrator <pool|auto>', desc: 'prefer this orchestrator pool for a new goal or resumed run, falling back immediately when it is quota-gated, ineligible, or unavailable', default: 'auto (capability- and quota-based selection)' },
585
+ { flag: '--strict-orchestrator <pool>', desc: 'require exactly this orchestrator pool for controlled provider QA; waits when that pool is quota-gated instead of falling back; mutually exclusive with --orchestrator', default: 'off' },
586
+ { flag: '--orchestrator-model <model|auto>', desc: 'pin the exact model used by the autonomous planner; only pools that can guarantee this model remain eligible', default: 'auto (effort-tier strategy or connector default)' },
587
+ { flag: '--worker-pool <pool|auto>', desc: 'pin every non-planner dispatch, including scout, fan-out items, repairs, and verifiers, to one pool', default: 'auto (normal routing)' },
588
+ { flag: '--worker-model <model|auto>', desc: 'pin the exact model for every non-planner dispatch; only pools that can guarantee it remain eligible', default: 'auto (effort-tier strategy or connector default)' },
585
589
  { flag: '--max-agents <n>', desc: 'planning target for total dispatched agents (soft, not a hard stop)', default: '30 (max 500)' },
586
590
  { flag: '--max-expansion-rounds <n>', desc: 'planning target for planner replanning rounds', default: '8 (max 50)' },
587
591
  { flag: '--max-actions <n>', desc: 'planning target for total dispatched actions', default: '40 (max 1000)' },
@@ -605,6 +609,7 @@ const workflowGoalText = rich({
605
609
  ],
606
610
  examples: [
607
611
  { cmd: 'bullswarm workflow goal "Audit this repo for TODOs and file a one-page summary" --cwd .' },
612
+ { cmd: 'bullswarm workflow goal "Implement and verify the change" --cwd . --strict-orchestrator codex --orchestrator-model gpt-5.6-sol --worker-pool opencode2 --worker-model kaihk/gpt-5.6-luna', note: 'controlled Sol-planner/Luna-worker run' },
608
613
  ],
609
614
  next: 'bullswarm workflow watch <shortId> to follow progress, or bullswarm workflow tui for the interactive browser.',
610
615
  });
@@ -660,7 +665,7 @@ const workflowCapabilitiesText = rich({
660
665
  options: [{ flag: '--json', desc: 'accepted for consistency with other commands, but has no effect', default: 'output is always JSON regardless of this flag' }],
661
666
  safety: ['read-only — performs live pool discovery to populate pool/meter state; nothing is written'],
662
667
  examples: [{ cmd: 'bullswarm workflow capabilities' }],
663
- next: 'bullswarm workflow goal "<goal>" --orchestrator <pool> to pin one of the reported pools, or bullswarm strategy show to review model tier assignments.',
668
+ next: 'bullswarm workflow goal "<goal>" --orchestrator <pool> to prefer one of the reported pools with fallback, or bullswarm strategy show to review model tier assignments.',
664
669
  });
665
670
 
666
671
  const workflowInspectText = rich({
@@ -676,8 +681,9 @@ const workflowInspectText = rich({
676
681
 
677
682
  const workflowTuiText = rich({
678
683
  usage: 'bullswarm workflow tui [<runId>] [--json] [--all] [--show <runId>] [--cancel <runId>]',
679
- purpose: 'Open the interactive full-screen phase agent step/detail browser for ongoing '
680
- + 'and historical runs, or print a static/JSON snapshot for a non-interactive caller.',
684
+ purpose: 'Open the interactive full-screen workflow timeline with Workflow Planner, phase, '
685
+ + 'live-agent, and technical drill-down views for ongoing and historical runs, or print a '
686
+ + 'static/JSON snapshot for a non-interactive caller.',
681
687
  args: [{ name: '[<runId>]', desc: 'shortId or runId to open directly in detail view; omit to see the run picker' }],
682
688
  options: [
683
689
  { flag: '--json', desc: "print a JSON snapshot instead of opening the interactive browser (list of ongoing runs, or one run's state/report/events when a runId is given)", default: "opens the interactive browser on a TTY; without a TTY, a given runId instead prints one static text detail tree" },
@@ -689,6 +695,8 @@ const workflowTuiText = rich({
689
695
  'interactive mode and the --json/--show/--all views are read-only',
690
696
  '--cancel writes state.json (cancelRequested=true, status=cancelling) — cooperative, not a force-kill: the workflow stops at its next safe checkpoint',
691
697
  'inside the interactive browser, q detaches without stopping the underlying workflow; c requests the same cancellation with a confirmation prompt',
698
+ 'the default timeline is derived from durable state and events; press v for raw action-ledger and event evidence',
699
+ 'below 100 columns the timeline remains full-width; press t to toggle Timeline and Phases, then Enter/Esc to drill into agents and activity',
692
700
  ],
693
701
  examples: [
694
702
  { cmd: 'bullswarm workflow tui', note: 'interactive run picker' },
@@ -867,8 +875,8 @@ const workflowRunsShowText = rich({
867
875
 
868
876
  const workflowRunsResultText = rich({
869
877
  usage: 'bullswarm workflow runs result <shortId|runId> [--json]',
870
- purpose: 'Print the stable, caller-facing delivery content, verification verdict, progress, '
871
- + 'and usage envelope for one run — the intended integration point for scripts and agents.',
878
+ purpose: 'Print the stable caller envelope: primary delivery, parallel deliveries[] frontier, '
879
+ + 'strongest verification verdict, progress, and usage for one run — the intended integration point for scripts and agents.',
872
880
  args: [{ name: '<shortId|runId>', desc: 'run identifier' }],
873
881
  options: [{ flag: '--json', desc: 'print the full result document as JSON', default: 'human-readable summary (delivery preview truncated to 64KB)' }],
874
882
  safety: ['read-only'],
@@ -295,21 +295,83 @@ export function shouldAutoWatchGoal(opts) {
295
295
  opts.json !== true && opts.resume == null && opts.request == null;
296
296
  }
297
297
 
298
- export function applyResumeOrchestratorOverride(doc, requested) {
299
- if (!requested) return doc;
300
- const pool = requested === 'auto' ? null : requested;
298
+ export function applyResumeOrchestratorOverride(doc, requested, strictRequested = null) {
299
+ if (!requested && !strictRequested) return doc;
300
+ if (requested && strictRequested) {
301
+ throw new Error('--orchestrator and --strict-orchestrator are mutually exclusive');
302
+ }
303
+ const strict = Boolean(strictRequested);
304
+ const selected = strictRequested ?? requested;
305
+ const pool = selected === 'auto' ? null : selected;
301
306
  doc.intent ??= {};
302
307
  doc.orchestration ??= {};
303
308
  doc.intent.requestedOrchestrator = pool ?? 'auto';
304
309
  doc.orchestration.requestedPool = pool;
310
+ doc.orchestration.strictPool = strict ? pool : null;
305
311
  doc.orchestration.selection = pool
306
- ? 'user-pinned-for-testing'
312
+ ? (strict ? 'user-strict-for-testing' : 'user-preferred-with-fallback')
307
313
  : 'capability-strategy-and-quota';
308
314
  for (const phase of doc.phases ?? []) {
309
315
  for (const step of phase.steps ?? []) {
310
316
  if (step.type !== 'decide') continue;
311
- if (pool) step.pool = pool;
312
- else delete step.pool;
317
+ delete step.pool;
318
+ delete step.preferredPool;
319
+ if (pool) step[strict ? 'pool' : 'preferredPool'] = pool;
320
+ }
321
+ }
322
+ return doc;
323
+ }
324
+
325
+ export function applyResumeModelOverrides(doc, {
326
+ orchestratorModel = null,
327
+ workerPool = null,
328
+ workerModel = null,
329
+ } = {}) {
330
+ const normalize = (value) => value === 'auto' ? null : value;
331
+ const plannerModel = normalize(orchestratorModel);
332
+ const workers = normalize(workerPool);
333
+ const workerModelLock = normalize(workerModel);
334
+ if (orchestratorModel == null && workerPool == null && workerModel == null) return doc;
335
+ doc.intent ??= {};
336
+ doc.orchestration ??= {};
337
+ if (orchestratorModel != null) {
338
+ doc.intent.requestedOrchestratorModel = plannerModel ?? 'auto';
339
+ doc.orchestration.requestedModel = plannerModel;
340
+ }
341
+ if (workerPool != null) {
342
+ doc.intent.requestedWorkerPool = workers ?? 'auto';
343
+ doc.orchestration.workerPool = workers;
344
+ }
345
+ if (workerModel != null) {
346
+ doc.intent.requestedWorkerModel = workerModelLock ?? 'auto';
347
+ doc.orchestration.workerModel = workerModelLock;
348
+ }
349
+ for (const phase of doc.phases ?? []) {
350
+ for (const step of phase.steps ?? []) {
351
+ if (step.type === 'decide') {
352
+ step.actionDefaults ??= {};
353
+ if (orchestratorModel != null) {
354
+ if (plannerModel) step.model = plannerModel;
355
+ else delete step.model;
356
+ }
357
+ if (workerPool != null) {
358
+ if (workers) step.actionDefaults.pool = workers;
359
+ else delete step.actionDefaults.pool;
360
+ }
361
+ if (workerModel != null) {
362
+ if (workerModelLock) step.actionDefaults.model = workerModelLock;
363
+ else delete step.actionDefaults.model;
364
+ }
365
+ continue;
366
+ }
367
+ if (workerPool != null) {
368
+ if (workers) step.pool = workers;
369
+ else delete step.pool;
370
+ }
371
+ if (workerModel != null) {
372
+ if (workerModelLock) step.model = workerModelLock;
373
+ else delete step.model;
374
+ }
313
375
  }
314
376
  }
315
377
  return doc;
@@ -324,6 +386,10 @@ async function wfGoal(opts) {
324
386
  console.error('✗ --watch is only valid for a new human-readable independent launch; do not combine it with --detach, --foreground, --json, --resume, or --request');
325
387
  return 2;
326
388
  }
389
+ if (opts.orchestrator && opts['strict-orchestrator']) {
390
+ console.error('✗ --orchestrator and --strict-orchestrator are mutually exclusive');
391
+ return 2;
392
+ }
327
393
  const { names, pools } = await livePoolNames();
328
394
  let doc;
329
395
  let resumeRunId = null;
@@ -345,7 +411,17 @@ async function wfGoal(opts) {
345
411
  console.error(`✗ cannot load durable workflow for ${resumeRunId}: ${err.message}`);
346
412
  return 1;
347
413
  }
348
- applyResumeOrchestratorOverride(doc, opts.orchestrator);
414
+ try {
415
+ applyResumeOrchestratorOverride(doc, opts.orchestrator, opts['strict-orchestrator']);
416
+ applyResumeModelOverrides(doc, {
417
+ orchestratorModel: opts['orchestrator-model'],
418
+ workerPool: opts['worker-pool'],
419
+ workerModel: opts['worker-model'],
420
+ });
421
+ } catch (err) {
422
+ console.error(`✗ invalid goal options: ${err.message}`);
423
+ return 2;
424
+ }
349
425
  } else if (opts.request) {
350
426
  try {
351
427
  const request = JSON.parse(readFileSync(resolve(opts.request), 'utf8'));
@@ -364,13 +440,24 @@ async function wfGoal(opts) {
364
440
  console.error(goalUsage());
365
441
  return 2;
366
442
  }
367
- const orchestrator = opts.orchestrator && opts.orchestrator !== 'auto'
368
- ? opts.orchestrator : null;
443
+ const requestedOrchestrator = opts['strict-orchestrator'] ?? opts.orchestrator;
444
+ const orchestrator = requestedOrchestrator && requestedOrchestrator !== 'auto'
445
+ ? requestedOrchestrator : null;
446
+ const workerPool = opts['worker-pool'] && opts['worker-pool'] !== 'auto'
447
+ ? opts['worker-pool'] : null;
448
+ const workerModel = opts['worker-model'] && opts['worker-model'] !== 'auto'
449
+ ? opts['worker-model'] : null;
450
+ const orchestratorModel = opts['orchestrator-model'] && opts['orchestrator-model'] !== 'auto'
451
+ ? opts['orchestrator-model'] : null;
369
452
  try {
370
453
  doc = buildGoalWorkflow({
371
454
  goal,
372
455
  cwd: opts.cwd ?? process.cwd(),
373
456
  orchestrator,
457
+ strictOrchestrator: Boolean(opts['strict-orchestrator']),
458
+ orchestratorModel,
459
+ workerPool,
460
+ workerModel,
374
461
  settings: goalSettings(opts),
375
462
  scout: !opts.noScout,
376
463
  worktreeIsolation: loadState(BULLSWARM_DIR()).config?.worktreeIsolation ?? 'agent-decides',
@@ -616,7 +703,8 @@ async function wfInspect(opts) {
616
703
  function parseFlags(argv) {
617
704
  const out = { inputs: {}, rest: [] };
618
705
  const valueFlags = new Set([
619
- 'resume', 'after', 'cwd', 'orchestrator', 'request', 'run-id',
706
+ 'resume', 'after', 'cwd', 'orchestrator', 'strict-orchestrator', 'orchestrator-model',
707
+ 'worker-pool', 'worker-model', 'request', 'run-id',
620
708
  'max-agents', 'max-expansion-rounds', 'max-actions',
621
709
  'max-items-per-expansion', 'max-workflow-seconds', 'concurrency',
622
710
  'retry-attempts', 'interval', 'heartbeat', 'message',