bullswarm 0.10.3 → 0.10.4

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 # 235 tests, no network needed (meters read from cache)
40
+ npm test # 256 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,55 @@
1
1
  # bullswarm changelog
2
2
 
3
+ ## 0.10.4 — durable interactive workflow viewer
4
+
5
+ - Added `workflow runs result <id> [--json]`, a stable
6
+ `bullswarm.workflow.result.v1` handoff for parent agents. It selects the final
7
+ successful delivery rather than the last orchestrator response, pairs it with
8
+ its dependent verification verdict, and reports progress, step logs, token
9
+ usage, and honest complete-or-partial tool-call counts.
10
+ - Goal launches now return and print a four-part operating handoff for agentic
11
+ inspection, low-noise watching, the human TUI, and terminal result retrieval.
12
+ - Plain `workflow goal` now launches independently, prints that handoff, and
13
+ returns. The new explicit `--watch` flag follows low-noise progress until the
14
+ terminal state; the human TUI is opened from the printed command.
15
+ - `--foreground` retains terminal-owned execution and `--detach` remains an
16
+ explicit backward-compatible spelling of the new default.
17
+ - Rebuilt interactive workflow inspection as a responsive Phase → Agent →
18
+ Agent-activity browser: desktop uses two contextual panes while mobile and
19
+ narrow SSH terminals use one full-width level. It includes arrow/Enter/Esc
20
+ navigation, numbered semantic actions, active-agent following, scrolling,
21
+ completed-agent outcomes, total semantic-action counts, terminal agent
22
+ progress, resize handling, safe detach, and confirmed stopping.
23
+ - Made the autonomous orchestrator a compact selectable control-plane panel
24
+ stacked above the phase tree. Arrow/Enter or `o` opens its
25
+ durable session, checkpoint decisions, semantic
26
+ activity, usage, prompt, outcomes, and artifacts without counting planner
27
+ turns as phase workers.
28
+ - Standardized workflow TUI state marks across orchestrator, phases, agents,
29
+ and semantic activity: `○` pending, animated Braille spinner active, `⧖`
30
+ waiting, `✓` complete, and `✗` failed or interrupted.
31
+ - Made `maxExpansionRounds` an advisory convergence target instead of a hard
32
+ failure boundary. Near the target the orchestrator is told to consolidate
33
+ existing evidence and avoid marginal expansion; essential bounded work may
34
+ exceed it and the overage is recorded.
35
+ - Added truthful qualified terminal outcomes: planner `stop` now yields
36
+ `completed_with_concerns` with a ready best-effort delivery when useful work
37
+ exists, or `blocked` when it does not. Result envelopes expose `verified`,
38
+ the stopping reason, and unresolved concerns without relabeling failed
39
+ verification as success.
40
+ - Made phase and agent status derive from semantic output verdicts, so a
41
+ verifier process that successfully returns `ok:false` is displayed as a
42
+ failed verification rather than a completed check.
43
+ - Added persisted `strategy exclude-model` / `include-model` policy. Excluded
44
+ models are removed from strategy recommendations and dispatch assignments;
45
+ connectors pin an allowed same-tier fallback or become ineligible when they
46
+ cannot guarantee the exclusion.
47
+ - Let the CLI process drain stdout before exiting, preventing large
48
+ `workflow tui --json` snapshots from being truncated around the platform
49
+ pipe-buffer boundary.
50
+ - Expanded `workflow --help` into an operational map for building, observing,
51
+ controlling, and auditing workflows.
52
+
3
53
  ## 0.10.3 — contextual help everywhere
4
54
 
5
55
  - Added side-effect-free `-h` / `--help` handling for the top-level CLI and
package/README.md CHANGED
@@ -14,6 +14,15 @@ bullswarm workflow run --help
14
14
  bullswarm workflow draft step add --help
15
15
  ```
16
16
 
17
+ `workflow goal` launches a durable background runner, prints operating commands,
18
+ and returns by default. Add `--watch` to immediately follow low-noise progress
19
+ until terminal, or `--foreground` to keep execution owned by the initiating
20
+ terminal. Open the full-screen Phase → Agent → Activity viewer at any time with
21
+ `bullswarm workflow tui <shortId>`; use Up/Down, Enter, Esc, and `q` to detach.
22
+ Wide terminals use two
23
+ contextual panes (phases + agents, then agents + activity); narrow and mobile
24
+ terminals give each level the full screen automatically.
25
+
17
26
  ## The doctrine (non-negotiable)
18
27
 
19
28
  1. **Judge by CONTENT, not exit code.** Every delegate CLI can exit 0 while
@@ -88,6 +97,7 @@ bullswarm strategy auto status
88
97
  bullswarm strategy set-subscription command-code \
89
98
  --plan GOAT --monthly-usd 10 --included-usd 70 --quota-window monthly
90
99
  bullswarm strategy assign high --pool claude-code --model claude-opus-4-6
100
+ bullswarm strategy exclude-model claude-fable-5
91
101
  bullswarm run --effort high --lane analyze --task-file /tmp/task.md --json
92
102
  ```
93
103
 
@@ -103,6 +113,12 @@ prices, and benchmarks stay `null` rather than being guessed. An assignment is
103
113
  only a preference: quarantine, exhaustion, burst gates, and capability checks
104
114
  still win.
105
115
 
116
+ Model exclusions are hard routing policy. An excluded model is removed from
117
+ recommendations and assignments, and Bullswarm pins a same-tier allowed model
118
+ through the connector-owned model flag whenever the provider default could be
119
+ excluded. A pool that cannot guarantee the exclusion is ineligible for that
120
+ dispatch. Reverse the policy with `bullswarm strategy include-model <model>`.
121
+
106
122
  Every run and workflow attempt reports its selected agent/model and estimated
107
123
  usage. When a delegate does not expose counters, Bullswarm labels its UTF-8
108
124
  byte/4 token estimate. The breakdown separates standard read, cache read,
@@ -116,26 +132,33 @@ phase/step/attempt tree.
116
132
  For normal multi-step work, give Bullswarm the goal—not a JSON graph:
117
133
 
118
134
  ```bash
119
- # Foreground: streams progress and returns when terminal.
135
+ # Default: starts independently, prints observation/result commands, and returns.
120
136
  bullswarm workflow goal \
121
137
  "Fix the failing tests with the smallest correct change and verify them" \
122
138
  --cwd ~/some-repo
123
139
 
124
- # Detached: the initiating CLI exits; the workflow continues independently.
140
+ # Follow low-noise semantic progress immediately after launch.
125
141
  bullswarm workflow goal \
126
142
  "Audit and repair the parser, then run its acceptance tests" \
127
- --cwd ~/some-repo --detach --json
143
+ --cwd ~/some-repo --watch
128
144
  ```
129
145
 
130
146
  Bullswarm first honors an approved high-tier provider/model assignment when it
131
147
  remains eligible, otherwise it selects an eligible `workflow-planning`
132
- orchestrator by live quota surplus. The orchestrator observes durable evidence, proposes bounded actions,
148
+ orchestrator by live quota surplus. The orchestrator is one durable control-plane
149
+ conversation: Grok and Claude resume the same provider session at later
150
+ checkpoints, while Bullswarm keeps each turn separately auditable for routing,
151
+ usage, and recovery. It observes durable evidence, proposes bounded actions,
133
152
  and decides when another expansion or verification is necessary. Bullswarm
134
153
  validates the proposal, owns agent/process selection, routes workers, and calls
135
154
  the orchestrator again until completion, cancellation, failure, approval, or a
136
155
  hard graph-growth safeguard. No initial phases, prompts, JSON schema, or agent choice are
137
156
  required from the user.
138
157
 
158
+ Planner actions carry forward-only kebab-case phase names. A finished phase is
159
+ sealed: later planner turns must create a new phase for repair or verification,
160
+ so the TUI cannot jump backward or append work beneath a completed phase.
161
+
139
162
  The detached response includes a short ID and exact observation commands:
140
163
 
141
164
  ```bash
@@ -155,8 +178,9 @@ bullswarm workflow goal --resume <shortId> --json
155
178
 
156
179
  `--orchestrator <pool>` exists for controlled testing; ordinary use should
157
180
  leave selection on `auto`. `--max-agents` and `--max-workflow-seconds` are
158
- advisory planning targets; hard graph-growth safeguards are adjusted with
159
- `--max-expansion-rounds`, `--max-actions`, and `--max-items-per-expansion`.
181
+ advisory planning targets; `--max-expansion-rounds` is also an advisory
182
+ convergence target. Hard structural safeguards are adjusted with
183
+ `--max-actions` and `--max-items-per-expansion`.
160
184
  Interactive setup also records a worktree-isolation
161
185
  preference (`agent-decides`, `off`, or `required`); Bullswarm communicates that
162
186
  policy to the orchestrator without imposing repository topology itself.
@@ -205,6 +229,7 @@ bullswarm workflow runs --historical --since yesterday --until today
205
229
  bullswarm workflow runs --all --from 2026-08-20 --to 2026-08-27
206
230
  bullswarm workflow runs --limit 20 # cap the result count
207
231
  bullswarm workflow runs show <shortId> # state + report + summary
232
+ bullswarm workflow runs result <shortId> --json # stable result for the calling agent
208
233
  bullswarm runs show <shortId> # top-level shorthand
209
234
  bullswarm workflow runs delete <shortId> --yes # remove the run dir
210
235
 
@@ -217,6 +242,19 @@ Run-history time filters always compare when the workflow was initiated
217
242
  exclusive; `--started-after`/`--from` and `--started-before`/`--to` are aliases.
218
243
  Values accept ISO timestamps, local `YYYY-MM-DD` dates, `today`, `yesterday`,
219
244
  `tomorrow`, `now`, or relative durations such as `30m`, `24h`, `7d`, and `2w`.
245
+
246
+ After a workflow reaches a terminal state, agents should consume
247
+ `workflow runs result <id> --json` instead of probing `state.json`, task files,
248
+ or provider-specific output. The versioned `bullswarm.workflow.result.v1`
249
+ envelope identifies the final delivery artifact and its matching verification
250
+ verdict, and includes progress, step logs, tokens, and an explicitly
251
+ complete-or-partial tool-call total. `runs show` remains the low-level debugging
252
+ surface.
253
+ Goal launch output includes an `instructions` handoff with four named paths:
254
+ `agentInspect` for a machine-readable snapshot, `watch` for low-noise progress,
255
+ `humanTui` for the interactive browser, and `result` for the terminal delivery.
256
+ Use `--watch` when the initiating terminal should immediately follow progress;
257
+ otherwise the command returns after printing this handoff.
220
258
  Time filters preserve the existing scope, so use `--all` or `--historical` when
221
259
  auditing completed runs.
222
260
 
@@ -234,9 +272,20 @@ bullswarm workflow watch <shortId> --jsonl # automation-friendly stream
234
272
  bullswarm workflow watch <shortId> --once # one current/terminal snapshot
235
273
  ```
236
274
 
237
- `workflow tui` is the interactive, Claude-style `/workflows` view. It watches
238
- ongoing runs from disk and supports `j`/`k` or arrow-key selection, Enter for
239
- details, `c` to request a cooperative stop, `r` to refresh, and `q` to quit.
275
+ `workflow tui` is the interactive, Claude-style `/workflows` view. For an
276
+ autonomous goal its left navigation stacks a compact Orchestrator panel above
277
+ the Phases panel; internal planner turns never appear as workers or phases.
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,
282
+ an animated Braille spinner for active work, `⧖` waiting, `✓` finished, and
283
+ `✗` failed or interrupted. The non-emoji `⧖` avoids the inconsistent cell
284
+ width of `⌛` across terminal fonts. It watches ongoing runs from disk and supports `j`/`k` or arrow-key selection, Enter for
285
+ details, Esc to go back, `c` to request a confirmed cooperative stop, `r` to
286
+ refresh, and `q` to detach. Its responsive drill-down fits both desktop and
287
+ mobile SSH terminals without squeezing phase, agent, and activity into three
288
+ narrow columns.
240
289
 
241
290
  ```bash
242
291
  bullswarm workflow tui
@@ -281,12 +330,18 @@ declaratively map provider events into a common semantic action record:
281
330
  {"id":"provider-action-id","at":"...","kind":"shell_command|read_file|edit|response","status":"running|completed|failed","summary":"safe scalar preview"}
282
331
  ```
283
332
 
284
- The live workflow pane retains the latest three logical actions per agent.
333
+ The live workflow pane retains and numbers the latest three logical actions per agent.
285
334
  Repeated updates for the same tool call replace its status, and streaming text
286
335
  chunks coalesce into one response action. Heartbeats, token/thought deltas,
287
336
  usage messages, hooks, and unparsed output remain liveness evidence but do not
288
- occupy the action pane. Connector-specific flags, paths, and mappings live in
337
+ occupy the action pane. The viewer tracks the total logical-action count so it
338
+ can display `last 3 of N`, and completed-agent detail includes a scrollable
339
+ Outcome read from the durable output artifact. Connector-specific flags, paths, and mappings live in
289
340
  `connectors/*.json` under `eventStream`; core contains no provider event names.
341
+ Raw structured stdout is treated as an agent transcript, not a provider error
342
+ channel, so reading source text such as an auth-signature matcher cannot falsely
343
+ quarantine Grok or Command Code. Error-shaped semantic results and stderr
344
+ diagnostics still trigger the auth/quota guard.
290
345
 
291
346
  After ten minutes without transport, parsed-event, or semantic-action evidence,
292
347
  an active child is labeled `suspected_stalled`. This is an inspection signal,
@@ -335,13 +390,22 @@ limits:
335
390
  }
336
391
  ```
337
392
 
338
- `maxAgents` and `maxWorkflowSeconds` are advisory inputs to the orchestrator.
339
- Crossing either target is recorded in durable state but never stops a worker,
340
- skips verification, or fails a run. `maxExpansionRounds`, `maxActions`, and
341
- `maxItemsPerExpansion` remain hard graph-growth safeguards. Delegates have no
393
+ `maxAgents`, `maxWorkflowSeconds`, and `maxExpansionRounds` are advisory inputs
394
+ to the orchestrator. Approaching them strongly biases the planner toward
395
+ consolidating existing artifacts and returning the best useful outcome;
396
+ crossing them is recorded but never stops a worker, skips verification, or
397
+ fails a run. `maxActions` and `maxItemsPerExpansion` remain hard structural
398
+ safeguards. Reaching one returns a qualified outcome when useful work exists,
399
+ rather than discarding the run as a blanket failure. Delegates have no
342
400
  implicit wall-clock timeout; set a step's `timeoutSec` (or direct-run
343
401
  `--timeout`) only when an operator explicitly wants a hard termination timer.
344
402
 
403
+ An autonomous `complete` remains strictly verified. A planner `stop` produces
404
+ `completed_with_concerns` when a useful delivery exists, including unresolved
405
+ verification concerns and the stopping reason; it produces `blocked` only
406
+ when no useful delivery exists. `workflow runs result` treats the qualified
407
+ delivery as ready while reporting `verified:false`.
408
+
345
409
  The planner returns versioned JSON. It may propose `needs_more_work` with
346
410
  bounded `run`, inline-`fanout`, or `verify` actions. The deterministic runtime
347
411
  validates IDs, dependencies, operation types, capabilities, and budgets before
package/bin/bullswarm.js CHANGED
@@ -4,9 +4,9 @@
4
4
  import { main } from '../src/cli.js';
5
5
 
6
6
  main(process.argv.slice(2)).then(
7
- (code) => process.exit(code),
7
+ (code) => { process.exitCode = code; },
8
8
  (err) => {
9
9
  console.error(err?.message ?? err);
10
- process.exit(1);
10
+ process.exitCode = 1;
11
11
  },
12
12
  );
@@ -53,6 +53,10 @@
53
53
  "capabilities": ["strong-analysis", "code-reading", "file-editing", "workflow-planning"],
54
54
  "knownModels": ["claude-fable-5", "claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5"],
55
55
  "modelSelection": { "flag": "--model", "mode": "replace-or-append" },
56
+ "conversation": {
57
+ "newArgs": ["--session-id", "{sessionId}"],
58
+ "resumeArgs": ["--resume", "{sessionId}"]
59
+ },
56
60
  "modelProfiles": [
57
61
  { "match": "^claude-fable-5$", "tier": "high", "qualityRank": 6, "pricing": { "inputUsdPerMillion": 10, "cacheReadUsdPerMillion": 1, "outputUsdPerMillion": 50 }, "pricingSource": "https://platform.claude.com/docs/en/about-claude/pricing", "pricingUpdatedAt": "2026-08-27" },
58
62
  { "match": "^claude-opus-(5|4-[5-8])$", "tier": "high", "qualityRank": 5, "pricing": { "inputUsdPerMillion": 5, "cacheReadUsdPerMillion": 0.5, "outputUsdPerMillion": 25 }, "pricingSource": "https://platform.claude.com/docs/en/about-claude/pricing", "pricingUpdatedAt": "2026-08-27" },
@@ -48,6 +48,10 @@
48
48
  "knownModels": ["grok-4.6", "grok-4.5"],
49
49
  "model": "grok-4.6",
50
50
  "modelSelection": { "flag": "--model", "mode": "replace-or-append" },
51
+ "conversation": {
52
+ "newArgs": ["--session-id", "{sessionId}"],
53
+ "resumeArgs": ["--resume", "{sessionId}"]
54
+ },
51
55
  "modelProfiles": [
52
56
  { "match": "^grok-4\\.6$", "tier": "high", "qualityRank": 5, "pricing": { "inputUsdPerMillion": 2, "cacheReadUsdPerMillion": 0.5, "outputUsdPerMillion": 6 }, "pricingSource": "https://docs.x.ai/developers/models/grok-4.6", "pricingUpdatedAt": "2026-08-27" },
53
57
  { "match": "^grok-4\\.5$", "tier": "medium", "qualityRank": 4 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bullswarm",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
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
@@ -86,16 +86,26 @@ Prefer this for ordinary goal-driven work. The caller supplies intent only:
86
86
  ```bash
87
87
  bullswarm workflow goal \
88
88
  "Fix the failing parser tests with the smallest correct change and verify them" \
89
- --cwd=<abs/path/to/repo> --detach --json
89
+ --cwd=<abs/path/to/repo> --json
90
90
  ```
91
91
 
92
92
  The returned JSON contains `runId`, `shortId`, logs, and exact observation
93
- commands. Bullswarm chooses a capable orchestrator by quota surplus, supplies
93
+ commands. Its `instructions` object separates `agentInspect`, low-noise `watch`,
94
+ interactive `humanTui`, and terminal `result` retrieval. Bullswarm chooses a capable orchestrator by quota surplus, supplies
94
95
  the internal planning contract, validates every proposed graph expansion,
95
96
  routes workers independently, requires verification evidence, and replans until
96
97
  a truthful terminal state. The initiating agent does not create phases, action
97
98
  IDs, dependency JSON, planner prompts, or pool assignments.
98
99
 
100
+ The autonomous orchestrator is one durable control-plane conversation, separate
101
+ from execution phases. Connectors that declare conversation continuation (Grok
102
+ and Claude Code) resume the provider session at later evidence checkpoints;
103
+ individual turns remain visible in JSON audit state for spend and recovery, but
104
+ the human TUI displays one orchestrator thread rather than several agents.
105
+ Planner-created actions also have forward-only named phases. Once a phase has
106
+ executed, a later turn must use a new phase name; Bullswarm rejects attempts to
107
+ append new work beneath a completed phase.
108
+
99
109
  Observe from any other shell or agent:
100
110
 
101
111
  ```bash
@@ -106,6 +116,13 @@ bullswarm workflow events --json <shortId> --after 0
106
116
  bullswarm workflow action show --json <shortId> <actionId>
107
117
  ```
108
118
 
119
+ `workflow goal` starts the durable runner in the background, prints operating
120
+ instructions, and returns by default. Add `--watch` to immediately follow
121
+ low-noise progress until terminal. Open the full-screen Phase → Agent →
122
+ Agent-steps viewer with the printed `humanTui` command; Up/Down, Enter, Esc, and
123
+ `q` navigate or detach while work continues. `--foreground` explicitly restores
124
+ terminal-owned execution.
125
+
109
126
  The detached runner does not depend on the initiating CLI remaining alive.
110
127
  Resume a process-interrupted run from its persisted definition with
111
128
  `bullswarm workflow goal --resume <shortId> --json`. Leave orchestrator
@@ -183,6 +200,7 @@ bullswarm workflow runs --name <workflow> # filter by workflow name
183
200
  bullswarm workflow runs --all --since 7d # initiated in the last 7 days
184
201
  bullswarm workflow runs --historical --since yesterday --until today
185
202
  bullswarm workflow runs show <shortId> # state + report + summary
203
+ bullswarm workflow runs result <shortId> --json # stable delivery for the caller
186
204
  bullswarm workflow runs delete <shortId> --yes
187
205
  ```
188
206
 
@@ -193,6 +211,12 @@ ISO timestamps, local dates, today/yesterday/tomorrow/now, or durations such as
193
211
  `7d`. Add `--all` or `--historical`; time filters do not silently change the
194
212
  normal ongoing-only scope.
195
213
 
214
+ When the run is terminal, use `workflow runs result <id> --json` as the
215
+ handoff contract. Its versioned result envelope points to the selected delivery
216
+ artifact, the dependent verification verdict, progress, and usage. Do not guess
217
+ the output schema by scraping task files or assume the last provider response is
218
+ the deliverable; `runs show` is for low-level debugging.
219
+
196
220
  Before authoring or choosing a workflow, agents can inspect the live execution
197
221
  fabric and the workflow document itself:
198
222
 
@@ -219,6 +243,7 @@ bullswarm strategy show --json
219
243
  bullswarm strategy apply --yes --refresh-hours 24
220
244
  bullswarm strategy auto status
221
245
  bullswarm strategy assign high --pool <pool> --model <model>
246
+ bullswarm strategy exclude-model <model>
222
247
  ```
223
248
 
224
249
  Connector-declared discovery, dated benchmark/pricing evidence, live quota,
@@ -230,6 +255,19 @@ an assignment, but it never bypasses capability, quarantine, exhaustion, or
230
255
  burst-gate safety. Each attempt records the chosen agent/model and labeled
231
256
  token, cost, and normalized-quota estimates in the workflow tree.
232
257
 
258
+ `exclude-model` is a persisted hard policy, not a prompt hint. Excluded models
259
+ are removed from recommendations and ignored in assignments. When a connector
260
+ supports explicit model selection, Bullswarm pins an allowed model in the same
261
+ effort tier; otherwise that pool is excluded because its implicit default
262
+ cannot be guaranteed. Restore eligibility with `strategy include-model`.
263
+
264
+ In the human TUI, the autonomous orchestrator has a compact selectable panel
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.
268
+ The shared state marks are `○` not started, animated Braille spinner active,
269
+ `⧖` waiting, `✓` finished, and `✗` failed or interrupted.
270
+
233
271
  Run state also exposes the versioned plan, action ledger, aggregate usage, every attempt,
234
272
  planner decisions and reasons, budgets, `currentPhase`, `currentStep`, and
235
273
  `activeAgents` in `workflow tui --json <shortId>`. Each attempt includes its
@@ -265,11 +303,13 @@ blocks into the same action shape and retains the latest three. Ten minutes
265
303
  without any transport/event/action evidence becomes `suspected_stalled` with
266
304
  `autoTerminate:false`; it is deliberately not treated as proof of death.
267
305
 
268
- For adaptive work, declare a `decide` step plus `maxExpansionRounds` and the
269
- other graph-growth safeguards (`maxActions` and `maxItemsPerExpansion`).
270
- `maxAgents` and `maxWorkflowSeconds` are advisory planning targets: they expose
271
- remaining headroom and overage to the orchestrator but never stop a worker or
272
- skip required verification. The loop is durable:
306
+ For adaptive work, declare a `decide` step plus advisory
307
+ `maxExpansionRounds` and the hard structural safeguards (`maxActions` and
308
+ `maxItemsPerExpansion`). `maxAgents`, `maxWorkflowSeconds`, and
309
+ `maxExpansionRounds` expose remaining headroom and overage to the orchestrator
310
+ but never stop a worker or skip required verification. Near the targets the
311
+ planner is instructed to converge, consolidate existing artifacts, and avoid
312
+ optional work. The loop is durable:
273
313
 
274
314
  ```text
275
315
  execute -> observe -> decide -> validate proposal -> append -> execute -> observe
@@ -278,6 +318,9 @@ execute -> observe -> decide -> validate proposal -> append -> execute -> observ
278
318
  Allowed planner decisions are `proceed`, `complete`, `needs_more_work`,
279
319
  `retry`, `escalate`, `wait_for_approval`, and `stop`. Expansion decisions must
280
320
  contain bounded actions; malformed or over-budget output executes nothing.
321
+ `complete` still requires successful delivery and verification. `stop` returns
322
+ `completed_with_concerns` with a ready best-effort artifact when useful work
323
+ exists, or `blocked` when it does not; neither hides failed verification.
281
324
  Use `workflows/adaptive-code-review.json` as the starting template.
282
325
  Planner proposals cannot choose `pool`, `addDir`, or `taskFile`. Those fields
283
326
  are runtime-owned. An initiator may constrain them with a decide step's
package/src/cli.js CHANGED
@@ -18,6 +18,7 @@ import { cmdWorkflow } from './workflow/cli.js';
18
18
  import { cmdStrategy, maybeRefreshStrategy } from './strategy-cli.js';
19
19
  import { cmdIntegrate, installIntegration } from './integrate.js';
20
20
  import { helpForArgs } from './help.js';
21
+ import { resolveDispatchModel } from './lib/strategy.js';
21
22
 
22
23
  export function getBullswarmDir() {
23
24
  const h = process.env.BULLSWARM_HOME?.trim();
@@ -118,7 +119,15 @@ async function cmdRun(opts) {
118
119
  // Burst gate (M3): a pool whose 5h window is >=90% used is excluded from
119
120
  // dispatch entirely this run — it paces nothing, it's just out of burst room.
120
121
  const gated = pools.filter((p) => p.burstGate);
121
- const eligiblePools = gated.length ? pools.filter((p) => !p.burstGate) : pools;
122
+ const ungatedPools = gated.length ? pools.filter((p) => !p.burstGate) : pools;
123
+ const assignment = state.strategy?.assignments?.[effortTier] ?? null;
124
+ const eligiblePools = ungatedPools.map((pool) => ({
125
+ ...pool,
126
+ modelPolicy: resolveDispatchModel(pool.connector ?? pool, effortTier, {
127
+ assignment,
128
+ excludedModels: state.strategy?.excludedModels ?? [],
129
+ }),
130
+ })).filter((pool) => pool.modelPolicy.eligible);
122
131
 
123
132
  const route = pickPool(lane, eligiblePools, {
124
133
  callerEligible: opts['no-caller'] !== true,
@@ -146,8 +155,8 @@ async function cmdRun(opts) {
146
155
  // connector spec lives one level down.
147
156
  const poolView = route.pick.connector ?? { name: route.pick.pool };
148
157
  const connector = poolView.connector ?? poolView;
149
- const assignment = state.strategy?.assignments?.[effortTier] ?? null;
150
- const selectedModel = assignment?.pool === connector.name ? assignment.model : null;
158
+ const selectedModel = poolView.modelPolicy?.model
159
+ ?? (assignment?.pool === connector.name ? assignment.model : null);
151
160
  const runtimeConnector = {
152
161
  ...connector,
153
162
  subscription: poolView.subscription ?? connector.subscription ?? null,
@@ -237,6 +246,8 @@ function emit(verdict, opts) {
237
246
 
238
247
  function cmdHealth(opts) {
239
248
  const state = loadState(getBullswarmDir());
249
+ const released = sweepQuarantines(state, Date.now());
250
+ if (released.length) saveState(getBullswarmDir(), state);
240
251
  const runsDir = join(getBullswarmDir(), 'runs');
241
252
  const findings = [];
242
253
 
package/src/help.js CHANGED
@@ -44,13 +44,15 @@ const HELP = {
44
44
  release: { _text: leaf('bullswarm release <patch|minor|major> [--dry-run]') },
45
45
  strategy: {
46
46
  _text: leaf('bullswarm strategy <command> [options]',
47
- 'Commands: refresh, apply, show, assign, clear-assignment, set-subscription, auto.'),
47
+ 'Commands: refresh, apply, show, assign, clear-assignment, exclude-model, include-model, set-subscription, auto.'),
48
48
  refresh: { _text: leaf('bullswarm strategy refresh [--json] [--apply --yes] [--refresh-hours <n>]') },
49
49
  recommend: { _text: leaf('bullswarm strategy recommend [--json] [--apply --yes]', 'Alias for strategy refresh.') },
50
50
  apply: { _text: leaf('bullswarm strategy apply --yes [--refresh-hours <n>]') },
51
51
  show: { _text: leaf('bullswarm strategy show [--json]') },
52
52
  assign: { _text: leaf('bullswarm strategy assign <high|medium|low> --pool <pool> --model <model>') },
53
53
  'clear-assignment': { _text: leaf('bullswarm strategy clear-assignment <high|medium|low>') },
54
+ 'exclude-model': { _text: leaf('bullswarm strategy exclude-model <model>', 'Persistently prevent this exact model from orchestration and worker dispatches.') },
55
+ 'include-model': { _text: leaf('bullswarm strategy include-model <model>', 'Remove a persisted model exclusion.') },
54
56
  'set-subscription': { _text: leaf('bullswarm strategy set-subscription <pool> [--plan <name>] [--monthly-usd <n|unknown>] [--included-usd <n|unknown>] [--quota-window <name>]') },
55
57
  auto: {
56
58
  _text: leaf('bullswarm strategy auto <status|off> [--yes]'),
@@ -60,15 +62,44 @@ const HELP = {
60
62
  },
61
63
  workflow: {
62
64
  _text: leaf('bullswarm workflow <command> [options]',
63
- 'Commands: goal, run, validate, list, draft, runs, capabilities, inspect, tui, watch, events, steer, action, approval.'),
64
- goal: { _text: leaf('bullswarm workflow goal "<goal>" [--cwd <dir>] [--detach] [--json] [planning options]',
65
- 'Use --resume <shortId|runId> to resume. Planning options include --orchestrator, --max-agents, --max-expansion-rounds, --max-actions, --max-items-per-expansion, --max-workflow-seconds, --concurrency, and --retry-attempts.') },
65
+ `Create, execute, observe, and audit durable multi-agent workflows.
66
+
67
+ Build and execute:
68
+ goal <goal> autonomously plan, execute, verify, and replan a goal
69
+ run <workflow> run an existing workflow file or saved draft
70
+ draft ... incrementally build a fixed workflow graph
71
+ validate <target> validate without executing
72
+ inspect <target> show the document, semantics, and validation details
73
+ list list available workflow definitions
74
+
75
+ Observe and control:
76
+ runs search ongoing and historical workflow instances
77
+ tui [runId] full-screen phase → agent → step/detail browser
78
+ watch <runId> follow low-noise progress until terminal
79
+ events <runId> replay durable events after a sequence cursor
80
+ action show ... inspect one action and all of its attempts
81
+ steer <runId> queue guidance for the next planner checkpoint
82
+ approval ... approve or reject a waiting decision gate
83
+
84
+ Execution fabric:
85
+ capabilities show pools, lanes, models, meters, and routing constraints
86
+
87
+ Common examples:
88
+ bullswarm workflow goal "Audit this repository" --cwd=.
89
+ bullswarm workflow runs --all --since=7d
90
+ bullswarm workflow tui <shortId>
91
+ bullswarm workflow draft --help
92
+
93
+ Run bullswarm workflow <command> --help for complete command options.`),
94
+ goal: { _text: leaf('bullswarm workflow goal "<goal>" [--cwd <dir>] [--watch|--foreground] [--json] [planning options]',
95
+ 'Default: launch independently, print operating instructions, and return. --watch immediately follows low-noise progress until terminal. --foreground keeps execution terminal-owned. Use --resume <shortId|runId> to resume. Planning options include --orchestrator, --max-agents, --max-expansion-rounds, --max-actions, --max-items-per-expansion, --max-workflow-seconds, --concurrency, and --retry-attempts.') },
66
96
  run: { _text: leaf('bullswarm workflow run <file-or-name> [--input k=v]... [--resume <shortId|runId>] [--json] [--quiet]') },
67
97
  validate: { _text: leaf('bullswarm workflow validate <file-or-name>') },
68
98
  list: { _text: leaf('bullswarm workflow list [--json]') },
69
99
  capabilities: { _text: leaf('bullswarm workflow capabilities [--json]') },
70
100
  inspect: { _text: leaf('bullswarm workflow inspect <file-or-name>') },
71
- tui: { _text: leaf('bullswarm workflow tui [<runId>] [--json] [--all] [--show <runId>] [--cancel <runId>]') },
101
+ tui: { _text: leaf('bullswarm workflow tui [<runId>] [--json] [--all] [--show <runId>] [--cancel <runId>]',
102
+ 'Interactive mode opens a full-screen phase → agent → step/detail browser. Up/down selects, Enter drills in, Esc goes back, q detaches without stopping work, and c requests explicit cancellation confirmation.') },
72
103
  watch: { _text: leaf('bullswarm workflow watch <runId> [--interval <seconds>] [--heartbeat <seconds>] [--jsonl] [--once]') },
73
104
  events: { _text: leaf('bullswarm workflow events <runId> [--after <sequence>] [--json]') },
74
105
  steer: { _text: leaf('bullswarm workflow steer <runId> --message <guidance> [--json]') },
@@ -107,9 +138,11 @@ export function helpForArgs(argv) {
107
138
  function runsHelp() {
108
139
  return {
109
140
  _text: leaf('bullswarm workflow runs [list] [--all|--historical] [--name <workflow>] [--since <time>] [--until <time>] [--limit <n>] [--json]',
110
- 'Commands: show <id>, delete <id> --yes. Time filters use the workflow initiation timestamp.'),
141
+ 'Commands: show <id>, result <id>, delete <id> --yes. Time filters use the workflow initiation timestamp.'),
111
142
  list: { _text: leaf('bullswarm workflow runs list [--all|--historical] [--name <workflow>] [--since <time>] [--until <time>] [--limit <n>] [--json]') },
112
143
  show: { _text: leaf('bullswarm workflow runs show <shortId|runId> [--json]') },
144
+ result: { _text: leaf('bullswarm workflow runs result <shortId|runId> [--json]',
145
+ 'Return the stable caller-facing delivery, verification verdict, progress, and usage envelope.') },
113
146
  delete: { _text: leaf('bullswarm workflow runs delete <shortId|runId> --yes [--force] [--json]') },
114
147
  };
115
148
  }
@@ -178,9 +178,19 @@ export function createAgentEventDecoder(eventStream, { onEvent, onProgress } = {
178
178
 
179
179
  export function recordAgentAction(agent, event, maxActions = 3) {
180
180
  agent.lastActions ??= [];
181
+ const priorVisibleCount = agent.lastActions.length;
182
+ if (!agent._actionHistory) {
183
+ Object.defineProperty(agent, '_actionHistory', {
184
+ value: new Map(agent.lastActions.map((action) => [action.id, action])),
185
+ writable: true,
186
+ enumerable: false,
187
+ });
188
+ }
181
189
  const existingIndex = agent.lastActions.findIndex((action) => action.id === event.id);
182
- const existing = existingIndex >= 0 ? agent.lastActions.splice(existingIndex, 1)[0] : {};
183
- const isNew = existingIndex < 0;
190
+ const visibleExisting = existingIndex >= 0 ? agent.lastActions.splice(existingIndex, 1)[0] : null;
191
+ const rememberedExisting = agent._actionHistory.get(event.id);
192
+ const existing = visibleExisting ?? rememberedExisting ?? {};
193
+ const isNew = !rememberedExisting;
184
194
  const statusChanged = existing.status != null && existing.status !== event.status;
185
195
  let summary = event.summary ?? existing.summary ?? null;
186
196
  let summaryRaw = event.summary ?? existing._summaryRaw ?? existing.summary ?? null;
@@ -193,8 +203,15 @@ export function recordAgentAction(agent, event, maxActions = 3) {
193
203
  const eventKind = event.kind === 'agent' && existing.kind ? existing.kind : event.kind;
194
204
  const action = { ...existing, ...event, kind: eventKind, summary, summaryMode: undefined };
195
205
  Object.defineProperty(action, '_summaryRaw', { value: summaryRaw, writable: true, enumerable: false });
206
+ agent._actionHistory.delete(event.id);
207
+ agent._actionHistory.set(event.id, action);
208
+ while (agent._actionHistory.size > 256) {
209
+ agent._actionHistory.delete(agent._actionHistory.keys().next().value);
210
+ }
196
211
  agent.lastActions.push(action);
197
212
  agent.lastActions = agent.lastActions.slice(-maxActions);
213
+ if (agent.actionCount == null) agent.actionCount = priorVisibleCount;
214
+ if (isNew) agent.actionCount += 1;
198
215
  agent.lastActionAt = event.at;
199
216
  agent.lastProgressAt = event.at;
200
217
  return { action: agent.lastActions.at(-1), isNew, statusChanged };
package/src/lib/config.js CHANGED
@@ -49,7 +49,9 @@ export function buildPools(bullswarmDir, now = Date.now(), readings = {}) {
49
49
  costRank: conn.costRank ?? 5,
50
50
  lanes: conn.lanes,
51
51
  capabilities: conn.capabilities ?? [],
52
- quarantine: ps.quarantine ?? null,
52
+ quarantine: isQuarantined({ quarantine: ps.quarantine ?? null }, now)
53
+ ? ps.quarantine
54
+ : null,
53
55
  incumbentLane: Object.entries(state.incumbents ?? {})
54
56
  .filter(([, v]) => v === name)
55
57
  .map(([k]) => k),
@@ -65,6 +67,7 @@ export function buildPools(bullswarmDir, now = Date.now(), readings = {}) {
65
67
  ...(state.strategy?.subscriptions?.[name] ?? {}),
66
68
  },
67
69
  strategyAssignments: state.strategy?.assignments ?? {},
70
+ strategyExcludedModels: state.strategy?.excludedModels ?? [],
68
71
  };
69
72
  pools.push(pool);
70
73
  }
package/src/lib/route.js CHANGED
@@ -117,6 +117,8 @@ export function pickPool(lane, pools, opts = {}) {
117
117
 
118
118
  const candidates = scored.map((e) => ({
119
119
  pool: e.pool.name,
120
+ model: e.pool.modelPolicy?.model ?? null,
121
+ modelPolicy: e.pool.modelPolicy?.source ?? null,
120
122
  pace: Math.round(e.pace * 10) / 10,
121
123
  costRank: e.pool.costRank ?? null,
122
124
  }));