bullswarm 0.13.2 → 0.14.1

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,85 @@
1
1
  # bullswarm changelog
2
2
 
3
+ ## 0.14.1 — the TUI survives its writer; steering lands or expires truthfully
4
+
5
+ Proven on a goal-3 re-run (`d7xyg2`): 1 planner turn / 269 s (baseline 0.13.1: 1 / 294 s), planner context 6.2 k chars (from 32.7 k), auto-completed, deliverable verified, zero observation crashes — `docs/experiments/2026-08-29-dogfood-bullswarm-builds-bullswarm.md`.
6
+
7
+ - Workflow `state.json`/`report.json`/`workflow.json` writes are atomic
8
+ (temp + rename, new `src/workflow/fsjson.js`): a concurrent reader can never
9
+ observe a half-written file. Earned: `workflow tui` crashed with
10
+ "Unterminated string in JSON at position 138968" parsing `state.json`
11
+ mid-write (observed twice, 2026-08-29).
12
+ - Observation readers tolerate torn or missing JSON: the TUI keeps painting
13
+ the last good frame of the same run, and a render or key-handler error is
14
+ shown in the message line instead of killing the process and stranding the
15
+ terminal in alt-screen raw mode. Mutating commands (stop, approval) retry
16
+ the read once and then refuse loudly instead of silently dropping the
17
+ operator's command. `runs delete` treats an unreadable `state.json` as
18
+ ongoing (refuses without `--force`) rather than deleting a possibly-live run.
19
+ - An action being re-run (repair round, re-verify, schema retry) reads as
20
+ `running` and its phase as `active` even when its previous round recorded
21
+ `ok:false`; a failed mark now means failed-and-not-being-retried. (User
22
+ report: the TUI showed ✗ "2/2 complete" beside a live spinner.)
23
+ - Pending operator steering defers program self-completion: a clean program
24
+ with `completion: all-actions-ok` returns to the planner gate (event
25
+ `decision.completion_deferred`), which delivers the steer — instead of
26
+ auto-completing and silently discarding it (defect observed live:
27
+ 0 `steering.delivered` events for a queued steer). Steering that can no
28
+ longer reach any gate is marked `expired_undelivered` with event
29
+ `steering.expired` at the terminal transition; interrupted runs keep their
30
+ queue for the resumed run's next gate.
31
+ - Resume re-runs an action the interruption cancelled mid-flight instead of
32
+ re-planning around a phantom failure: cancelled actions and the dependents
33
+ blocked only by them are reopened (event `action.reopened`) and the accepted
34
+ program continues from where it stopped. Observed on a SIGTERM-interrupted
35
+ run: 1 cancelled action → 4 "blocked" → a spurious planner turn.
36
+ - Planner contract: a verify with several `dependsOn` must set `review`
37
+ (rule 4); the program's last worker must be covered by a successful verify
38
+ (rule 8) — both were the causes of extra planner gates on the goal-3 proof
39
+ run. A corrective turn's `validationFeedback.rejectedResponseExcerpt` is
40
+ capped at 2 000 chars, and the rejected proposal is resent as a skeleton
41
+ (ids, shapes, dependsOn; prompts elided) — the planner's thread already
42
+ holds it verbatim.
43
+ - A verify without `review` is no longer grounds to reject a whole program:
44
+ it reviews its single (or last) dependency's artifact, or audits the
45
+ repository directly when it has no `dependsOn` (`reviewScope: repository`).
46
+ Observed on two proof runs: a 9-action program bounced for one field,
47
+ costing a 5-minute correction turn each time.
48
+
49
+ ## 0.14.0 — structured worker output, compact planner contract
50
+
51
+ - A verify whose reply cannot be parsed as the verdict JSON gets ONE bounded
52
+ re-ask (event `verify.verdict_retry`) before its failure can reach a planner
53
+ boundary — observed on run `ejk9w2`: one unparseable verdict cost a full
54
+ planner turn plus ~8 minutes of re-proving a passing state.
55
+ - Planner contract amendments from the same run's observations: a verify is
56
+ scoped to what can be true at its point in the graph (later-scheduled work is
57
+ not a defect; cosmetic mismatches are concerns, never ok:false); when the
58
+ goal's acceptance checks pass the planner returns complete instead of adding
59
+ polish actions; restored the shared-working-tree, redundant-verification,
60
+ and operatorSteering guidance dropped by the contract merge.
61
+ - "Full" planner-context excerpts (scout, new-since-last-decision, failing
62
+ verifies) obey the per-excerpt and total budgets again; the compaction must
63
+ never rebuild the 163 k-char contexts it replaced.
64
+ - Planner context and contract compacted: complete emitted planner task text up to the durable-context marker, worktree-isolation suffix included **OBSERVED** `16,316 -> 5,208` characters, and a sample turn-2 durable context **COMPUTED** `163,000 -> 23,547` characters by replacing full attempt records with compact ledger rows and retaining full output excerpts only for new/scout or `ok:false` verify actions.
65
+ - Planner `run` actions and fan-out `stepTemplate`s may declare an optional
66
+ `outputSchema`, an object-typed JSON-Schema subset. The runtime tells the
67
+ worker to end its output with one matching JSON object, parses and validates
68
+ it, and persists a `run` result as `outputs.<id>.data` with `schemaOk: true`;
69
+ fan-out results store those fields inside each `outputs.<fanoutId>.items[]`
70
+ entry. Successful validation emits `action.output_validated`.
71
+ - Schema failures emit `action.output_schema_retry` and receive exactly one
72
+ bounded retry with the validation errors and the previous output tail. If
73
+ that retry also fails, the action remains `ok:false`, records
74
+ `schemaOk:false` and `schemaErrors`, and keeps the output text with the
75
+ reason `output did not match outputSchema: <errors>`. Resumed runs do not
76
+ re-dispatch actions already marked `schemaOk:true`.
77
+ - Dependent prompts can render `{{outputs.<id>.data.<field>}}`, and
78
+ `fanout.itemsFrom` accepts `outputs.<id>.data.items` without an extraction
79
+ agent when the array is already present. Planner decision validation rejects
80
+ `outputSchema` on a proposed `verify` because verify has a fixed verdict
81
+ shape.
82
+
3
83
  ## 0.13.2 — user text is never a template
4
84
 
5
85
  - `workflow goal` failed before anything ran when the goal text quoted
@@ -234,7 +234,7 @@ from `docs/experiments/2026-08-29-ultracode-vs-bullswarm.md`, never projected.
234
234
  | Phases | Labels for grouping; never synchronise | Forward-only kebab-case names per action; also just labels | None |
235
235
  | Parallelism | `pipeline` default, `parallel` barrier; cap min(16, CPUs−2) | `executeActions` ran dependency-ready siblings **serially** (`runner.js:558`); only `fanout` items ran concurrently; goal default concurrency 3 | **Fixed in 0.11.0** — ready-set scheduler + default 8 |
236
236
  | Planner bias | Script author is told to fan out and default to pipeline | Goal prompt said "return needs_more_work with the **smallest useful set** of bounded … actions" (`goal.js:18`) and planner prompt said "keep actions cohesive" | **Fixed in 0.11.0** — "propose the COMPLETE dependency graph", per-item fix→verify chains, file ownership, self-contained prompts |
237
- | Per-agent prompt | Self-contained, plus JSON schema enforced at tool layer | Planner-authored prompt; free-text answer, content-verified by heuristics; `verify` returns JSON verdict | Partial. Schema-enforced worker output is a candidate, not adopted yet |
237
+ | Per-agent prompt | Self-contained, plus JSON schema enforced at tool layer | Planner-authored prompt; `outputSchema` validates structured worker data, while `verify` retains its fixed JSON verdict | Adopted for declared schemas; tool-layer enforcement remains a difference |
238
238
  | Failure handling | Loops in code; `null` on agent death | Planner replans (costly); 0.10.9 added corrective turns for invalid decisions and 0.11.0 recovers mis-shaped `verify.review` before dispatch | Improved; retry-in-code per action still absent |
239
239
  | Determinism / resume | Journal of return values; prefix cache | Durable `state.json` + `events.jsonl` + action ledger; resume skips durable outputs | Equivalent |
240
240
  | Data-driven fan-out | `pipeline(discovered.items, …)` — count unknown when the script is written | Decision schema forced inline `items`; the planner spent a turn waiting for discovery | **Fixed in 0.12.0** — `itemsFrom` on proposed fan-outs + one bounded extraction retry |
@@ -288,8 +288,7 @@ author and the `Workflow` runtime.
288
288
  if the output still has no array the runtime runs ONE bounded, read-only
289
289
  extraction action over it (never re-running the producer, which may have
290
290
  mutated files). That is the "schema retry" of Claude's `StructuredOutput`,
291
- done as a second cheap agent instead of a tool-layer retry. A general
292
- `outputSchema` on run actions is still open (§5).
291
+ done as a second cheap agent instead of a tool-layer retry.
293
292
  3. **Pre-authored repair** — shipped. `repair: { prompt, maxRounds }` on a
294
293
  verify: verify-fail → `<verifyId>-repair-<n>` (concerns verbatim) →
295
294
  re-verify, inside the executor. Claude's fix-loop as code.
@@ -350,6 +349,21 @@ author and the `Workflow` runtime.
350
349
  exist — the script's `while (!ok)` loop *is* the evidence — which is the
351
350
  general lesson: every piece of control flow bullswarm moves from planner
352
351
  into runtime needs its evidence rule moved with it.
352
+ 11. **[SPEC] Schema-enforced worker output** — a planner `run` action or fan-out
353
+ `stepTemplate` may declare an object-typed `outputSchema` subset. The
354
+ runtime appends instructions for one trailing matching JSON object, with no
355
+ prose or markdown fences after it, then parses and validates the object.
356
+ A successful `run` persists `outputs.<id>.data` and `schemaOk: true`; a
357
+ fan-out stores those schema results inside each
358
+ `outputs.<fanoutId>.items[]` entry. Both emit `action.output_validated`. A mismatch emits
359
+ `action.output_schema_retry` and gets exactly one bounded retry carrying
360
+ the validation errors and the previous output tail; a second mismatch
361
+ fails the action while retaining its output text and recording
362
+ `schemaOk:false` and `schemaErrors`. Dependent prompts can render data
363
+ fields, and `fanout.itemsFrom` can consume `outputs.<id>.data.items` without
364
+ extraction when it is already an array. Planner decision validation rejects
365
+ `outputSchema` on a proposed `verify` because verify has a fixed verdict
366
+ shape.
353
367
 
354
368
  **Honest limitation.** `itemsFrom` removes the planner *turn*, not the stage
355
369
  *barrier*: a verify depending on a data-driven fan-out waits for all items,
@@ -361,10 +375,6 @@ them.
361
375
 
362
376
  ## 5. Not adopted (yet), and why
363
377
 
364
- - **Schema-enforced worker output.** bullswarm's content verification and the
365
- JSON `verify` verdict cover the failure mode today; adding per-action
366
- `outputSchema` is the next step if planners keep re-asking workers for
367
- structure.
368
378
  - **Per-action worktree isolation.** File ownership declared by the planner is
369
379
  cheaper and matches Claude's own guidance ("EXPENSIVE … use ONLY when agents
370
380
  mutate files in parallel and would otherwise conflict").
@@ -0,0 +1,171 @@
1
+ # Dogfood 2026-08-29 — bullswarm builds bullswarm (outputSchema + planner refactor)
2
+
3
+ Observation log of the two dogfood runs that produced 0.14.0, kept verbatim
4
+ from the driving session's notes; the post-run defects below produced 0.14.1.
5
+
6
+
7
+ Goal file: `goal4.txt` (7 deliverables, single-implementer constraint). Repo branch: `feat/output-schema`.
8
+ Runtime: local worktree `bullswarm-rt` pinned at main (user: redispatch with local runtime, keep committing; no npm wait).
9
+ Home: default `~/.bullswarm` (heterogeneous pools) — user can `bullswarm workflow tui <shortId>`.
10
+
11
+ ## Attempt 1 — 01:43:37 Z, installed 0.13.1 — failed at validation, nothing ran
12
+ `autonomous workflow invalid (nothing ran): phases[0].steps[0](scout): template ref "{{outputs.x.data.field}}" cannot resolve`.
13
+ Cause: goal text spliced into the scout prompt; validator parsed a quoted ref in user text.
14
+ Fix (bullswarm defect #1): goal → declared `inputs.goal`, inserted at render time; unresolved grammar-valid refs are
15
+ left literal + `template.unresolved_ref` event instead of fatal. Commit `7badea3`, released 0.13.2 (`a0f0965`).
16
+ Verified: scout task file of attempt 3 contains `{{outputs.x.data.field}}` verbatim; only `{{inputs.goal}}` resolved.
17
+
18
+ ## Attempt 2 — 01:47:37 Z — launcher bug (mine, not bullswarm): zsh does not word-split `$BS="node path"`. Fixed script.
19
+
20
+ ## Attempt 3 — 01:48:19 Z, runtime 0.13.2 @ a0f0965 — run `wf-mtdq1l9v-ed22fe` / `75t4n2`
21
+ - 01:48:22 scout → pool `opencode2` model `kaihk/gpt-5.6-luna`. Routing: "most-behind capable pool (surplus 0)";
22
+ candidates opencode2 pace 0 (unmetered) > grok −7.5 > claude-code −9.4 > codex −49.1.
23
+ Observation: an unmetered pool reads as exactly on pace (0) and therefore outranks every metered pool that is
24
+ ahead of pace. Not a crash, but quota-unknown pools capture all work whenever metered pools are burning ahead.
25
+ - 01:48:22 → 01:49:54 scout ok (92 s, opencode2).
26
+ - 01:50:06 → 02:00:40 planner turn 1 (**~634 s** — roughly 2× the goal-2/3 turns; goal text is 6.5 k chars and the
27
+ program is 12 actions). Decision: `needs_more_work`, 12 actions, `completion: all-actions-ok` attached.
28
+ Program honours the single-implementer constraint: `impl-src` (all src) ∥ `docs`; `tests-schema`/`tests-adaptive`/
29
+ `tests-gaps` + `verify-impl(repair 2)` fan out after impl-src; per-writer verifies with repair; `final-report` →
30
+ `verify-suite(repair 2)`. Note: `verify-impl`'s repair may edit src while the test writers read it — accepted risk,
31
+ `verify-suite` runs the whole suite at the end.
32
+ - 02:00:31 `impl-src` and `docs` → opencode2 `kaihk/gpt-5.6-luna` (medium effort, "most-behind capable pool (surplus 0)").
33
+ All build work lands on the unmetered pool while claude-code/grok/codex are ahead of pace. Orchestrator stayed on
34
+ claude-code (pinned).
35
+ - 02:00:31 → 02:07:09 `impl-src` ok (398 s, opencode2); `docs` ok (140 s). 02:07:11 five actions started at once
36
+ (3 test writers + verify-impl + verify-docs).
37
+ - 02:08:20 `verify-impl` ok:false → `verify-impl-repair-1` (concerns were concrete and correct: schema JSON omitted from
38
+ the retry task text; fan-out item resume skipped schemaOk; no combined run+fanout skeleton). 02:08:36 `verify-docs`
39
+ ok:false → `verify-docs-repair-1` (doc claimed fan-out persists top-level data/schemaOk; doc claimed static validator
40
+ rejects outputSchema on verify but only decision.js did). Repairs 121 s / 124 s. Both are repair-loop live uses; if the
41
+ program then self-completes this is the first live exercise of the 0.13.1 fix path.
42
+ - 02:12 → 02:18 second round of repairs: `verify-impl-repair-2` (escalation could allow a 2nd schema retry — legit design
43
+ nit; "schema.js untracked so absent from git diff --stat" — verifier misreading), `verify-adaptive-tests-repair-1/2`
44
+ (rejected twice for the same reason: "diff is not purely additive: modifies an existing assertion" — the
45
+ programFeatures assertion HAD to change; an unrepairable process criterion). Both verifies ended ok:false after
46
+ maxRounds → `final-report`/`verify-suite` blocked (`failed_terminal: dynamic actions blocked by failed or unresolved
47
+ dependencies`) → program boundary → planner turn 2 at 02:18:52.
48
+ Observation (behaviour): a verifier that judges process criteria ("purely additive", "untracked file") instead of the
49
+ goal's acceptance checks produces rejections no repair can satisfy; two repair rounds (~10 min) were spent before the
50
+ boundary. Candidate for the prompt audit: verify doctrine "ok:false only for failed acceptance checks; process
51
+ observations are concerns" and/or runtime: identical concerns after a repair → boundary immediately.
52
+ - 02:18:52 → ~02:25:20 planner turn 2 (~390 s): `needs_more_work`, 3 sequential actions — `recheck-src` (verify, repair 2,
53
+ depends on verify-impl-repair-2) → `full-suite-report` (run) → `verify-final` (verify, repair 2). Reason correctly
54
+ notes no whole-suite evidence existed after the repairs (impl-src's npm test predated them).
55
+ - 02:25:52 → 02:32:36 follow-up program: `recheck-src` ok:false → `recheck-src-repair-1` → re-verify ok; `full-suite-report`
56
+ ok; `verify-final` ok → **`decision.auto_completed` (program-completion)** at 02:32:39.
57
+ ## Result (attempt 3)
58
+ - Wall **44 min 17 s** (2 654 s), 28 dispatches (26 opencode2 + 2 planner on claude-code), max concurrent 5,
59
+ parallelism 1.5, planner 2 turns / 1 045 s (39 % — turn 1 alone 637 s), repairs 6 (2 repaired ok, 4 re-verify
60
+ rejected), tokens ≈ 108 k (estimate).
61
+ - Deliverable: 10 files changed + 2 new (489+/38−); `npm test` **318/318** (299 + 19) on my own run; validator module,
62
+ decision/validate/runtime/runner/template changes all present; docs + changelog written.
63
+ - My review: sound; one robustness flaw fixed by hand — `readTrailingObject` used a reverse brace/quote scanner whose
64
+ escape handling is wrong scanning backwards (a `\"` inside a string could derail it and waste the single retry);
65
+ replaced with the parse-candidates approach `hasStructuredAnswer` already uses. Double failure now reports the
66
+ retry's errors. Escalation concern from verify-impl judged mistaken (escalation follows failed dispatches only).
67
+ - 0.13.1 fix path: NOT exercised here either — the latest worker at completion was `full-suite-report`, verified by a
68
+ direct edge (`verify-final`).
69
+ ## Adversarial review via `bullswarm run --lane analyze` (02:36 → 02:39 Z, 206 s, opencode2 kaihk/gpt-5.6-luna)
70
+ - Verdict "do not release" with **two confirmed, reproduced defects** — both in code I had reviewed and passed:
71
+ (1) my rewritten `readTrailingObject` returned the first schema-valid `{…}` from the right, so a nested object
72
+ could be recorded (`{"wrapper":{"ok":"inner"}}` → data `{"ok":"inner"}`); (2) `schema.js` used `in`, so
73
+ `toString`/`constructor`/`__proto__` counted as present/declared. Cleared: escaped quotes, fenced JSON, resume rules,
74
+ exactly-one retry, validator paths. Fixed + regression tests (320/320); my fix also had an infinite loop when output
75
+ starts with `{` (lastIndexOf clamps negative fromIndex) — caught by the suite hanging, fixed.
76
+ - Evidence for open item "adversarial verification by default": a 3-minute refute-framed review found what the
77
+ run's own verifies (6 rounds) and my manual review both missed.
78
+ ## Goal 5 — planner context/contract refactor — run `wf-mtds7tzx-95ab05` / `ejk9w2`, 02:49:09 Z, runtime 0.13.2 local
79
+ - scout 72 s (opencode2); planner turn 1 02:50:55 → 02:58:56 (~480 s): 6 actions, completion attached; noticed the goal's
80
+ stale "318 passing" and used the real 320. Program: impl-src → verify-src(repair 2) → update-tests ∥ update-docs →
81
+ verify-tests(repair 2) → verify-suite(repair 1).
82
+ - 02:58:56 → 03:09:04 impl-src (~610 s). verify-src rejected twice, both times on SUBSTANTIVE spec points (obsolete
83
+ skeleton text left in a comment; 6 JSON examples instead of 2; `<item>` instead of `{{item}}` in examples; excerpt
84
+ policy). One misread to check in the final diff: it called the existing 3 000/36 000-char excerpt caps a violation of
85
+ "full excerpt" although the goal said "(existing budget logic)" — the repair may have removed the caps.
86
+ Ordering tension: verify-src runs before update-tests, so it necessarily sees 5 failing old assertions; the planner
87
+ should either fold assertion updates into impl-src or make verify-src judge src only.
88
+ - Goal 5 finished 04:00:40 Z (71.5 min, 3 planner turns + program-completion, 15 dispatches, plannerSec 1 105+175).
89
+ Where the time went: 18–20 min planner turns; 12.4 min verify-src repair loop enforcing my over-exacting spec and
90
+ judging intermediate state; ~8 min decision-3 nit round (`align-prefix-number` + `confirm-docs`) triggered by an
91
+ UNPARSEABLE final-check verdict; the queued steer (03:57:32, "converge now") was NEVER delivered — 0 steering.delivered events; the run auto-completed (program-completion, 04:00:40) and deliverSteering only runs at planner gates, so auto-completion silently discards pending operator steering. Known issue; convergence came from the runtime, not the steer.
92
+ Deliverable reviewed + committed `15f1534`: 10-rule contract (2.2 k chars) + 2 examples (1.9 k) replace 16.3 k prefix;
93
+ compact ledger rows; id-only failures; 200-char stale excerpts; `decision.context_built` size event; 323/323.
94
+ - My follow-up (commit after 15f1534): verify verdict parse failure → ONE bounded re-ask (`verify.verdict_retry`,
95
+ test with a garbled-once verifier, 324/324); contract amendments (verify scoping, converge-not-polish, restored
96
+ shared-tree/redundant-verification/operatorSteering lines the merge dropped); re-budgeted "full" excerpts (the
97
+ uncapped version could have rebuilt the 163 k contexts).
98
+ - Speed answer to the user: ~35 of 66 min (at question time) was real work; fixes target the rest — re-ask (−8 min),
99
+ verify scoping (−12 min), convergence rule (−nit rounds). Remaining lever: planner turn latency itself (Opus
100
+ high-effort per boundary; context compaction cuts cost ~7×, latency is model thinking time).
101
+
102
+ ## Post-run defects → 0.14.1 (fixed directly, dogfooding paused by user direction)
103
+ - `workflow tui` crashed twice (`detailRow` dashboard.js:809 `JSON.parse` of
104
+ state.json mid-write; the throw escaped the repaint timer and killed the TUI,
105
+ stranding the terminal in alt-screen raw mode). Fix: atomic temp+rename
106
+ writes for state/report/workflow.json + torn-read-tolerant observation
107
+ readers + guarded paint/key handlers with last-good-frame fallback.
108
+ - TUI showed phase ✗ "2/2 complete" while a re-verify attempt was live. Fix:
109
+ an action with an active agent reads as running; phase precedence
110
+ active > failed > completed.
111
+ - The goal-5 steer was never delivered: auto-completion bypassed the planner
112
+ gate and silently discarded pending steering (0 steering.delivered events).
113
+ Fix: pending steering defers self-completion to the planner
114
+ (`decision.completion_deferred`); undeliverable steering is marked
115
+ `expired_undelivered` (`steering.expired`) at the terminal transition.
116
+ - Routing concentration on the unmetered pool (26/28 dispatches) confirmed as
117
+ design intent (quota protection outranks diversity) and documented in the
118
+ skill rather than changed.
119
+
120
+ ## Proof run 1 — goal-3 re-run `d8pr8s` (wf-mtdvuk9m), 0.14.1-pre @ 0bbe78c
121
+ Baseline (0.13.1, same fixture/goal/pool/flags): 28m42s wall, 1 planner turn, 294 s planner.
122
+ - **completed + verified, deliverable exactly right** (csv+slugify guarded, existing tests byte-identical, 63/63),
123
+ zero crashes, 8 workers, no repairs.
124
+ - Wall **32m19s**, planner **5 dispatches / 463 s** (195+114+58+66+30). Per-turn latency DOWN (max 195 s vs 294 s);
125
+ context per turn 6.2k–48k chars (`decision.context_built` measuring itself) vs the old 16.3k prefix + up-to-178k contexts.
126
+ - The 3 extra gates, each diagnosed and fixed in `c0ff947`:
127
+ 1. first proposal rejected — verify with several dependsOn lacked `review` → contract rule 4 now states it;
128
+ 2. evidence-policy boundary + rejected `complete` — final-report left as last unverified worker → rule 8 now
129
+ states the LAST worker must be covered by a verify;
130
+ 3. one deliberate operator steer — which **live-proved the 0.14.1 steering fix**: `decision.completion_deferred`
131
+ → `steering.delivered` (first ever observed; the goal-5 defect showed 0) → planner turn honoured it.
132
+ - Also observed and fixed: a corrective turn re-inflated validationFeedback to 24k chars (raw response duplicated
133
+ the parsed proposal) → excerpt capped at 2k.
134
+ - 0.13.1 completion-evidence policy exercised live for the first time: it refused auto-completion twice, correctly.
135
+
136
+ ## Proof run 2 — goal-3 re-run `djnjka` (wf-mtdx5htt), 0.14.1-pre @ c0ff947 — interrupted, then cancelled
137
+ - Turn-1 proposal **accepted first try** (no validation rejection, no correction turn): contract fix #1 confirmed.
138
+ Turn-1 context 6,201 chars.
139
+ - At 05:21 the driving session's background task was killed by the harness (not the user); SIGTERM reached the
140
+ runner, which persisted `interrupted` + resumable (1/3 steps, in-flight `triage` cancelled) — the 0.13 interruption
141
+ path working as designed.
142
+ - Resume at 05:54 exposed a **resume defect**: the cancelled `triage` was persisted `ok:false` ("workflow
143
+ cancellation requested"), so its 4 dependents were marked "blocked by failed or unresolved dependencies" and the
144
+ planner was asked to re-plan around a failure that never happened. Cancelled the run; fixed in `459c58c`
145
+ (cancelled actions + dependents blocked only by them are reopened on resume, event `action.reopened`; regression
146
+ test drives a cancel marker into a slow in-flight action and asserts exactly one further planner turn).
147
+
148
+ ## Proof run 3 — goal-3 re-run `4t6m5a` (wf-mtdyyqkw), 0.14.1-pre @ 459c58c — cancelled after diagnosis
149
+ - Turn-1 proposal (9 actions, sound shape: 3 disjoint impl workers ∥ audit of untouched modules → per-module
150
+ verifies → final-report → verify-final, completion attached) was **rejected** for one field: a zero-dependsOn
151
+ audit verify carried no `review`. Correction turn cost ~5 min and re-inflated context to 47.8k chars
152
+ (validationFeedback 38k — the 2k cap on rejectedResponseExcerpt was insufficient because rejectedProposal
153
+ itself is 37k). Root cause is the validator's posture, not the planner: rejecting a whole program for a field the
154
+ runtime can default. Fixed in `548eabe`: review defaults to the single/last dependency's artifact, or
155
+ `reviewScope: repository` for a no-dependency audit; contract rule 4 reworded. Run cancelled to re-prove cleanly.
156
+
157
+ ## Proof run 4 — goal-3 re-run `d7xyg2` (wf-mtdzhw88), 0.14.1-pre @ 548eabe — **PASS**
158
+ | metric | 0.13.1 baseline (x3x2a2-era, same fixture) | 0.14.1-pre run 4 |
159
+ | --- | ---: | ---: |
160
+ | outcome | completed, verified | completed, verified, **auto-completed** (program-completion) |
161
+ | wall | 28 min 42 s | 30 min 12 s |
162
+ | planner turns / plannerSec | 1 / 294 s | **1 / 269 s** |
163
+ | planner context (turn 1) | 32.7 k chars (measured on a sibling run) | **6.2 k chars** |
164
+ | dispatches (workers) | — | 8 (7) · max concurrent 9 |
165
+ | corrections / rejections / repairs / verdict re-asks | 0 / 0 / 0 / 0 | 0 / 0 / 0 / 0 |
166
+ | deliverable | csv + slugify guarded, 63/63 | csv + slugify guarded, **63/63**, existing test files byte-identical |
167
+ Program: scout → probe (all exports, wrong-type matrix) → fanout guards over the discovered modules → verify-guards →
168
+ report → verify-final, `completion: all-actions-ok`. Wall is within noise of baseline (+90 s, dominated by worker
169
+ model time: probe 4.7 min, guards 5.1 min, verify-guards 4.7 min); the planner side is faster and 5× smaller.
170
+ Zero observation crashes across four runs of TUI/watch/runs/result/static-tui polling and a 20 s stress loop
171
+ (1,681 paints against the live writer, 0 torn, 0 throws).
@@ -0,0 +1,157 @@
1
+ # Planner prompt and context audit — 2026-08-29
2
+
3
+ Question from the user: after the 0.11 → 0.14 iterations, does the instruction
4
+ set given to the orchestrator/planner still make sense, or should it be
5
+ simplified or refactored?
6
+
7
+ Method: measure what the planner actually receives, not what the source files
8
+ look like. The two planner task files of dogfood run `wf-mtdq1l9v-ed22fe`
9
+ (`75t4n2`, bullswarm building `outputSchema` in its own repo, runtime 0.13.2)
10
+ are the specimens; sizes are characters of the task text
11
+ (`task-orchestrator-*.md`), tokens ≈ chars / 4.
12
+
13
+ ## 1. What one planner turn receives
14
+
15
+ | section | source | turn 1 | turn 2 |
16
+ | --- | --- | ---: | ---: |
17
+ | orchestrator prompt + worktree line | `goal.js` `AUTONOMOUS_ORCHESTRATOR_PROMPT` | 5 000 | 5 000 |
18
+ | PLANNING DOCTRINE (11 bullets) | `runtime.js` runDecision | 3 605 | 3 605 |
19
+ | Action skeletons (6 shapes + verify semantics) | `runtime.js` | 1 342 | 1 342 |
20
+ | Program skeleton (discovery → fan-out → verify → suite) | `runtime.js` | 1 217 | 1 217 |
21
+ | Graph skeleton (two chains + suite) + fanout paragraph + runtime-owned line | `runtime.js` | 4 054 | 4 054 |
22
+ | durable context (JSON) | `runtime.js` plannerContext | 17 474 | 162 946 |
23
+ | **total** | | **32 730** (~8 k tokens) | **178 452** (~45 k tokens) |
24
+
25
+ The fixed prefix is 15.2 k chars on every turn. The durable context grew
26
+ **9×** between turn 1 and turn 2 of the same run.
27
+
28
+ ### Where the 163 k of turn 2 went
29
+
30
+ | key | chars | what it is |
31
+ | --- | ---: | --- |
32
+ | `completedActions` (19 entries) | 66 700 | every finished action **with its full attempt records**: routing candidates and pace numbers, usage, pricing table, child pid, timings — ~3.5 k per action |
33
+ | `outputs` (19 entries) | 41 560 | `outputExcerpt` of ~3 k chars for *every* finished action, including ones that finished ok and were already verified in the previous program |
34
+ | `intent` | 6 657 | the goal text (6.5 k) + cwd + policy — needed, once |
35
+ | `failures` | 1 073 | the two blocked actions — duplicates of `completedActions` entries |
36
+ | `availablePools`, `budget`, `executionConstraints`, `closedPhases`, … | ~1 800 | fine |
37
+
38
+ ## 2. What is said more than once
39
+
40
+ Reading the prefix as the planner does, the same rules appear two or three
41
+ times in different words:
42
+
43
+ | rule | orchestrator prompt | doctrine | skeletons |
44
+ | --- | --- | --- | --- |
45
+ | propose the whole program, one round trip costs minutes | item 2 | bullets 1, 2 | "all in ONE decision" ×2 |
46
+ | N items → N run + N verify + one suite verify | item 5 | "Per-item chains" | Graph skeleton |
47
+ | unknown items → discovery + `itemsFrom` fan-out | item 5 | "Unknown item count" | Program skeleton + fanout paragraph |
48
+ | every verify gets a `repair` policy | item 5 | "Verification failures" | verify skeleton |
49
+ | `completion: all-actions-ok` on a clean program | item 5 | "Self-completing programs" | — |
50
+ | self-contained worker prompts, file ownership | item 3 | "File ownership", "Self-contained prompts" | — |
51
+ | don't propose pool/addDir/taskFile | closing line | — | final line |
52
+
53
+ Item 5 of the orchestrator prompt alone is 1 050 chars and restates four
54
+ doctrine bullets. The two program skeletons both end in the same
55
+ `verify-items → verify-suite` tail.
56
+
57
+ ## 3. Does it matter? Measured
58
+
59
+ - Turn 1 (32.7 k chars) took **637 s**; turn 2 (178 k chars) took **390 s**.
60
+ Latency is therefore dominated by the model's reasoning on the goal, not by
61
+ context size — the 6.5 k-char goal and a 12-action program cost more thinking
62
+ than reading 45 k tokens. Trimming context is a **cost** and **attention**
63
+ lever, not primarily a latency lever.
64
+ - Cost: turn 2 read ~45 k tokens to emit a ~1 k-token decision. At Opus
65
+ prices that is ~$0.25 per boundary; a run with four boundaries (goal 2 on
66
+ 0.12.1) spends more on re-reading attempt metadata than on the decisions.
67
+ - Attention: the planner's turn-2 reason correctly diagnosed the blocked
68
+ graph, so quality did not visibly suffer here — but 64 k chars of pricing
69
+ tables and routing candidates are noise it must skip to find the two
70
+ `ok:false` concerns that matter.
71
+ - Behaviour observed in three runs (goal 2, goal 3, dogfood): every rule the
72
+ prefix repeats was followed on the first turn (whole program, per-item
73
+ chains, repair policies, `completion`). No observed decision needed a rule
74
+ to be stated twice.
75
+
76
+ ## 4. Recommendation
77
+
78
+ Yes — refactor, in two independent pieces, both measurable:
79
+
80
+ **A. Compact the durable context (the 9× growth).** Planner-facing ledger rows
81
+ instead of raw ledger entries: `{ id, type, phase, status, pool, durationSec,
82
+ attempts, why }` (~150 chars; 19 actions → ~3 k instead of 66.7 k). Keep a
83
+ full `outputExcerpt` only for actions finished **since the last decision** and
84
+ for every `ok:false` verify; older ok actions get a one-line summary (id, ok,
85
+ first 200 chars). Replace `failures` with the ids of failing actions (their
86
+ full entry already sits in the ledger). Expected turn-2 context: ~25 k chars
87
+ instead of 163 k. Pure runtime change; no planner behaviour change intended.
88
+
89
+ **B. One contract instead of three overlapping texts.** Merge
90
+ `AUTONOMOUS_ORCHESTRATOR_PROMPT` and the doctrine bullets into a single ordered
91
+ list of ~10 rules (target ≤ 4 k chars, from 8.1 k), each stated once with its
92
+ reason; keep exactly two JSON examples — the action shapes list and one
93
+ complete program (discovery → data-driven fan-out → per-item verify with
94
+ repair → suite verify, with `completion`) — and delete the second program
95
+ skeleton (target ≤ 3 k, from 6.6 k). Total prefix ≤ 7 k chars, from 15.2 k.
96
+
97
+ Acceptance for both: unit tests on the context builder (row shape, excerpt
98
+ policy by decision sequence) and on the prompt (each rule appears once; the
99
+ skeleton assertions in `tests/workflow-adaptive.test.js` updated); then one
100
+ re-run of goal 3 on the same fixture (baseline 0.13.1: 28 min 42 s, 1 planner
101
+ turn, 294 s) to confirm the decision shape is unchanged and record the new
102
+ per-turn size.
103
+
104
+ Not recommended: cutting the goal text or the scout excerpt from the context —
105
+ both were used verbatim by every first-turn program observed.
106
+
107
+ ## 5. Outcome
108
+
109
+ Measurements below were taken after the refactor from the current source and
110
+ from the committed source saved into `/tmp/goal-before.mjs` and
111
+ `/tmp/runtime-before.js`, using the same temporary measurement script. The
112
+ canonical prefix is the complete emitted planner task text counted from its
113
+ first character up to (not including) the durable-context marker, with the
114
+ worktree-isolation suffix included. The committed baseline predates the named
115
+ section exports, so its emitted prefix was reconstructed from the committed
116
+ `runtime.js` task-text assembly and the committed `AUTONOMOUS_ORCHESTRATOR_PROMPT`.
117
+
118
+ - Complete emitted planner task text up to the durable-context marker,
119
+ worktree-isolation suffix included: **OBSERVED**, `16,316` characters before
120
+ and `5,208` characters after. Command: `node /tmp/measure-planner.mjs`.
121
+ - Static planner task-prefix array through the durable-context boundary:
122
+ **OBSERVED**, `16,209` characters before and `5,101` characters after. The
123
+ after value is 107 characters shorter because it excludes the unchanged
124
+ worktree-isolation suffix; this is a secondary source-level measurement, not
125
+ the canonical emitted-prefix headline. Command: `node /tmp/measure-planner.mjs`.
126
+ - `PLANNER_RULES_SECTION`: **OBSERVED**, `2,202` characters after. Command:
127
+ `node /tmp/measure-planner.mjs`.
128
+ - `PLANNER_EXAMPLES_SECTION`: **OBSERVED**, `1,867` characters after. Command:
129
+ `node /tmp/measure-planner.mjs`.
130
+ - `AUTONOMOUS_ORCHESTRATOR_PROMPT`: **OBSERVED**, `4,670` characters after;
131
+ the committed before source had no separately exported rules or examples
132
+ sections, so separate before-section sizes are **NOT AVAILABLE**, not
133
+ inferred. Command: `node /tmp/measure-planner.mjs`.
134
+ - Sample durable context: **OBSERVED** baseline `163,000` characters in the
135
+ audit's rounded turn-2 durable-context total (the detailed table records
136
+ `162,946`; the full turn-2 task was `178,452`), and **COMPUTED** `23,547`
137
+ characters after. The computed sample applies 19
138
+ compact ledger rows at 150 characters each, keeps a 3,000-character scout
139
+ excerpt and two 3,000-character failing-verify excerpts, truncates the
140
+ other 16 action excerpts to 200 characters, represents two failures as
141
+ 20-character IDs, and retains the audit's 6,657-character intent and
142
+ 1,800-character other-context components. Command: `node /tmp/compute-context.mjs`.
143
+
144
+ Deliverables:
145
+
146
+ - Durable planner context shrank because completed actions are compact ledger
147
+ rows, failures are IDs, and stale successful output is truncated.
148
+ - Planner contract shrank because overlapping prompt/doctrine/skeleton text is
149
+ now one ordered rules section plus exactly two JSON examples, single-sourced
150
+ in `src/workflow/goal.js`.
151
+ - Runtime prompt construction shrank because `src/workflow/runtime.js` imports
152
+ the shared contract instead of carrying a duplicate doctrine and graph
153
+ skeleton.
154
+ - `skill/SKILL.md` was left unchanged: it documents the general durable
155
+ context and the separate run-state/TUI attempt view, but does not document a
156
+ renamed/dropped planner-context field shape such as the old attempt records
157
+ or an old `failures` representation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bullswarm",
3
- "version": "0.13.2",
3
+ "version": "0.14.1",
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
@@ -246,7 +246,12 @@ bullswarm workflow steer <shortId> --message "guidance for the next planner chec
246
246
  `capabilities` reports available pools, supported lanes, configured models,
247
247
  meter readings, burst gates, quarantine state, retry limits, and the important
248
248
  routing rule. Automatic routing chooses the highest time-adjusted quota surplus
249
- among capable pools. For strategic model selection, first run:
249
+ among capable pools. An unmetered pool reads as exactly on pace (surplus 0), so
250
+ whenever every metered pool is burning ahead of its window (negative
251
+ surplus) the unmetered pool wins ALL work — by design: quota protection
252
+ outranks provider diversity (observed 2026-08-29: 26 of 28 dispatches on
253
+ one unmetered pool). If that concentration is unwanted, meter the pool or
254
+ exclude its models via `strategy exclude-model`. For strategic model selection, first run:
250
255
 
251
256
  ```bash
252
257
  bullswarm strategy refresh
@@ -361,6 +366,11 @@ that expressible without extra turns:
361
366
  (`source: "program-completion"`, event `decision.auto_completed`) and the run
362
367
  ends without another planner turn; a failing action emits
363
368
  `decision.completion_predicate_unmet` and the boundary returns to the planner.
369
+ - `outputSchema` on a `run` or fan-out `stepTemplate` — declare it when a
370
+ downstream action needs reliable structured data, such as an object to render
371
+ into a dependent prompt or an `items` array for `fanout.itemsFrom`; leave it
372
+ off for ordinary prose; planner proposals must not put it on `verify`, whose
373
+ verdict shape is fixed.
364
374
 
365
375
  Every fan-out records a summary artifact as `outputs.<id>.outFile` and a
366
376
  boolean `ok` (item count in `succeeded`), so a verify may depend on a fan-out
@@ -401,6 +411,17 @@ Resume by shortId:
401
411
  bullswarm workflow draft run my-audit --resume <shortId> --json --quiet
402
412
  ```
403
413
 
414
+ ## Writing goals that converge
415
+
416
+ State outcomes, not measurements. A goal that fixes character counts, exact
417
+ event names, or cosmetic layout turns every verifier into a nit machine:
418
+ observed 2026-08-29 (run `ejk9w2`), a spec with hard numeric limits cost a
419
+ 12-minute verify/repair loop enforcing them against an intermediate state.
420
+ Say what must be true at the end (`npm test` passes, the planner receives one
421
+ contract stated once, context stays bounded) and let workers pick the numbers;
422
+ put any hard limit in ONE final acceptance check, not on every intermediate
423
+ verify.
424
+
404
425
  ## Writing prompts that the verify gate will accept
405
426
 
406
427
  The verify gate (`src/lib/verify.js`) flags outputs as `intent_only`
@@ -340,7 +340,7 @@ async function wfGoal(opts) {
340
340
  try {
341
341
  doc = existsSync(workflowPath)
342
342
  ? JSON.parse(readFileSync(workflowPath, 'utf8'))
343
- : JSON.parse(readFileSync(statePath, 'utf8'))._doc;
343
+ : readJsonForUpdate(statePath, 'workflow state')._doc;
344
344
  } catch (err) {
345
345
  console.error(`✗ cannot load durable workflow for ${resumeRunId}: ${err.message}`);
346
346
  return 1;