bullswarm 0.13.0 → 0.13.2
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 +27 -0
- package/docs/claude-dynamic-workflow-mechanics.md +25 -3
- package/docs/experiments/2026-08-29-ultracode-vs-bullswarm.md +132 -18
- package/package.json +1 -1
- package/src/workflow/goal.js +12 -2
- package/src/workflow/runner.js +14 -1
- package/src/workflow/runtime.js +11 -3
- package/src/workflow/template.js +16 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# bullswarm changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.2 — user text is never a template
|
|
4
|
+
|
|
5
|
+
- `workflow goal` failed before anything ran when the goal text quoted
|
|
6
|
+
something shaped like a template ref (`{{outputs.x.data.field}}` in a goal
|
|
7
|
+
*about* templates): the goal was spliced into the scout prompt and the
|
|
8
|
+
workflow validator rejected the ref as unresolvable — "autonomous workflow
|
|
9
|
+
invalid (nothing ran)". The goal is now a declared input (`inputs.goal`)
|
|
10
|
+
inserted at render time, so nothing in user text is ever parsed.
|
|
11
|
+
- A grammar-valid ref with nothing behind it no longer kills the action at
|
|
12
|
+
render time. It is left literally in the prompt and reported as
|
|
13
|
+
`template.unresolved_ref { actionId, ref }`; planner-authored prompts may
|
|
14
|
+
quote refs as text, and a worker can usually still act on the literal.
|
|
15
|
+
`renderTemplate(str, scope, { strict: true })` keeps the old hard failure.
|
|
16
|
+
|
|
17
|
+
## 0.13.1 — a repaired verify counts as verification of its repair
|
|
18
|
+
|
|
19
|
+
- `completionEvidenceGaps` accepted a verify as evidence for the latest worker
|
|
20
|
+
only when the verify depended on that worker. The executor's repair loop
|
|
21
|
+
produces the reverse edge — `<verify>-repair-N` depends on `<verify>`, then
|
|
22
|
+
the same verify re-runs — so after a clean repair round every `complete`
|
|
23
|
+
was rejected with "missing a successful verification of latest worker
|
|
24
|
+
<verify>-repair-1", and 0.13.0's `all-actions-ok` auto-completion would
|
|
25
|
+
have been blocked the same way. Observed on goal-2 run `wf-mtdcghw0`
|
|
26
|
+
(2026-08-28): three extra planner turns and one redundant verify (~11 min)
|
|
27
|
+
to prove what the re-verify had already shown. A verify that ended ok:true
|
|
28
|
+
after its own repair action now verifies that repair.
|
|
29
|
+
|
|
3
30
|
## 0.13.0 — programs can complete themselves
|
|
4
31
|
|
|
5
32
|
- A planner may attach `completion: { when: "all-actions-ok", reason }` to a
|
|
@@ -216,7 +216,15 @@ from `docs/experiments/2026-08-29-ultracode-vs-bullswarm.md`, never projected.
|
|
|
216
216
|
policy and race on the barrel file." The policy it cites was a caution line
|
|
217
217
|
in the planner prompt; a concurrency cap of 8 was available and unused.
|
|
218
218
|
Discovery alone then ran 458 s. (Final numbers: experiment report.)
|
|
219
|
-
-
|
|
219
|
+
- Final comparison on the 6-module fixture (experiment report for the full
|
|
220
|
+
tables). Goal 2 (known items): Claude 58 min, 24 agents, 0 orchestrator
|
|
221
|
+
turns during execution, parallelism 3.1 · bullswarm 0.11.1 41 min, 3 planner
|
|
222
|
+
turns (27 %), max 6 concurrent, parallelism 2.74 · bullswarm 0.12.1 48 min of
|
|
223
|
+
execution after a 3 h quota wait it survived, 4 planner turns (two caused by
|
|
224
|
+
the bug fixed in 0.13.1), max 7 concurrent, one live repair round. Goal 3
|
|
225
|
+
(discovered items) on 0.13.1: 28 min 42 s, **one planner turn**, the runtime
|
|
226
|
+
recorded `complete` itself, exactly the three unguarded modules fixed,
|
|
227
|
+
75/75 tests.
|
|
220
228
|
|
|
221
229
|
## 3. bullswarm today, mechanic by mechanic
|
|
222
230
|
|
|
@@ -316,7 +324,7 @@ author and the `Workflow` runtime.
|
|
|
316
324
|
sees `outputs.scout.ok=false` with the reason, and a run where only the
|
|
317
325
|
scout succeeded is `blocked`, never "delivered".
|
|
318
326
|
|
|
319
|
-
8. **Program-level completion** (0.13.0
|
|
327
|
+
8. **Program-level completion** (0.13.0) —
|
|
320
328
|
`completion: { when: "all-actions-ok", reason }` on a program. Claude's
|
|
321
329
|
script simply returns when its code is done; bullswarm still spent a final
|
|
322
330
|
planner turn (110–250 s measured) to say `complete` after a clean run. Now
|
|
@@ -324,10 +332,24 @@ author and the `Workflow` runtime.
|
|
|
324
332
|
never below the completion policy) and consults the planner only when
|
|
325
333
|
something failed. With 0.12.0's repair-in-program this makes a clean run
|
|
326
334
|
**one planner turn**: compile, execute, done — Claude's "0 orchestrator turns
|
|
327
|
-
during execution" for the passing case.
|
|
335
|
+
during execution" for the passing case. **[OBSERVED]** goal-3 run
|
|
336
|
+
`wf-mtdkvx0k` (0.13.1): the planner attached the predicate on its own, the
|
|
337
|
+
runtime emitted `decision.auto_completed` (`source: program-completion`),
|
|
338
|
+
one planner process for the whole 28 min run.
|
|
328
339
|
9. **Rate limits are waited for** (0.12.1): a burst-gated provider parks the
|
|
329
340
|
dispatch in `waiting_for_quota` until the window resets instead of failing
|
|
330
341
|
the run in 4 s, which is what the first 0.12.0 comparison launch did.
|
|
342
|
+
**[OBSERVED]** goal-2 run `wf-mtdcghw0`: parked at 95 % for 3 h 2 min,
|
|
343
|
+
dispatched 17 s after the provider reset.
|
|
344
|
+
10. **A repair is verified by its verify's re-run** (0.13.1). The executor's
|
|
345
|
+
repair loop creates `<verify>-repair-N` *depending on* the verify, then runs
|
|
346
|
+
the verify again; the completion-evidence check only followed
|
|
347
|
+
`verify.dependsOn` and so never saw a repair as verified. **[OBSERVED]** on
|
|
348
|
+
`wf-mtdcghw0`: a clean `complete` rejected, three more planner turns
|
|
349
|
+
(~11 min) to re-prove a passed re-verify. In Claude's model this bug cannot
|
|
350
|
+
exist — the script's `while (!ok)` loop *is* the evidence — which is the
|
|
351
|
+
general lesson: every piece of control flow bullswarm moves from planner
|
|
352
|
+
into runtime needs its evidence rule moved with it.
|
|
331
353
|
|
|
332
354
|
**Honest limitation.** `itemsFrom` removes the planner *turn*, not the stage
|
|
333
355
|
*barrier*: a verify depending on a data-driven fan-out waits for all items,
|
|
@@ -325,7 +325,7 @@ together with its instructions. (2) The remediation round spent two of its
|
|
|
325
325
|
three fixes on "non-blocking" nits from verifiers that had returned
|
|
326
326
|
`ok:true`; 0.12.0's doctrine tells the planner those are informational.
|
|
327
327
|
|
|
328
|
-
### bullswarm 0.12.0 (installed binary) — same goal, fresh copy `g2-bs-v3`
|
|
328
|
+
### bullswarm 0.12.0 → 0.12.1 (installed binary) — same goal, fresh copy `g2-bs-v3`
|
|
329
329
|
|
|
330
330
|
**First launch, 19:09:48 Z, installed 0.12.0 — failed in 4 s.** Both the
|
|
331
331
|
scout and the orchestrator were `failed_terminal` with `no eligible pool`
|
|
@@ -339,32 +339,127 @@ the runtime parks the dispatch in `waiting_for_quota`, re-reads the meter
|
|
|
339
339
|
every 60 s, continues when the window resets, and only fails — naming pool,
|
|
340
340
|
usage and reset time — after reset + 10 min grace.
|
|
341
341
|
|
|
342
|
-
|
|
342
|
+
**Second launch, 19:28:03 Z, installed 0.12.1, run `wf-mtdcghw0-bfefc7` on a
|
|
343
|
+
fresh pristine `g2-bs-v3` — observed the wait working.** The meter read 95 %
|
|
344
|
+
at launch, so the runtime parked the first dispatch (the scout) in stage
|
|
345
|
+
`waiting_for_quota` with `until 22:40:00 Z` (reset 22:30 + 10 min grace),
|
|
346
|
+
event `dispatch.waiting_for_quota` carrying pool, usage and reset time. It
|
|
347
|
+
re-read the meter every 60 s for **3 h 2 min 14 s** (`waitedMs 10933739`) and
|
|
348
|
+
emitted `dispatch.quota_available` at **22:30:16.979 Z** — 17 s after the
|
|
349
|
+
provider reset — then dispatched the scout with no operator action. Wall-clock
|
|
350
|
+
numbers below therefore exclude this wait (`metrics-bullswarm.mjs` reports
|
|
351
|
+
`quotaWaitSec` and `wallExclWaitSec` separately); the wait is a provider
|
|
352
|
+
constraint, not execution time.
|
|
353
|
+
|
|
354
|
+
**Outcome: `completed`, `verified: true`, 23:18:38 Z.** Timeline (all Z):
|
|
355
|
+
|
|
356
|
+
| when | what |
|
|
357
|
+
|---|---|
|
|
358
|
+
| 22:30:17 | scout dispatched (read-only survey), 197 s |
|
|
359
|
+
| 22:33:34 → 22:39:14 | planner turn 1, **340 s** → one `needs_more_work` program of 14 actions |
|
|
360
|
+
| 22:39:14 | **7 workers started in the same second**: `build-{csv,duration,intervals,lru,semver,slugify}` + `write-docs-index` |
|
|
361
|
+
| 22:44:45 → 22:46:36 | each `verify-<module>` started the moment its own builder finished — per-chain pipelining, no stage barrier (`verify-intervals` was running while `build-slugify` still built) |
|
|
362
|
+
| 22:52:29 | all six module verifies `ok:true` (`verify-slugify` 354 s — see landmine note) → `verify-full-delivery` |
|
|
363
|
+
| 22:59:02 | `verify-full-delivery` **ok:false**: `docs/README.md` missing → runtime spawned `verify-full-delivery-repair-1` (`source: repair-policy`), no planner turn |
|
|
364
|
+
| 23:01:19 → 23:07:58 | repair wrote `docs/README.md` (137 s); re-verify passed (399 s) |
|
|
365
|
+
| 23:07:58 → 23:09:42 | planner turn 2 (105 s): `complete` — **rejected by the runtime**: "missing a successful verification of latest worker verify-full-delivery-repair-1" |
|
|
366
|
+
| 23:09:42 → 23:12:25 | planner turn 3 (163 s): diagnosed the rejection as mechanical, added one read-only `verify-final-acceptance` depending on the repair |
|
|
367
|
+
| 23:12:25 → 23:17:54 | `verify-final-acceptance` ok:true (328 s) |
|
|
368
|
+
| 23:17:54 → 23:18:38 | planner turn 4 (44 s): `complete`, accepted |
|
|
369
|
+
|
|
370
|
+
Numbers (`metrics-bullswarm.mjs`, wait excluded):
|
|
371
|
+
|
|
372
|
+
| metric | 0.11.1 (`g2-bs-v2`) | **0.12.1 (`g2-bs-v3`)** | Claude #2 |
|
|
373
|
+
|---|---|---|---|
|
|
374
|
+
| execution wall | 41 min 14 s | **48 min 21 s** (2 901 s; +10 934 s quota wait) | 58 min |
|
|
375
|
+
| planner turns / seconds | 3 / 667 s (27 %) | **4 / 652 s (22 %)** — turns 2–4 (312 s) plus `verify-final-acceptance` (328 s) exist only because of the rejection bug below | 0 during execution |
|
|
376
|
+
| dispatches / max concurrent | 22 / 6 | **22 / 7** | 24 / ~10 |
|
|
377
|
+
| parallelism (busy ÷ wall) | 2.74 | **2.11** | 3.1 |
|
|
378
|
+
| actions by source | planner 14+7+0 | **planner 14 + 1, repair-policy 1** | script |
|
|
379
|
+
| tests after | 130/130 | **120/120** (52 + 68 new, 6 files ≥ 9 tests each) | 168/168 |
|
|
380
|
+
| existing tests / src | byte-identical / comment-only | **byte-identical / comment-only** (audit-fixture.sh: 0 non-comment line diffs in all 7 src files) | same |
|
|
381
|
+
| deliverables | all | **all** (6 docs pages, `docs/README.md` 6-row index) | all |
|
|
382
|
+
| estimated tokens | — | 201 568 (utf8/4 estimate) | — |
|
|
383
|
+
|
|
384
|
+
What the run showed:
|
|
385
|
+
|
|
386
|
+
1. **The brace landmine is closed (controlled A/B).** `g2-bs-v3` is a pristine copy, so `src/slugify.js` still carries the `@param {{maxLength?: number}}` JSDoc that killed 0.11.1's `verify-slugify` with zero attempts. On 0.12.1 the same verify dispatched, reviewed the artifact with the braces intact, and returned `ok:true` (with three informational concerns, none of which spawned a polish action — the doctrine held).
|
|
387
|
+
2. **The planner compiled a Claude-shaped program on the first turn.** Six independent `build → verify` chains + a parallel docs-index builder + one final gate, each verify carrying `repair {maxRounds: 1}`. The runtime then ran it as a pipeline: verifies started per chain, not after a barrier.
|
|
388
|
+
3. **The repair loop worked live, and paid for a planner mistake.** `write-docs-index` was compiled with `dependsOn: []`, so it launched with the builders and found no `docs/` to index; the worker refused to invent summaries and returned a status note. The final verify caught the missing file and the runtime's repair round fixed it — no planner turn, ~9 min. In Claude's model the same mistake is an authoring error in the script; here it is a compile error by the planner. Neither runtime can catch it deterministically; both recover through verification.
|
|
389
|
+
4. **Runtime bug found: a repair action is never "verified".** `completionEvidenceGaps` accepts a verify as evidence for the latest worker only if `verify.dependsOn` includes that worker. A repair action depends on its verify (the reverse edge), and the verify's post-repair re-run *is* its verification, but the check does not know that — so a clean `complete` was rejected and the run spent 3 more turns and ~11 min proving what it already had. The same check gates 0.13.0's `all-actions-ok` auto-completion, which would have been blocked the same way. Fix: 0.13.1 (below).
|
|
390
|
+
|
|
391
|
+
Take the bug and the dependency slip out and this run is ~29 min of execution with two planner turns — the shape the 0.12.0 design targeted.
|
|
343
392
|
|
|
344
393
|
The originally planned 0.10.9 goal-2 run was dropped at the user's request
|
|
345
394
|
(2026-08-29): the installed latest is the only baseline that matters.
|
|
346
395
|
|
|
396
|
+
### bullswarm 0.13.1 (installed binary) — goal 3, discovery-shaped, fresh copy `g3-bs-v3`
|
|
397
|
+
|
|
398
|
+
Goal 3 was written to exercise what goal 2 cannot: an **unknown item list**.
|
|
399
|
+
"Some — not all — of the exported functions accept a wrong-typed argument and
|
|
400
|
+
misbehave. Find out which modules actually have this problem (probe every
|
|
401
|
+
export; keep only the misbehaving modules), then for EACH affected module only:
|
|
402
|
+
add top-of-function argument validation (TypeError naming function, parameter,
|
|
403
|
+
expected type; behaviour for valid input unchanged) and `tests/<module>.guards.test.js`
|
|
404
|
+
(node:test, one test per guard). Do not modify existing tests or touch modules
|
|
405
|
+
that already validate. Finish with `npm test` passing and report exactly which
|
|
406
|
+
modules you changed and which you left alone, with evidence." Same fixture
|
|
407
|
+
family, same single pool (`claude-opus-5`), 0.13.1 installed after the goal-2
|
|
408
|
+
run ended so the binary each run used is unambiguous. Run `wf-mtdkvx0k-c40480`,
|
|
409
|
+
23:23:56 → 23:52:41 Z.
|
|
410
|
+
|
|
411
|
+
| when (Z) | what |
|
|
412
|
+
|---|---|
|
|
413
|
+
| 23:23:59 → 23:28:05 | scout (246 s): probed all six modules; found exactly three misbehaving (csv, slugify, semver) with per-function evidence |
|
|
414
|
+
| 23:28:05 → 23:32:59 | planner turn 1 (294 s): **one 9-action program with `completion: {when: "all-actions-ok"}`** — `fix-{csv,slugify,semver}` + `audit-remaining` (independently re-probe duration/intervals/lru/index) in parallel, each with its own `verify-*` carrying `repair {maxRounds: 2}`, then `verify-suite` |
|
|
415
|
+
| 23:32:59 | 4 workers started in the same second |
|
|
416
|
+
| 23:37:04 → 23:39:31 | each `verify-<m>` started as its own fix finished (pipeline, no barrier) |
|
|
417
|
+
| 23:42:44 → 23:52:41 | `verify-suite` (597 s) ok:true |
|
|
418
|
+
| 23:52:41 | **runtime recorded `complete` itself** — `decision.auto_completed`, `source: program-completion`; no second planner process |
|
|
419
|
+
|
|
420
|
+
| metric | value |
|
|
421
|
+
|---|---|
|
|
422
|
+
| wall | **28 min 42 s** (1 722 s), no quota wait |
|
|
423
|
+
| planner turns / seconds | **1 / 294 s (17 %)** |
|
|
424
|
+
| dispatches / max concurrent / parallelism | 11 / 4 / 1.82 (four items → four chains; width was item-bound, cap 8 unused) |
|
|
425
|
+
| repairs | 0 needed (every verify ok:true first time) |
|
|
426
|
+
| actions by source | planner 9; completion recorded by the runtime |
|
|
427
|
+
| result (audit-fixture.sh + `npm test`) | `src/csv.js`, `src/semver.js`, `src/slugify.js` modified (24/9/9 non-comment lines); duration/intervals/lru/index untouched; 3 new `*.guards.test.js`; existing tests byte-identical; **75/75** (52 + 23) |
|
|
428
|
+
| tokens (estimate) | 51 422 |
|
|
429
|
+
|
|
430
|
+
Two notes. First, the planner did **not** use `fanout.itemsFrom` — it inlined
|
|
431
|
+
the three modules the scout had already named and gave the "not yet confirmed"
|
|
432
|
+
half of the repo to one `audit-remaining` worker. That is the right call (the
|
|
433
|
+
scout had done the discovery), and it is exactly what Claude's author does when
|
|
434
|
+
it discovers the list inline before writing the script; `itemsFrom` stays the
|
|
435
|
+
tool for lists that only exist after a worker runs. Second, `verify-others` and
|
|
436
|
+
`verify-suite` both flagged `lru` throwing `RangeError` rather than `TypeError`
|
|
437
|
+
for a wrong-typed capacity and both correctly treated it as informational (the
|
|
438
|
+
existing test pins `RangeError`): passing-with-nits produced no extra work,
|
|
439
|
+
as the doctrine intends.
|
|
440
|
+
|
|
347
441
|
## Behaviour differences observed
|
|
348
442
|
|
|
349
443
|
Same goal, same fixture, same model (Opus for every worker and for bullswarm's
|
|
350
444
|
planner; the Claude session's author was Opus too). Read left to right: what
|
|
351
445
|
Claude did, what bullswarm 0.11.1 did on the identical run, and what 0.12.x
|
|
352
|
-
now does about it
|
|
353
|
-
|
|
446
|
+
now does about it. Every 0.12.x/0.13.x cell is unit-tested; cells marked
|
|
447
|
+
**observed** were also seen live in the `g2-bs-v3` (0.12.1) and `g3-bs-v3`
|
|
448
|
+
(0.13.1) runs above.
|
|
354
449
|
|
|
355
|
-
| Dimension | Claude Code `Workflow` (ultracode) — observed | bullswarm 0.11.1 — observed | bullswarm 0.12.
|
|
450
|
+
| Dimension | Claude Code `Workflow` (ultracode) — observed | bullswarm 0.11.1 — observed | bullswarm 0.12.1 / 0.13.1 |
|
|
356
451
|
| --- | --- | --- | --- |
|
|
357
|
-
| Who plans, and when | The session author read every file and ran the tests inline (4 min), then wrote **one script** (23 k chars, 5 `agent()` sites). **0 orchestrator turns during the 48 min 51 s of execution.** | The planner compiled the **whole 14-action graph in one decision** (253 s) — but blind: goal text + cwd only, no repo survey, no worker output text in its context. Consulted **3 times** (253 s, 304 s, 110 s) = **27 % of wall**. | Read-only `scout` action before the planner; `outputExcerpt` of every finished action in the planner context; prompt reframed as "compile the goal into a PROGRAM"; planner told it is consulted only at the program boundary. |
|
|
452
|
+
| Who plans, and when | The session author read every file and ran the tests inline (4 min), then wrote **one script** (23 k chars, 5 `agent()` sites). **0 orchestrator turns during the 48 min 51 s of execution.** | The planner compiled the **whole 14-action graph in one decision** (253 s) — but blind: goal text + cwd only, no repo survey, no worker output text in its context. Consulted **3 times** (253 s, 304 s, 110 s) = **27 % of wall**. | Read-only `scout` action before the planner; `outputExcerpt` of every finished action in the planner context; prompt reframed as "compile the goal into a PROGRAM"; planner told it is consulted only at the program boundary. **Observed:** both runs compiled the whole program on turn 1 from the scout's survey; goal 3 ran on **one planner turn** (0.13.0 self-completion). |
|
|
358
453
|
| Item discovery | `pipeline(MODULES, probe, author, verify, fix-loop)` over a known list; when a list is unknown Claude discovers it inline *before* writing the script. | Goal named the six modules → inlined them. Nothing to discover here. | `fanout.itemsFrom: "outputs.<discovery>.outFile"` resolved at run time (+ one bounded read-only extraction retry), so an unknown item count never costs a planner turn. |
|
|
359
454
|
| Parallel width and overlap | **6 concurrent** (= six items, cap 8), mean parallelism 3.1. Per-item pipeline: author-B starts the second probe-B ends; no barriers. | **6 concurrent**, mean parallelism 2.74. Ready-set scheduler: each `verify-<m>` started the second its own `module-<m>` finished; `docs-index` waited for all six by design. | Unchanged for known items. Limitation stays: a verify on a *discovered* fan-out waits for all items (no per-item chain inside a fan-out yet). |
|
|
360
|
-
| Verify → fix | Fix loops **pre-authored in code** (`while (!verdict.ok && rounds < N)`): slugify ×2, intervals ×1, all inside the script; 9 verifies, 3 fixes, 0 planner involvement. | A failed/blocked verify came back to the **planner** (turn 2, 304 s), which authored `slugify-recheck` + `verify-slugify-2`. Round trip ≈ 5 min before the fix even started. | `verify.repair { prompt, maxRounds 1–3 }` — the executor runs `<verify>-repair-<n>` with the concerns verbatim and re-runs the same verify; only still-failing verifies return to the planner. |
|
|
361
|
-
| Passing verifies with nits | Schema-forced `{ok, issues}`; the script fixes only when `!ok`. Nits on passing modules were ignored. | Planner spent **2 of 3 remediation fixes** (`polish-semver`, `polish-lru`) on "non-blocking" notes from verifiers that had returned `ok:true` — an extra ~10 min program round. | Doctrine line: an `ok:true` verify is accepted; its concerns are informational. |
|
|
362
|
-
| Robustness to content | Prompts are JS strings; the runtime substitutes nothing. A parse error in the *script* was caught by the harness and corrected inline in 94 s. | The template renderer parsed **any** `{{…}}` — in a planner prompt *and* in the review artifact it appended. `verify-slugify` died at render time with **0 attempts**, blocked `verify-suite`, cost a planner round, and the fix **rewrote fixture source** (JSDoc) to dodge the bug. | Only a known root + dotted identifiers is a template ref; other double braces are text. `verify` appends the reviewed artifact verbatim, never rendered. |
|
|
363
|
-
| Provider rate limits | Agents retry on API errors; a terminal error resolves the agent to `null`, the script keeps going. The session waits. | Pool burst-gated (5h window 91 %) → the whole run **failed in 4 s** with `no eligible pool`, no reset time named (first 0.12.0 launch, 19:09 Z). | 0.12.1: `waiting_for_quota` stage, meter re-read every 60 s, continue when the window resets; fail only after reset + 10 min grace, naming pool / usage / reset time. |
|
|
455
|
+
| Verify → fix | Fix loops **pre-authored in code** (`while (!verdict.ok && rounds < N)`): slugify ×2, intervals ×1, all inside the script; 9 verifies, 3 fixes, 0 planner involvement. | A failed/blocked verify came back to the **planner** (turn 2, 304 s), which authored `slugify-recheck` + `verify-slugify-2`. Round trip ≈ 5 min before the fix even started. | `verify.repair { prompt, maxRounds 1–3 }` — the executor runs `<verify>-repair-<n>` with the concerns verbatim and re-runs the same verify; only still-failing verifies return to the planner. **Observed** (goal 2): `verify-full-delivery` failed on a missing `docs/README.md`, the repair wrote it and the re-verify passed, ~9 min, no planner turn. Exposed the 0.13.1 bug (repair never counted as verified). |
|
|
456
|
+
| Passing verifies with nits | Schema-forced `{ok, issues}`; the script fixes only when `!ok`. Nits on passing modules were ignored. | Planner spent **2 of 3 remediation fixes** (`polish-semver`, `polish-lru`) on "non-blocking" notes from verifiers that had returned `ok:true` — an extra ~10 min program round. | Doctrine line: an `ok:true` verify is accepted; its concerns are informational. **Observed:** 6 + 4 passing verifies with concerns in the two runs, zero polish actions. |
|
|
457
|
+
| Robustness to content | Prompts are JS strings; the runtime substitutes nothing. A parse error in the *script* was caught by the harness and corrected inline in 94 s. | The template renderer parsed **any** `{{…}}` — in a planner prompt *and* in the review artifact it appended. `verify-slugify` died at render time with **0 attempts**, blocked `verify-suite`, cost a planner round, and the fix **rewrote fixture source** (JSDoc) to dodge the bug. | Only a known root + dotted identifiers is a template ref; other double braces are text. `verify` appends the reviewed artifact verbatim, never rendered. **Observed:** same pristine `slugify.js` with `{{maxLength?: number}}`, `verify-slugify` ran and passed. |
|
|
458
|
+
| Provider rate limits | Agents retry on API errors; a terminal error resolves the agent to `null`, the script keeps going. The session waits. | Pool burst-gated (5h window 91 %) → the whole run **failed in 4 s** with `no eligible pool`, no reset time named (first 0.12.0 launch, 19:09 Z). | 0.12.1: `waiting_for_quota` stage, meter re-read every 60 s, continue when the window resets; fail only after reset + 10 min grace, naming pool / usage / reset time. **Observed:** waited 3 h 2 min at 95 %, resumed 17 s after the reset, no operator action. |
|
|
364
459
|
| Structured worker output | `schema:` forces a `StructuredOutput` tool call; mismatches retry at the tool layer, so the script never parses prose. | Prose "content gate" (`looksLikeWork`); a bare JSON array answer was rejected as an "announcement"; the verify verdict is the only structured channel. | Content gate accepts JSON; `parseJsonArray` prefers the trailing array; one extraction action when discovery output has no array. A general `outputSchema` on run actions is still open. |
|
|
365
460
|
| Failure semantics | In code: `parallel()` never rejects, a throwing stage drops its item to `null`, `.filter(Boolean)`. | Runtime `onError: continue` per step; failed dependencies block dependents; blocked graph → planner. | Same, plus: a failed scout is non-fatal; fan-out `ok` is a boolean so dependents can wait on a whole fan-out. |
|
|
366
|
-
| Outcome quality (audits, read-only) | 168/168 tests (52 + 116 new); existing tests byte-identical; `src` comment-only; every deliverable present. | 130/130 tests (52 + 78 new); existing tests byte-identical; `src` comment-only; every deliverable present. |
|
|
367
|
-
| Time and agents | 58 min end to end; 24 agents. | 41 min end to end; 22 dispatches. Faster because it wrote fewer tests per module (11–15 vs 16–24) and skipped Claude's probe stage — not because it orchestrated better. | — |
|
|
461
|
+
| Outcome quality (audits, read-only) | 168/168 tests (52 + 116 new); existing tests byte-identical; `src` comment-only; every deliverable present. | 130/130 tests (52 + 78 new); existing tests byte-identical; `src` comment-only; every deliverable present. | Goal 2 on 0.12.1: 120/120 (52 + 68); existing tests byte-identical; `src` comment-only; every deliverable present. Goal 3 on 0.13.1: exactly the three unguarded modules changed, 75/75. |
|
|
462
|
+
| Time and agents | 58 min end to end; 24 agents. | 41 min end to end; 22 dispatches. Faster because it wrote fewer tests per module (11–15 vs 16–24) and skipped Claude's probe stage — not because it orchestrated better. | Goal 2 on 0.12.1: 48 min of execution (+3 h quota wait), 22 dispatches, max 7 concurrent — ~11 min of it spent on the 0.13.1 bug and ~9 min on one repair round. Goal 3 on 0.13.1: 28 min 42 s, 11 dispatches, 1 planner turn. |
|
|
368
463
|
|
|
369
464
|
The short version: after 0.11.x the *shape* already matched (one decision =
|
|
370
465
|
whole graph, six in parallel, per-item verify overlap). What still separated
|
|
@@ -376,8 +471,9 @@ the runtime.
|
|
|
376
471
|
|
|
377
472
|
## What to change in bullswarm
|
|
378
473
|
|
|
379
|
-
**Shipped in this cycle** (0.12.0 `c1b71a8`, 0.12.1 `beeed94
|
|
380
|
-
unit
|
|
474
|
+
**Shipped in this cycle** (0.12.0 `c1b71a8`, 0.12.1 `beeed94`, 0.13.0
|
|
475
|
+
`6e5f620`, 0.13.1 `1bf0840` — all released to npm and installed; unit suite
|
|
476
|
+
299/299):
|
|
381
477
|
|
|
382
478
|
1. Orchestrator as **compiler**: prompt reframed; planner consulted only at the
|
|
383
479
|
program boundary; `programFeatures: ['itemsFrom', 'repair']` advertised.
|
|
@@ -393,11 +489,18 @@ unit tests, 295/295):
|
|
|
393
489
|
8. Template refs are grammar-checked; review artifacts are never rendered.
|
|
394
490
|
9. Doctrine: `ok:true` verifies are accepted; concerns are informational.
|
|
395
491
|
10. Burst-gated providers are waited for (`waiting_for_quota`), not failed.
|
|
396
|
-
11. **Program-level completion predicate** (
|
|
397
|
-
the 0.12.1 observation run is in flight): `completion: { when:
|
|
492
|
+
11. **Program-level completion predicate** (0.13.0): `completion: { when:
|
|
398
493
|
"all-actions-ok", reason }` lets a clean program record its own `complete`
|
|
399
|
-
decision — no final planner turn
|
|
400
|
-
|
|
494
|
+
decision — no final planner turn just to say so. Anything failing still
|
|
495
|
+
returns to the planner. **Observed** on goal 3: the planner attached it
|
|
496
|
+
unprompted, the runtime recorded `complete` (`source: program-completion`)
|
|
497
|
+
at 23:52:41 Z, one planner turn for the whole run.
|
|
498
|
+
12. **A repaired verify counts as verification of its repair** (0.13.1): the
|
|
499
|
+
completion-evidence check only followed `verify.dependsOn`; a repair action
|
|
500
|
+
depends on its verify (reverse edge), so after a clean repair round every
|
|
501
|
+
`complete` was rejected as "missing a successful verification of latest
|
|
502
|
+
worker <verify>-repair-1" — observed on goal 2 (three extra planner turns,
|
|
503
|
+
~11 min) and it would have blocked item 11 in the same situation.
|
|
401
504
|
|
|
402
505
|
**Still open, in priority order** (each is a measured gap, not a guess):
|
|
403
506
|
|
|
@@ -418,3 +521,14 @@ unit tests, 295/295):
|
|
|
418
521
|
whatever the planner wrote. A default skeptic framing in the verify wrapper
|
|
419
522
|
is cheap and would have caught nothing extra here — listed for parity, not
|
|
420
523
|
urgency.
|
|
524
|
+
5. **Dependency slips by the planner.** Goal 2's `write-docs-index` was
|
|
525
|
+
compiled with `dependsOn: []` although it reads the six `docs/<m>.md` files
|
|
526
|
+
the builders create; it ran first and found nothing. The verify + repair
|
|
527
|
+
loop recovered it (~9 min). Claude has the same failure class (a mis-ordered
|
|
528
|
+
`pipeline` stage) and the same recovery. A doctrine line — "an action that
|
|
529
|
+
reads another proposed action's deliverable must depend on it" — is free;
|
|
530
|
+
a deterministic check is not possible without declared outputs, which would
|
|
531
|
+
be a small schema addition (`produces: [paths]`).
|
|
532
|
+
6. **Record `completion` on the decision.** The planner artifact carries the
|
|
533
|
+
predicate but `state.decisions[]` does not, so `watch`/metrics cannot show
|
|
534
|
+
that a program declared itself self-completing until it does. One field.
|
package/package.json
CHANGED
package/src/workflow/goal.js
CHANGED
|
@@ -122,7 +122,17 @@ export function buildGoalWorkflow({
|
|
|
122
122
|
requireSuccessfulVerification: true,
|
|
123
123
|
},
|
|
124
124
|
},
|
|
125
|
-
|
|
125
|
+
// The goal is user text. It is declared as an input and inserted into
|
|
126
|
+
// prompts at render time ({{inputs.goal}}), so anything in it that looks
|
|
127
|
+
// like a template ref — a goal about templates quoting
|
|
128
|
+
// `{{outputs.x.data.field}}`, say — is inserted verbatim, never resolved.
|
|
129
|
+
inputs: {
|
|
130
|
+
goal: {
|
|
131
|
+
description: 'The user goal, verbatim.',
|
|
132
|
+
required: true,
|
|
133
|
+
default: goal.trim(),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
126
136
|
settings: {
|
|
127
137
|
concurrency,
|
|
128
138
|
retryAttempts,
|
|
@@ -141,7 +151,7 @@ export function buildGoalWorkflow({
|
|
|
141
151
|
type: 'run',
|
|
142
152
|
lane: 'analyze',
|
|
143
153
|
addDir: targetDir,
|
|
144
|
-
prompt: scoutPrompt(goal
|
|
154
|
+
prompt: scoutPrompt('{{inputs.goal}}', targetDir),
|
|
145
155
|
}] : []), {
|
|
146
156
|
id: 'orchestrator',
|
|
147
157
|
type: 'decide',
|
package/src/workflow/runner.js
CHANGED
|
@@ -475,6 +475,19 @@ function actionOutputOk(action, outputs) {
|
|
|
475
475
|
: action.status === 'succeeded';
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
+
// A verify is evidence for a worker when the worker feeds it (dependsOn), or
|
|
479
|
+
// when the worker is that verify's own repair action: the executor re-runs the
|
|
480
|
+
// verify after every repair round, so a verify that ended ok:true after its
|
|
481
|
+
// `<verify>-repair-N` has verified the repair even though the dependency edge
|
|
482
|
+
// points the other way. Without this a clean run's `complete` was rejected as
|
|
483
|
+
// "missing a successful verification of latest worker <verify>-repair-1"
|
|
484
|
+
// (observed on goal-2 run wf-mtdcghw0, 2026-08-28) and cost three planner turns.
|
|
485
|
+
export function verifiesWorker(verify, worker) {
|
|
486
|
+
if ((verify.dependsOn ?? []).includes(worker.id)) return true;
|
|
487
|
+
return (worker.dependsOn ?? []).includes(verify.id)
|
|
488
|
+
&& new RegExp(`^${verify.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-repair-\\d+$`).test(worker.id);
|
|
489
|
+
}
|
|
490
|
+
|
|
478
491
|
export function completionEvidenceGaps(dynamicActions, policy, outputs = {}) {
|
|
479
492
|
const missing = [];
|
|
480
493
|
const successfulWorkers = dynamicActions.filter(
|
|
@@ -490,7 +503,7 @@ export function completionEvidenceGaps(dynamicActions, policy, outputs = {}) {
|
|
|
490
503
|
&& action.status === 'succeeded'
|
|
491
504
|
&& actionOutputOk(action, outputs)
|
|
492
505
|
&& latestSuccessfulWorker
|
|
493
|
-
&& (action
|
|
506
|
+
&& verifiesWorker(action, latestSuccessfulWorker),
|
|
494
507
|
)) {
|
|
495
508
|
missing.push(latestSuccessfulWorker
|
|
496
509
|
? `a successful verification of latest worker ${latestSuccessfulWorker.id}`
|
package/src/workflow/runtime.js
CHANGED
|
@@ -878,6 +878,13 @@ export class WorkflowRuntime {
|
|
|
878
878
|
}
|
|
879
879
|
}
|
|
880
880
|
|
|
881
|
+
/** renderDeep options that turn an unresolved ref into a visible event. */
|
|
882
|
+
renderOpts(actionId) {
|
|
883
|
+
return {
|
|
884
|
+
onUnresolved: (ref) => this.emit('template.unresolved_ref', { actionId, ref }),
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
881
888
|
async runSingle(step, scope, opts = {}) {
|
|
882
889
|
this.enforceRequiredInputs(step.id);
|
|
883
890
|
const rendered = renderDeep(
|
|
@@ -888,6 +895,7 @@ export class WorkflowRuntime {
|
|
|
888
895
|
taskFile: step.taskFile,
|
|
889
896
|
},
|
|
890
897
|
scope,
|
|
898
|
+
this.renderOpts(step.id),
|
|
891
899
|
);
|
|
892
900
|
const taskText = rendered.prompt
|
|
893
901
|
?? readFileSync(rendered.taskFile, 'utf8');
|
|
@@ -965,7 +973,7 @@ export class WorkflowRuntime {
|
|
|
965
973
|
lane: step.lane ?? 'analyze',
|
|
966
974
|
addDir: step.addDir,
|
|
967
975
|
prompt: reviewInstructions,
|
|
968
|
-
}, scope);
|
|
976
|
+
}, scope, this.renderOpts(step.id));
|
|
969
977
|
// A custom prompt changes the review instructions, never the review
|
|
970
978
|
// input. Always append the resolved artifact so the skeptic receives
|
|
971
979
|
// the thing it is meant to judge.
|
|
@@ -1160,7 +1168,7 @@ export class WorkflowRuntime {
|
|
|
1160
1168
|
const rendered = renderDeep({
|
|
1161
1169
|
prompt: step.prompt ?? 'Judge whether the workflow has enough evidence to finish.',
|
|
1162
1170
|
addDir: step.addDir,
|
|
1163
|
-
}, scope);
|
|
1171
|
+
}, scope, this.renderOpts(step.id));
|
|
1164
1172
|
const taskText = [
|
|
1165
1173
|
rendered.prompt,
|
|
1166
1174
|
'',
|
|
@@ -1328,7 +1336,7 @@ export class WorkflowRuntime {
|
|
|
1328
1336
|
const itemScope = { ...scope, item };
|
|
1329
1337
|
let template;
|
|
1330
1338
|
try {
|
|
1331
|
-
template = renderDeep(step.stepTemplate, itemScope);
|
|
1339
|
+
template = renderDeep(step.stepTemplate, itemScope, this.renderOpts(`${step.id}[${i}]`));
|
|
1332
1340
|
} catch (err) {
|
|
1333
1341
|
const itemAction = this.ensureAction(step, { ...opts, item, itemIndex: i });
|
|
1334
1342
|
itemAction.status = 'failed_terminal';
|
package/src/workflow/template.js
CHANGED
|
@@ -32,25 +32,35 @@ export function isTemplateRef(ref) {
|
|
|
32
32
|
* {{runId}}, {{wfDir}}. Non-string values are JSON-stringified. Double-brace
|
|
33
33
|
* text that is not a ref (see isTemplateRef) is returned untouched.
|
|
34
34
|
*/
|
|
35
|
-
export function renderTemplate(str, scope) {
|
|
35
|
+
export function renderTemplate(str, scope, opts = {}) {
|
|
36
36
|
if (typeof str !== 'string') return str;
|
|
37
37
|
return str.replace(TEMPLATE_TOKEN_RE, (match, ref) => {
|
|
38
38
|
if (!isTemplateRef(ref)) return match;
|
|
39
39
|
const v = getPath(scope, ref.trim());
|
|
40
40
|
if (v === undefined) {
|
|
41
|
-
|
|
41
|
+
// A grammar-valid ref with nothing behind it. Planner-authored prompts
|
|
42
|
+
// legitimately quote refs as text (a goal *about* templates does), and a
|
|
43
|
+
// worker can usually still act on the literal — so this is reported,
|
|
44
|
+
// never fatal. Callers that want the old hard failure pass strict:true.
|
|
45
|
+
// (Observed 2026-08-29: a goal quoting `{{outputs.x.data.field}}` failed
|
|
46
|
+
// the whole run at validation with "nothing ran".)
|
|
47
|
+
if (opts.strict === true) {
|
|
48
|
+
throw new Error(`template ref "{{${ref.trim()}}}" unresolved at render time`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof opts.onUnresolved === 'function') opts.onUnresolved(ref.trim());
|
|
51
|
+
return match;
|
|
42
52
|
}
|
|
43
53
|
return typeof v === 'string' ? v : JSON.stringify(v);
|
|
44
54
|
});
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
/** Deep-render every string in a step-like object. */
|
|
48
|
-
export function renderDeep(obj, scope) {
|
|
49
|
-
if (typeof obj === 'string') return renderTemplate(obj, scope);
|
|
50
|
-
if (Array.isArray(obj)) return obj.map((v) => renderDeep(v, scope));
|
|
58
|
+
export function renderDeep(obj, scope, opts = {}) {
|
|
59
|
+
if (typeof obj === 'string') return renderTemplate(obj, scope, opts);
|
|
60
|
+
if (Array.isArray(obj)) return obj.map((v) => renderDeep(v, scope, opts));
|
|
51
61
|
if (obj && typeof obj === 'object') {
|
|
52
62
|
const out = {};
|
|
53
|
-
for (const [k, v] of Object.entries(obj)) out[k] = renderDeep(v, scope);
|
|
63
|
+
for (const [k, v] of Object.entries(obj)) out[k] = renderDeep(v, scope, opts);
|
|
54
64
|
return out;
|
|
55
65
|
}
|
|
56
66
|
return obj;
|