bullswarm 0.15.0 → 0.16.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,55 @@
1
1
  # bullswarm changelog
2
2
 
3
+ ## 0.16.0 — the planner sets the width; a re-verify judges the repair
4
+
5
+ - A re-verify after a repair round now receives the concerns it raised and the
6
+ repair's report, and may return ok:false only for an unresolved listed
7
+ concern or a regression; anything newly noticed is informational. Earned on
8
+ `r2vu9i`: `verify-src` round 2 rejected on two concerns round 1 never raised;
9
+ `verify-tests-runtime` round 2 rejected the very edit its round 1 demanded.
10
+ Live-proven on `bizp4s`: the one re-verify rejection was an `ENOENT`
11
+ regression in the acceptance checks, and its verdict opens "the two
12
+ original concerns are repaired".
13
+ - A phase is a pipeline stage: rule 3 now says one kebab-case name shared by
14
+ its actions, never one phase per action, and the complete-program example
15
+ uses five phases for eight actions (`verify` holds verify-fix, verify-tests
16
+ and verify-suite). Earned on `bizp4s`: the planner mirrored the example and
17
+ wrote sixteen one-action phases — no scheduling cost (phases never gate;
18
+ `dependsOn` does), but a TUI phase list carrying no information.
19
+ - Goal-4 rerun on this release (`bizp4s`, runtime `9af8fdf`, workers on
20
+ `kaihk/gpt-5.6-luna`): **25 min 13 s** (attempt 3: 44 min; 0.15.0: 72 min;
21
+ audited contract alone: 37 min), one planner turn (247 s, 16 % of wall),
22
+ parallelism 1.77, 3 repair rounds each fixing a real defect, 0 schema
23
+ retries, 0 corrections, auto-completed, 319/319, existing tests +174/−0.
24
+ - Goal-4 rerun on the audited contract (`r2vu9i`): 36 min 58 s (attempt 3:
25
+ 44 min; 0.15.0: 72 min), 5 parallel writers, parallelism 1.55, tests depend
26
+ on the implementation run rather than its verify, 0 schema retries, 0
27
+ corrections, auto-completed, 314/314.
28
+ - Planner contract audited against Claude Code's workflow-authoring reference
29
+ (three-lens review + adversarial verification, run on the real goal-4 task
30
+ text) and rewritten within the same caps (rules 3,999 / examples 2,938
31
+ chars). New in substance: a verdict is never data (depend on the run that
32
+ wrote your files, not on its verify); split to the width the tree allows
33
+ (one worker for N independent files is N chains in series); outputSchema
34
+ only where a later action reads the object, never on prose; a repair edits
35
+ files and cannot rewrite the answer under review; workers run their unit's
36
+ focused command, never the full suite; the planner sets `lane` and `effort`
37
+ per action. The complete-program example is now valid JSON and shows tests
38
+ running beside the src verify. `docs/planner-prompt-audit-2026-08-29.md` §6.
39
+ - Planner-proposed `lane`/`effort`/`requiresCapabilities` now survive the
40
+ gate defaults (`runner.js` spread order let `lane: build` overwrite every
41
+ proposal); `lane` is validated like `effort`.
42
+ - `outputSchema` output reading tolerates a closing markdown fence after the
43
+ trailing JSON object, and the schema instruction says the object is an
44
+ INSTANCE whose keys are the `properties` names (never the schema itself).
45
+ Earned on the goal-4 rerun `ydpjts` (0.15.0): a stray `"type"` key and then
46
+ a `}\n```` tail spent the single schema retry and a 279 s planner turn on
47
+ an otherwise complete report (≈ 11 min).
48
+ - Goal-4 rerun recorded in `docs/experiments/2026-08-29-dogfood-bullswarm-builds-bullswarm.md`:
49
+ 0 repairs (attempt 3: 6), planner 17 % of wall (39 %), 326/326 — but 72 min
50
+ vs 44 min because every worker landed on the slowest most-behind pool and
51
+ the program ran serially (parallelism 1.05).
52
+
3
53
  ## 0.15.0 — extra Claude Code logins as separate pools
4
54
 
5
55
  - Claude Code extra logins (`~/.claude-<slug>` / `$CLAUDE_CONFIG_DIR`) become
@@ -169,3 +169,141 @@ report → verify-final, `completion: all-actions-ok`. Wall is within noise of b
169
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
170
  Zero observation crashes across four runs of TUI/watch/runs/result/static-tui polling and a 20 s stress loop
171
171
  (1,681 paints against the live writer, 0 torn, 0 throws).
172
+
173
+ ## Goal-4 rerun on 0.15.0 — `ydpjts` (wf-mte8azjz-bcb079), 10:19:31 → 11:31:48 Z — reliability PASS, speed FAIL
174
+
175
+ Same goal text (`goal4.txt`), same flags (`--orchestrator claude-code --concurrency 8 --max-agents 40
176
+ --max-expansion-rounds 8`), fresh fixture `g4-bs-v2` = `git archive a0f0965` (v0.13.2, 299/299, no `schema.js`) on
177
+ branch `feat/output-schema`, default `~/.bullswarm` home, runtime `bullswarm-rt` @ 728231d (v0.15.0).
178
+
179
+ | metric | attempt 3 (0.13.2) | **rerun (0.15.0)** |
180
+ | --- | ---: | ---: |
181
+ | outcome | auto-completed after planner turn 2 | **auto-completed** after a 2-action recovery program |
182
+ | wall | 44 min 17 s | **72 min 14 s** (4 334 s) |
183
+ | planner turns / plannerSec | 2 / 1 045 s (39 %; turn 1 = 634 s) | **2 / 755 s (17 %)** — 476 s + 279 s; completion recorded by the runtime |
184
+ | planner context (turn 1 / turn 2) | — | 12.9 k / 41.5 k chars (`decision.context_built`) |
185
+ | dispatches | 28 (26 opencode2 + 2 planner) | **13** — 2 planner (opus-5) + 11 workers (sonnet-5), all on `claude-code` |
186
+ | max concurrent / parallelism | 5 / 1.5 | **2 / 1.05** |
187
+ | repairs | 6 (4 unrepairable: process-criteria rejections) | **0** — every verify ok:true first round |
188
+ | corrections / rejections / verdict re-asks | 0 / 0 / — | 0 / 0 / 0 |
189
+ | schema retries | — | 1 (`report`; both attempts failed, see below) |
190
+ | tests after | 318/318 (299 + 19) | **326/326** (299 + 27); existing test files extended only (+281 / −0); no commit; version untouched |
191
+
192
+ Program (turn 1, 476 s): `impl-src` ∥ `docs` → `verify-src`(repair 2) / `verify-docs`(repair 1) → `tests` →
193
+ `verify-tests`(repair 2) → `report`(outputSchema) → `verify-report`(repair 1, `review` defaulted), `completion`
194
+ attached. Three verifies omitted `review` and were accepted (0.14.1 defaulting) — the same omission cost a 5-minute
195
+ correction turn on proof runs 1 and 3. Rule 8 honoured (last worker covered by a verify).
196
+
197
+ Verifier behaviour is what 0.14.x was meant to produce: `verify-src` passed with a disclosed deviation
198
+ (`programFeatures` literal not extended because `tests/workflow-adaptive.test.js:206` regex-locks it and the goal
199
+ forbids modifying existing assertions) instead of rejecting on a process criterion; `verify-tests` flagged the filler
200
+ prose workaround as a concern, not a failure.
201
+
202
+ **The one failure and its cost (≈ 11.4 min):** `report` carried an `outputSchema`. Attempt 1 ended with an object
203
+ carrying a stray `"type"` key copied from the schema (`additionalProperties:false` → `type is not allowed`, correct).
204
+ The single retry ended `}\n\`\`\`` — a closing markdown fence — and `readTrailingObject` refused it as "did not end
205
+ with a JSON object" → `report` ok:false → `verify-report` blocked → `decision.completion_predicate_unmet` → planner
206
+ turn 2 (279 s), which diagnosed it correctly and re-delivered the report without a schema. Fixed in `393a914`:
207
+ trailing fences are stripped before the object is read, and the instruction says the object is an INSTANCE whose keys
208
+ are the `properties` names. Unit-tested; not yet exercised live.
209
+
210
+ **Where the 72 minutes went — the run was serial (parallelism 1.05):** scout 405 s → planner 476 s → `impl-src`
211
+ **1 231 s** → `verify-src` 330 s → `tests` **951 s** → `verify-tests` 116 s → `report` 141 + 123 s → planner 279 s →
212
+ `final-summary` 174 s → `verify-final-summary` 109 s. Two causes, neither a crash:
213
+ 1. Routing put every worker on `claude-code`/`claude-sonnet-5` because it was the most-behind capable pool
214
+ (pace +6.9 vs opencode2 0, `claude-code:wati` −14.8). The same `impl-src` took 398 s on `opencode2`
215
+ (`kaihk/gpt-5.6-luna`) in attempt 3; the read-only scout took 405 s vs 92 s. Worker model speed is not part of the
216
+ surplus formula.
217
+ 2. The planner proposed ONE `tests` worker after `verify-src` instead of three file-disjoint test writers in parallel
218
+ with the src verify (attempt 3's shape). Reliability-first, width-second: correct under the goal's single-implementer
219
+ constraint, but it lengthened the critical path by ~16 min.
220
+
221
+ Pass conditions set before the run: ≤ 1 repair round ✓ (0); 0 process-criteria rejections ✓; auto-completed ✓;
222
+ ≤ 35 min ✗ (72 min). Reliability at 12-action complexity is now proven on ≥ 0.14.1; speed is worker-bound and
223
+ routing-bound.
224
+
225
+ ## Goal-4 rerun on the audited contract — `r2vu9i` (wf-mtefmdie-b39e8f), 13:44:19 → 14:21:21 Z — **structural PASS, 36 min 58 s**
226
+
227
+ Runtime `d14c1fa` (contract from the second audit, §6 of `docs/planner-prompt-audit-2026-08-29.md`, plus the runner
228
+ lane fix); workers pinned to `opencode2`/`kaihk/gpt-5.6-luna` via `strategy assign high|medium|low` (cleared after);
229
+ orchestrator `claude-code`/`claude-opus-5`; fresh fixture `g4-bs-v3` = a0f0965 (299/299).
230
+
231
+ | metric | attempt 3 (0.13.2) | `ydpjts` (0.15.0) | **`r2vu9i` (audited contract)** |
232
+ | --- | ---: | ---: | ---: |
233
+ | wall | 44 min 17 s | 72 min 14 s | **36 min 58 s** (2 219 s) |
234
+ | planner turns / plannerSec | 2 / 1 045 s (39 %) | 2 / 755 s (17 %) | 2 / 648 s (29 %) — 409 s + 238 s |
235
+ | dispatches | 28 | 13 | 26 (24 gpt-5.6-luna workers + 2 opus planner) |
236
+ | max concurrent / parallelism | 5 / 1.5 | 2 / 1.05 | **4 / 1.55** |
237
+ | writers in parallel after planning | 2 (+3 test writers later) | 2 | **5** (impl-src ∥ 3 docs; 2 test writers as soon as impl-src landed) |
238
+ | repairs (rounds / repaired ok / re-verify rejected) | 6 / 2 / 4 | 0 | 4 / 2 / 2 |
239
+ | schema retries / corrections / verdict re-asks | — / 0 / — | 1 / 0 / 0 | **0 / 0 / 0** |
240
+ | tests after | 318 | 326 | **314/314** (299 + 15); existing tests +167 / −1 (one assertion extended, as goal item 5 requires); no commit; version untouched |
241
+
242
+ Structural pass conditions (set before launch): `tests-*` depend on `impl-src`, not `verify-src` ✓ (the planner's own
243
+ reason: "two file-disjoint test workers depend on that run (not its verify)"); more than one file-disjoint writer ✓ (5);
244
+ parallelism ≥ 1.5 ✓ (1.55); 0 corrections ✓; 0 schema retries ✓ (the report carried no schema); auto-completed ✓;
245
+ ≥ 299 tests, existing tests extended only ✓. Failed: 0 repairs ✗ (4 rounds). Lane/effort proposed by the planner reached
246
+ dispatch (`docs-changelog` routed `chore`/`low`) — the runner merge fix is live. `accept-suite` was a verify with no
247
+ dependsOn — the first live exercise of the repository-scope branch.
248
+
249
+ Where the four repair rounds went, and what each says:
250
+ - `verify-tests-schema` round 1: missing invalid-`minimum` case → repaired, re-verify ok. Legitimate; cost 49 + 30 s.
251
+ - `verify-src` round 1: a real defect (`recordOutput` dropped `data`/`schemaOk`, breaking resume-safety) → repaired.
252
+ Legitimate. Round 2 then rejected on two concerns round 1 never raised (enum structural equality, root-path naming)
253
+ → repaired, re-verify ok. **Moving goalposts**: 92 + 268 s spent on nits that should have been round-1 concerns.
254
+ - `verify-tests-runtime` round 1: acceptance command failed on the pre-existing `workflow-adaptive.test.js:206`
255
+ assertion (goal item 5 requires extending it) → the repair extended it → round 2 rejected BECAUSE an existing test
256
+ was modified — a process criterion the planner had written into the verify prompt ("append tests only") that
257
+ contradicts the goal. Unrepairable by construction → `verify-suite`/report blocked → planner turn 2 (238 s), which
258
+ diagnosed "a false rejection" and recovered in 207 s. Cost ≈ 10 min.
259
+ Fix shipped after the run (`9af8fdf`, unit-tested, not yet live): a re-verify receives the concerns it raised and the
260
+ repair's report and may reject only for an unresolved listed concern or a regression — both round-2 rejections above
261
+ become informational under it.
262
+
263
+ Speed accounting vs `ydpjts`: worker pool (gpt-5.6-luna vs sonnet-5) and width together took the critical path from
264
+ 72 to ~27 min of productive work; the remaining ~10 min is the verifier-behaviour waste above.
265
+
266
+ ## Goal-4 rerun with the re-verify fix — `bizp4s` (wf-mtehhbwd-7db3de), 14:36:26 → 15:01:39 Z — **PASS, 25 min 13 s**
267
+
268
+ Runtime `9af8fdf` (audited contract + runner lane fix + re-verify scoping); workers pinned to
269
+ `opencode2`/`kaihk/gpt-5.6-luna` via `strategy assign high|medium|low` (cleared after, assignments `{}`); orchestrator
270
+ `claude-code`/`claude-opus-5`; fresh fixture `g4-bs-v4` = a0f0965 (299/299, cloned clean from the v3 fixture commit).
271
+
272
+ | metric | attempt 3 (0.13.2) | `ydpjts` (0.15.0) | `r2vu9i` (audited contract) | **`bizp4s` (+ re-verify fix)** |
273
+ | --- | ---: | ---: | ---: | ---: |
274
+ | wall | 44 min 17 s | 72 min 14 s | 36 min 58 s | **25 min 13 s** (1 513 s) |
275
+ | planner turns / plannerSec | 2 / 1 045 s (39 %) | 2 / 755 s (17 %) | 2 / 648 s (29 %) | **1 / 247 s (16 %)** |
276
+ | dispatches | 28 | 13 | 26 | 23 (22 gpt-5.6-luna + 1 opus planner) |
277
+ | max concurrent / parallelism | 5 / 1.5 | 2 / 1.05 | 4 / 1.55 | **4 / 1.77** |
278
+ | writers in parallel after planning | 2 (+3) | 2 | 5 | 4 (impl-src ∥ 3 docs), then 2 test writers ∥ verify-src the moment impl-src landed |
279
+ | repairs (rounds / repaired ok / re-verify rejected) | 6 / 2 / 4 | 0 | 4 / 2 / 2 | 3 / 2 / 1 — every rejection a real defect |
280
+ | schema retries / corrections / verdict re-asks | — / 0 / — | 1 / 0 / 0 | 0 / 0 / 0 | **0 / 0 / 0** |
281
+ | tests after | 318 | 326 | 314/314 | **319/319** (299 + 20); existing tests +174 / −0; no commit; version untouched |
282
+
283
+ Program (one decision, 15 actions + completion): `impl-src`(build/high) ∥ `doc-changelog`(chore/low) ∥
284
+ `doc-skill`(chore/low) ∥ `doc-mechanics`(chore/medium), each doc with its own analyze/low verify; `test-schema`
285
+ (build/medium), `test-runtime`(build/high) and `verify-src`(analyze/high) all `dependsOn: ["impl-src"]`; `verify-suite`
286
+ (analyze/low) on the six unit verifies; `report`(chore/low, no schema) → `verify-report`(analyze/medium);
287
+ `completion: all-actions-ok`. Every planner-set effort tier reached dispatch (`configured <tier> assignment`).
288
+ `decision.auto_completed` — the planner was consulted exactly once.
289
+
290
+ The three repair rounds, and why none is verifier waste:
291
+ - `verify-src` round 1 (ok:false): `validateWorkflow` did not check `stepTemplate.outputSchema` and the runtime ignored it
292
+ during fan-out — the fan-out half of the goal was unimplemented. Repair 197 s.
293
+ - `verify-src` re-verify round 1 (ok:false, `action.reverify_rejected`): the repair's schema-retry path handed dispatch a
294
+ file name that dispatch re-suffixed `-attempt-2`, so the runtime read a nonexistent file (`ENOENT`; 3 focused tests
295
+ failing). A regression in the acceptance checks — exactly the rejection the new scoping still allows. The task text
296
+ carried `RE-VERIFY round 1 of 2 … Concerns you raised (verbatim):` with round 1's concerns, and the verdict's summary
297
+ opens "The two original fan-out concerns are repaired" — the verifier judged the repair, as instructed. Repair 177 s;
298
+ round 2 re-verify ok (52/52 focused).
299
+ - `verify-test-schema` round 1 (ok:false): no invalid-value case per supported keyword. Repair 63 s, re-verify ok.
300
+ - `verify-test-runtime` accepted first time: the goal's item-5 assertion was satisfied without touching an existing test
301
+ (+174 / −0), so the "append only" tension of `r2vu9i` never arose.
302
+
303
+ Pass conditions (set before `r2vu9i`): tests depend on `impl-src` ✓; > 1 file-disjoint writer ✓ (4, then 2 more);
304
+ parallelism ≥ 1.5 ✓ (1.77); 0 corrections ✓; 0 schema retries ✓; auto-completed ✓; ≥ 299 tests, existing tests only
305
+ extended ✓. "0 repairs" ✗ as a literal count (3), but the condition's intent — no repair round that a verifier caused —
306
+ is met: each round fixed a defect the deliverable needed fixed. Prediction before launch was "the two moving-goalpost
307
+ rounds and the 10-min recovery turn vanish, wall ≈ 30 min"; observed 25 min 13 s with one planner turn.
308
+
309
+ Cost: 22 of 23 dispatches on the unmetered opencode2 seat; Claude quota spent on one 247 s planner turn.
@@ -155,3 +155,36 @@ Deliverables:
155
155
  context and the separate run-state/TUI attempt view, but does not document a
156
156
  renamed/dropped planner-context field shape such as the old attempt records
157
157
  or an old `failures` representation.
158
+
159
+ ## 6. Second audit — after the goal-4 rerun on 0.15.0 (`ydpjts`)
160
+
161
+ Method: a Claude Code dynamic workflow (12 agents, 16 min) reviewed the COMPLETE turn-1 task text of run `ydpjts`
162
+ (19,577 chars; 6,199-char prefix) and the program the planner wrote, from three lenses — width/critical path,
163
+ worker-prompt authoring, clarity vs Claude's own workflow-authoring reference — and adversarially verified every new
164
+ finding (refute-framed, source-checked). 3 known + 6 new findings survived; 2 were refuted (one because `lane` was
165
+ dead text — see the runtime defect below — one for deleting a must-keep rule).
166
+
167
+ What the planner received said nothing wrong; it said too little in three places, and the example showed a linear
168
+ program:
169
+
170
+ | finding | evidence on `ydpjts` | contract change |
171
+ | --- | --- | --- |
172
+ | a verify's verdict was treated as a dependency | `tests` dependsOn `verify-src`; 330 s idle | rule 3: "a worker depends on the run that wrote its input files, never on that run's verify (a verdict is not data)" |
173
+ | width framed only as "known N items" | one `tests` worker, 951 s, where three file-disjoint writers were allowed | rule 4: "each file-disjoint unit ... one worker for N independent files is N chains in series" |
174
+ | outputSchema invited on a prose report | `report` schema → stray key, then fenced tail → retry + planner turn (≈ 685 s) | rule 6: schema only where a later action reads the object; prose gets none |
175
+ | repair cannot rewrite an answer under review (new) | `verify-report` repair prompt asked the worker to "restate the report" — unreadable by design (`repairAndReverify` re-reads the original outFile) | rule 7: "the repair edits files and cannot rewrite the answer under review, so reject only what a file edit can fix" |
176
+ | workers demanded `npm test` as acceptance (new) | `tests` prompt: "No other worker is editing the tree while you run" | shared-tree line: unit's focused command, never the full suite |
177
+ | no guidance on `effort`/`lane` (new) | every action ran at build/medium | rule 10: planner sets lane and effort; they pick the model tier |
178
+ | example program was linear and not valid JSON (new) | — | example rewritten: `{"actions":[…],"completion":…}`, tests depend on fix and run beside verify-fix, lane/effort shown |
179
+ | runtime: planner `lane` silently overwritten (new) | `runner.js` merged `{...action, ...actionDefaults}` so `lane: build` won | fixed: action overrides gate defaults; addDir stays runtime-owned; `lane` validated |
180
+
181
+ Sizes after: rules **3,999** (cap 4,000), examples **2,938** (cap 3,000). Every rule still stated once with its reason.
182
+ Acceptance: goal-4 rerun on the new contract with workers pinned to `opencode2`/`kaihk/gpt-5.6-luna` (strategy
183
+ assignments high/medium/low, cleared after) — structural pass conditions: tests depend on `impl-src` not
184
+ `verify-src`; more than one file-disjoint writer; parallelism ≥ 1.5; 0 repairs / corrections / schema retries;
185
+ auto-completed; ≥ 299 tests, existing tests extended only; wall reported with the pool mix.
186
+
187
+ Outcome: `r2vu9i` (runtime `d14c1fa`) met every structural condition except "0 repairs" (4 rounds, 2 of them verifier
188
+ moving-goalposts → re-verify scoping `9af8fdf`); `bizp4s` (runtime `9af8fdf`) then ran in 25 min 13 s with one planner
189
+ turn (247 s), parallelism 1.77, 3 repair rounds each fixing a real defect, 0 schema retries / corrections, 319/319.
190
+ Details in `docs/experiments/2026-08-29-dogfood-bullswarm-builds-bullswarm.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bullswarm",
3
- "version": "0.15.0",
3
+ "version": "0.16.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": {
@@ -181,6 +181,9 @@ export function validateDecisionProposal(proposal, {
181
181
  action.requiresCapabilities.some((capability) => typeof capability !== 'string' || !ID_RE.test(capability)))) {
182
182
  issues.push(`${at}.requiresCapabilities must contain kebab-case names`);
183
183
  }
184
+ if (action.lane != null && !['analyze', 'build', 'chore'].includes(action.lane)) {
185
+ issues.push(`${at}.lane must be analyze|build|chore`);
186
+ }
184
187
  if (action.effort != null && !['high', 'medium', 'low'].includes(action.effort)) {
185
188
  issues.push(`${at}.effort must be high|medium|low`);
186
189
  }
@@ -8,25 +8,25 @@ import { resolve } from 'node:path';
8
8
  const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
9
9
 
10
10
  export const PLANNER_RULES_SECTION = [
11
- '1. Compile the whole program in one decision: the runtime executes all proposed actions and consults you only at a finished-or-blocked boundary, so deferring decidable work costs another round trip.',
12
- '2. Make every worker prompt self-contained: include the exact goal, absolute cwd, owned files and a no-other-files boundary, expected artifact, acceptance command, and report format, because workers see only their own prompt.',
13
- '3. Use short kebab-case, forward-only phases and dependsOn only for real data or same-file ordering; recovery uses a new phase and never repeats an identical failed plan.',
14
- '4. For known N items, create N run plus N verify actions, each verify depending only on its own run, then one suite verify depending on all; a verify judges the artifact in review (default: its single or last dependency; a verify with no dependsOn audits the repository directly).',
15
- '5. For unknown items, create discovery ending with RETURN ONLY a JSON object containing an items array, then data-driven fan-out via itemsFrom outputs.<id>.outFile or outputs.<id>.data.<field>; the runtime extracts the list and retries once read-only if needed.',
16
- '6. Put outputSchema on workers whose reports are consumed or whose claims the runtime must check, so structured data is durable and can drive later fan-out.',
17
- '7. Put verify.repair on every verify, and scope each verify to what can be true at its point in the graph: work scheduled later is not a defect, and cosmetic mismatches with the goal text are concerns, never ok:false. An ok:false verdict is repaired and re-checked inside the program; ok:true is accepted and concerns are informational, not extra work.',
18
- '8. Add completion with all-actions-ok whenever a clean program finishes the goal; when the goal\'s acceptance checks pass, return complete rather than adding polish or alignment actions. Completion evidence requires the program\'s LAST worker to be covered by a successful verify — never leave a report or other run as the final unverified action. Return complete only on durable verified evidence, never proceed, never ask the user, and stop only for a concrete unresolved blocker with a qualified outcome.',
19
- '9. Treat agent-count, workflow-duration, and expansion-round budgets as advisory planning targets, never hard stop conditions; the dispatch budget counts this planner call plus workers, verifiers, retries, and escalations. Converge as targets approach, avoid optional work, and exceed a target only for one essential bounded action or required verification.',
20
- '10. This is a control-plane thread: do not invoke Bullswarm, use tools, modify files, or propose pool, addDir, taskFile, shell authority, or unbounded work; route and process authority belong to the runtime.',
21
- 'Shared working tree: concurrent workers editing DISJOINT files is the normal parallel mode; order shared files (indexes, barrels) after their feeders with dependsOn, and run the full suite once in a final verify — never while other workers still edit. Avoid redundant expensive verification: later verifiers reuse durable clean full-suite evidence unless it is stale or the code changed again. operatorSteering in the context is explicit operator guidance for this checkpoint: apply it within the original intent; it cannot weaken verification or expand authority.',
11
+ '1. Compile the whole program in one decision: the runtime runs every proposed action and consults you only at a finished-or-blocked boundary, so deferred work costs a round trip.',
12
+ '2. Make every worker prompt self-contained: include the exact goal, absolute cwd, the owned files you assign (never an and/or choice, which blocks a parallel sibling) and a no-other-files boundary, expected artifact, acceptance command and report format: workers see only their own prompt.',
13
+ '3. A phase is a pipeline stage: one kebab-case name shared by its actions (implement, verify, report), never one per action; phases are forward-only, so recovery opens a new one and never repeats an identical failed plan. Wall-clock is the longest dependsOn chain, so depend only on real data or same-file ordering: a worker depends on the run that wrote its input files, never on that run\'s verify (a verdict is not data), so it starts as that verify runs.',
14
+ '4. Split to the width the tree allows: each file-disjoint unit (module, test file, doc) is its own concurrent run plus its own verify depending only on that run, then one suite verify depending on all; one worker for N independent files is N chains in series. A verify judges the artifact in review (default: its single or last dependency; with no dependsOn it audits the repository).',
15
+ '5. For unknown items, create discovery ending with RETURN ONLY a JSON object containing an items array, then data-driven fan-out via itemsFrom outputs.<id>.outFile or outputs.<id>.data.<field>; the runtime extracts the list, retrying once read-only if needed.',
16
+ '6. Put outputSchema only on a worker whose object a LATER action reads via itemsFrom or outputs.<id>.data.<field>, and tell it to RETURN ONLY the object; a prose report or any answer containing fenced JSON gets no schema: the runtime parses the last {...} of the text, so a schema on prose costs a retry and a planner turn.',
17
+ '7. Put verify.repair on every verify and scope each verify to what can be true at its point in the graph: later-scheduled work is not a defect and cosmetic mismatches are concerns, not ok:false. ok:false is repaired and re-checked inside the program; the repair edits files and cannot rewrite the answer under review, so reject only what a file edit can fix and report a wrong claim as a concern with the true value; ok:true is accepted and its concerns are informational.',
18
+ '8. Add completion with all-actions-ok whenever a clean program finishes the goal; when its acceptance checks pass, return complete rather than adding polish actions. The program\'s LAST worker must be covered by a successful verify — never end on an unverified report or run. Return complete only on durable verified evidence, never proceed, never ask the user, and stop only for a concrete unresolved blocker with a qualified outcome.',
19
+ '9. Budgets (agents, duration, expansion rounds) are advisory targets, never hard stops; the dispatch budget counts this planner call plus workers, verifiers, retries and escalations. Converge as targets approach: skip optional work; exceed a target only for one essential action or a required verification.',
20
+ '10. Never propose pool, addDir, taskFile, shell authority, or unbounded work: routing is the runtime\'s. Set lane (analyze to read or judge, build to edit, chore for mechanical steps) and effort (low for checks and mechanical edits, high only where judgement decides) per action or repair; they pick the model tier (unset: build, medium).',
21
+ 'Shared working tree: workers editing DISJOINT files concurrently is the normal mode; order shared files (indexes, barrels) after their feeders with dependsOn. Workers and unit verifies run their unit\'s focused command, never the full suite, which sees files siblings still write; the suite runs once, in the final verify, after all editing and repair ends; later verifiers reuse it unless code changed. operatorSteering is operator guidance for this checkpoint: apply it within the original intent; it cannot weaken verification or expand authority.',
22
22
  ].join('\n');
23
23
 
24
24
  export const PLANNER_EXAMPLES_SECTION = [
25
25
  'Action shapes:',
26
- '[{"type":"run","phase":"implement","prompt":"..."},{"type":"run","phase":"report","prompt":"...","outputSchema":{"type":"object"}},{"type":"fanout","phase":"fix","items":["alpha"],"stepTemplate":{"prompt":"Handle {{item}}."}},{"type":"fanout","phase":"fix","itemsFrom":"outputs.discover.outFile","stepTemplate":{"prompt":"Handle {{item}}."}},{"type":"verify","phase":"verify","prompt":"Check the artifact.","repair":{"prompt":"Fix rejected concerns.","maxRounds":1}}]',
27
- 'Complete program:',
28
- '[{"id":"discover","type":"run","phase":"discover","prompt":"In /abs/repo discover items and end with RETURN ONLY a JSON object containing an items array of item names.","outputSchema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"string"}}},"required":["items"]}},{"id":"fix","type":"fanout","phase":"fix","itemsFrom":"outputs.discover.data.items","stepTemplate":{"prompt":"In /abs/repo edit only the files for {{item}} and run its focused acceptance command."},"dependsOn":["discover"]},{"id":"verify-items","type":"verify","phase":"verify-items","prompt":"Independently verify every item artifact.","dependsOn":["fix"],"repair":{"prompt":"Fix each rejected item in /abs/repo and re-run its focused command.","maxRounds":2}},{"id":"verify-suite","type":"verify","phase":"verify-suite","prompt":"Run the full acceptance command in /abs/repo.","dependsOn":["verify-items"],"repair":{"prompt":"Fix the suite failure in /abs/repo and rerun the suite.","maxRounds":1}}],"completion":{"when":"all-actions-ok","reason":"The item checks and final suite verification prove the goal."}]',
29
- 'Rules the validator enforces: action type is run, fanout, or verify; fanout has stepTemplate and either items or itemsFrom; verify.review, when given, is outputs.<id>.outFile; dependsOn names existing or proposed actions; runtime-owned fields are rejected.',
26
+ '[{"type":"run","phase":"implement","prompt":"..."},{"type":"run","phase":"inventory","lane":"chore","effort":"low","prompt":"... RETURN ONLY a JSON object.","outputSchema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"string"}}},"required":["items"]}},{"type":"fanout","phase":"fix","items":["alpha"],"stepTemplate":{"prompt":"Handle {{item}}."}},{"type":"verify","phase":"verify","lane":"analyze","prompt":"Check the artifact.","repair":{"prompt":"Fix rejected concerns.","maxRounds":1}}]',
27
+ 'Complete program (tests depend on fix, not verify-fix, so both run at once; five phases for eight actions):',
28
+ '{"actions":[{"id":"discover","type":"run","phase":"discover","lane":"chore","effort":"low","prompt":"In /abs/repo list modules needing work; RETURN ONLY a JSON object with an items array.","outputSchema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"string"}}},"required":["items"]}},{"id":"fix","type":"fanout","phase":"fix","itemsFrom":"outputs.discover.data.items","dependsOn":["discover"],"stepTemplate":{"prompt":"In /abs/repo edit only src/{{item}}.js and run node --test tests/{{item}}.test.js."}},{"id":"verify-fix","type":"verify","phase":"verify","dependsOn":["fix"],"prompt":"Check each fixed module against the spec.","repair":{"prompt":"Fix rejected concerns in /abs/repo and rerun that module\'s test.","maxRounds":2}},{"id":"tests","type":"fanout","phase":"tests","itemsFrom":"outputs.discover.data.items","dependsOn":["fix"],"stepTemplate":{"prompt":"In /abs/repo write only tests/{{item}}.guards.test.js and run node --test on it."}},{"id":"verify-tests","type":"verify","phase":"verify","dependsOn":["tests"],"prompt":"Check the new tests are non-vacuous.","repair":{"prompt":"Fix rejected tests in /abs/repo.","maxRounds":1}},{"id":"verify-suite","type":"verify","phase":"verify","dependsOn":["verify-fix","verify-tests"],"effort":"low","prompt":"Run npm test in /abs/repo.","repair":{"prompt":"Fix the suite failure in /abs/repo and rerun it.","maxRounds":1}},{"id":"report","type":"run","phase":"report","lane":"chore","effort":"low","dependsOn":["verify-suite"],"prompt":"In /abs/repo list each changed file with a reason and quote the suite tail; plain markdown."},{"id":"verify-report","type":"verify","phase":"report","dependsOn":["report"],"prompt":"Check each claim against git status and a fresh suite run; a wrong number is a concern with the true value.","repair":{"prompt":"Fix any real repository defect in /abs/repo.","maxRounds":1}}],"completion":{"when":"all-actions-ok","reason":"Fix, tests, suite and report are each verified."}}',
29
+ 'Rules the validator enforces: action type is run, fanout, or verify; fanout has stepTemplate and either items or itemsFrom; verify.review, when given, is outputs.<id>.outFile; dependsOn names existing or proposed actions; lane is analyze|build|chore and effort is low|medium|high; runtime-owned fields are rejected.',
30
30
  ].join('\n');
31
31
 
32
32
  export const AUTONOMOUS_ORCHESTRATOR_PROMPT = [
@@ -748,8 +748,14 @@ async function runDecisionLoop({ runtime, gate, phase, state, retryAttempts }) {
748
748
  runtime.emit('action.reverify_started', { verifyId: action.id, repairId, round });
749
749
  state.currentStep = { id: action.id, type: action.type, phase: executionPhase(action) };
750
750
  runtime.persist();
751
+ // The re-verify judges the repair, not the whole artifact afresh: it is
752
+ // told which concerns were raised and what the repair reports, and may
753
+ // reject only for an unresolved listed concern or a regression. Earned:
754
+ // goal-4 rerun r2vu9i — round 2 rejected on two concerns round 1 never
755
+ // raised although every round-1 concern was fixed (moving goalposts).
756
+ const repairExcerpt = String(state.outputs?.[repairId]?.outputExcerpt ?? '').slice(0, 1500);
751
757
  result = await runtime.runStep(
752
- { ...action, parentId: gate.id, _dynamic: true },
758
+ { ...action, parentId: gate.id, _dynamic: true, _reverify: { round, maxRounds, repairId, concerns, repairExcerpt } },
753
759
  { phase: executionPhase(action), retryAttempts },
754
760
  );
755
761
  runtime.emit(result.ok ? 'action.repaired' : 'action.reverify_rejected', {
@@ -1021,9 +1027,15 @@ async function runDecisionLoop({ runtime, gate, phase, state, retryAttempts }) {
1021
1027
  const actionDefaults = { ...(gate.addDir != null ? { addDir: gate.addDir } : {}), ...(gate.actionDefaults ?? {}) };
1022
1028
  proposal = {
1023
1029
  ...proposal,
1030
+ // The planner's lane/effort/requiresCapabilities win over the gate's
1031
+ // defaults (contract rule 10 makes lane and effort the planner's);
1032
+ // addDir stays runtime-owned and is spread last so a proposed null
1033
+ // cannot clobber the target. Earned: every goal-4 action ran as lane
1034
+ // "build" because this merge let the defaults overwrite the proposal.
1024
1035
  actions: proposal.actions.map((action) => ({
1025
- ...action,
1026
1036
  ...actionDefaults,
1037
+ ...action,
1038
+ ...(actionDefaults.addDir != null ? { addDir: actionDefaults.addDir } : {}),
1027
1039
  ...(action.type === 'fanout' && actionDefaults.addDir != null
1028
1040
  ? { stepTemplate: { ...action.stepTemplate, addDir: actionDefaults.addDir } }
1029
1041
  : {}),
@@ -916,7 +916,7 @@ export class WorkflowRuntime {
916
916
  '',
917
917
  retry
918
918
  ? `Your previous answer did not match the required schema: ${retry.errors.join('; ')}. Previous output tail: ${retry.tail}. Return the full answer again and END with a JSON object that matches.`
919
- : 'END YOUR OUTPUT with exactly one JSON object matching this schema. No prose or markdown fences may appear after it.',
919
+ : 'END YOUR OUTPUT with exactly one JSON object that is an INSTANCE of this schema — its keys are the names under "properties" (never copy the schema itself or its "type"/"properties" keys). No prose or markdown fences may appear after it.',
920
920
  JSON.stringify(schema),
921
921
  ].join('\n');
922
922
  }
@@ -931,7 +931,11 @@ export class WorkflowRuntime {
931
931
  readTrailingObject(path, schema) {
932
932
  let text;
933
933
  try { text = readFileSync(path, 'utf8'); } catch (err) { return { ok: false, errors: [`output file could not be read: ${err.message}`] }; }
934
- const trimmed = text.trimEnd();
934
+ // A closing markdown fence after the object is the most common way a
935
+ // worker disobeys "no fences": tolerate it rather than spend the single
936
+ // retry (or a planner turn) on an otherwise valid answer. Observed live
937
+ // on goal-4 run ydpjts (2026-08-29): the retry answer ended "}\n```".
938
+ const trimmed = text.replace(/(\s*```[\w-]*\s*)+$/u, '').trimEnd();
935
939
  if (!trimmed.endsWith('}')) return { ok: false, errors: ['output did not end with a JSON object'] };
936
940
  const close = trimmed.length - 1;
937
941
  // Walk "{" positions from the right. Inner braces of the trailing object
@@ -1083,8 +1087,17 @@ export class WorkflowRuntime {
1083
1087
  }
1084
1088
  })();
1085
1089
 
1090
+ const reverify = step._reverify && typeof step._reverify === 'object' ? step._reverify : null;
1086
1091
  const reviewInstructions = [
1087
1092
  step.prompt ?? 'You are a skeptical reviewer. Independently inspect the work and its current repository state.',
1093
+ ...(reverify ? [
1094
+ '',
1095
+ `RE-VERIFY round ${reverify.round} of ${reverify.maxRounds}: your previous verdict rejected this work and repair ${reverify.repairId} has since edited the repository. Judge the repair, not the work afresh.`,
1096
+ ...(Array.isArray(reverify.concerns) && reverify.concerns.length
1097
+ ? ['Concerns you raised (verbatim):', ...reverify.concerns.map((entry) => `- ${entry}`)] : []),
1098
+ ...(reverify.repairExcerpt ? ['The repair reported:', reverify.repairExcerpt] : []),
1099
+ 'Return ok:false ONLY if a listed concern is still unresolved or the repair introduced a regression in the acceptance checks. Anything you notice now that was already true before the repair goes in concerns as informational and never makes ok false: the first verdict was the moment to raise it.',
1100
+ ] : []),
1088
1101
  '',
1089
1102
  'RETURN ONLY a single JSON object of the form',
1090
1103
  '{"ok": <true|false>, "concerns": [<string>...], "summary": <string>}.',
@@ -1364,7 +1377,7 @@ export class WorkflowRuntime {
1364
1377
  `Return ONLY JSON with schemaVersion "${DECISION_SCHEMA_VERSION}", decision, reason, and actions.`,
1365
1378
  'Allowed decisions: proceed, complete, needs_more_work, retry, escalate, wait_for_approval, stop.',
1366
1379
  'Every proposed action MUST use the field "type" (never "kind").',
1367
- 'Every action MUST include a forward-only kebab-case "phase". Never reuse a name listed in closedPhases.',
1380
+ 'Every action MUST include a forward-only kebab-case "phase" shared by the actions of its stage. Never reuse a name listed in closedPhases.',
1368
1381
  '',
1369
1382
  ...(rendered.prompt.includes(AUTONOMOUS_ORCHESTRATOR_PROMPT) ? [] : [AUTONOMOUS_ORCHESTRATOR_PROMPT]),
1370
1383
  '',