bullswarm 0.10.8 → 0.11.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/AGENTS.md +1 -1
- package/CHANGELOG.md +59 -0
- package/docs/claude-dynamic-workflow-mechanics.md +231 -0
- package/docs/experiments/2026-08-29-ultracode-vs-bullswarm.md +240 -0
- package/package.json +1 -1
- package/skill/SKILL.md +16 -0
- package/src/cli.js +3 -5
- package/src/help.js +1222 -119
- package/src/integrate.js +2 -8
- package/src/strategy-cli.js +7 -19
- package/src/workflow/cli.js +14 -27
- package/src/workflow/decision.js +40 -5
- package/src/workflow/draft-cli.js +12 -32
- package/src/workflow/goal.js +5 -5
- package/src/workflow/runner.js +172 -61
- package/src/workflow/runs-cli.js +5 -16
- package/src/workflow/runtime.js +40 -4
- package/src/workflow/validate.js +1 -0
- package/src/workflow/watch-cli.js +42 -7
package/AGENTS.md
CHANGED
|
@@ -37,7 +37,7 @@ content. Published as `bullswarm` on npm.
|
|
|
37
37
|
## Development
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
npm test #
|
|
40
|
+
npm test # full suite, no network needed (meters read from cache)
|
|
41
41
|
node bin/bullswarm.js doctor --json # readiness report
|
|
42
42
|
node bin/bullswarm.js workflow list # discover workflows
|
|
43
43
|
node bin/bullswarm.js workflow runs # ongoing workflow instances
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# bullswarm changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.1 — reliable `--watch` handoff
|
|
4
|
+
|
|
5
|
+
- `workflow goal --watch` no longer races the detached child: the watcher now
|
|
6
|
+
waits up to 30 s for the run's `state.json` to appear before attaching, and
|
|
7
|
+
`runWorkflowWatch` accepts `waitForRunMs`. The 0.11.0 tag failed to publish
|
|
8
|
+
because this race made the release-gate test fail on the CI runner; 0.11.1
|
|
9
|
+
carries the full 0.11.0 change set below.
|
|
10
|
+
|
|
11
|
+
## 0.11.0 — plan the whole graph, run it wide
|
|
12
|
+
|
|
13
|
+
Adopts the driving mechanics of Claude Code's dynamic workflow (documented in
|
|
14
|
+
`docs/claude-dynamic-workflow-mechanics.md`) into the autonomous loop.
|
|
15
|
+
|
|
16
|
+
- Ready-set scheduler: every planner action whose dependencies have succeeded
|
|
17
|
+
starts immediately, and a dependent action starts the moment its own inputs
|
|
18
|
+
finish rather than when the whole round finishes. The global
|
|
19
|
+
`settings.concurrency` limiter caps real parallelism. Verify-B now overlaps
|
|
20
|
+
fix-C exactly like a Claude `pipeline()` stage.
|
|
21
|
+
- Planning doctrine: the planner is told to propose the complete dependency
|
|
22
|
+
graph in one decision (per-item fix→verify chains plus one whole-system
|
|
23
|
+
verify), to declare file ownership per action and order same-file edits with
|
|
24
|
+
`dependsOn`, to write self-contained worker prompts, and what a planning round
|
|
25
|
+
trip costs. The goal orchestrator prompt no longer asks for "the smallest
|
|
26
|
+
useful set" of actions. `executionConstraints.concurrency` is exposed.
|
|
27
|
+
- `workflow goal` default `--concurrency` is 8 (was 3; max 16).
|
|
28
|
+
- The planner prompt's shared-working-tree caution now says what is actually
|
|
29
|
+
unsafe (whole-tree mutation, running the full suite while others edit) and
|
|
30
|
+
states that concurrent workers editing disjoint files is the expected mode;
|
|
31
|
+
the 0.10.9 orchestrator had cited the old wording as its reason not to fan
|
|
32
|
+
out ("Implementation is deliberately NOT fanned out … shared-target mutation
|
|
33
|
+
policy").
|
|
34
|
+
- `verify.review` is recovered when a planner puts instructions or a filesystem
|
|
35
|
+
path there: instructions move to `prompt`, the single dependency's artifact is
|
|
36
|
+
inferred, and any `review` that is not `outputs.<actionId>.outFile` is
|
|
37
|
+
rejected at validation (feeding the corrective turn) instead of failing a
|
|
38
|
+
dispatch after a full planning round trip.
|
|
39
|
+
|
|
40
|
+
## 0.10.9 — planner self-correction and honest silence
|
|
41
|
+
|
|
42
|
+
- An invalid or non-JSON orchestrator decision no longer fails the run. The
|
|
43
|
+
runtime feeds the exact validator issues, the rejected proposal, and a
|
|
44
|
+
response excerpt back to the same orchestrator thread for bounded corrective
|
|
45
|
+
turns (`settings.maxPlannerCorrections`, default 2, emits
|
|
46
|
+
`decision.correction_requested`), then benches that pool as orchestrator for
|
|
47
|
+
the rest of the run and escalates to one other eligible pool
|
|
48
|
+
(`decision.orchestrator_escalated`), and only then settles on a qualified
|
|
49
|
+
`completed_with_concerns`/`blocked` outcome. The decide action's ledger status
|
|
50
|
+
now agrees with that outcome (`failed_retryable` while correcting,
|
|
51
|
+
`failed_terminal` on exhaustion) instead of reporting `succeeded`.
|
|
52
|
+
- The planner prompt now shows complete `run`, `fanout` (`items` +
|
|
53
|
+
`stepTemplate.prompt` with `{{item}}`), and `verify` (`review`) skeletons, so
|
|
54
|
+
the first proposal can match what `decision.js` validates.
|
|
55
|
+
- `workflow watch` separates two silences: `quiet` counts durable workflow
|
|
56
|
+
events, and a new `agent output … ago` figure (JSONL `transportQuietForSec`)
|
|
57
|
+
counts raw output from live agents, so a thinking agent and a dead one look
|
|
58
|
+
different on the same heartbeat line.
|
|
59
|
+
- Removed the dead thin-leaf help renderer left over from the help unification
|
|
60
|
+
and stopped hard-coding the test count in AGENTS.md.
|
|
61
|
+
|
|
3
62
|
## 0.10.8 — quieter monitoring and resilient orchestration
|
|
4
63
|
|
|
5
64
|
- Made `workflow watch` aggregate low-level activity into compact interval
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# How Claude Code drives a dynamic workflow — mechanics, and what bullswarm adopts
|
|
2
|
+
|
|
3
|
+
Written 2026-08-29 from inside a Claude Code session that has the `Workflow`
|
|
4
|
+
tool ("ultracode") loaded, by the model that authors those workflows.
|
|
5
|
+
|
|
6
|
+
Every statement is tagged:
|
|
7
|
+
|
|
8
|
+
- **[SPEC]** — quoted or closely paraphrased from the `Workflow` tool contract
|
|
9
|
+
and the `workflow-authoring` reference as loaded into the session on
|
|
10
|
+
2026-08-29. This is the documented behaviour the orchestrating model is
|
|
11
|
+
told to rely on.
|
|
12
|
+
- **[OBSERVED]** — measured in this repository's experiments
|
|
13
|
+
(`docs/experiments/2026-08-29-ultracode-vs-bullswarm.md`).
|
|
14
|
+
- **[INFERRED]** — my reading of how the harness must behave to satisfy the
|
|
15
|
+
spec. Not confirmed by source; treat as a hypothesis.
|
|
16
|
+
|
|
17
|
+
## 0. The one-paragraph shape
|
|
18
|
+
|
|
19
|
+
Claude's orchestrator (the main-loop model) does **not** decide step-by-step at
|
|
20
|
+
runtime. It writes a *program* — a small JavaScript script — that declares the
|
|
21
|
+
phases and calls `agent(prompt, opts)` once per worker, wired together with
|
|
22
|
+
`pipeline()` / `parallel()` / plain loops. The harness executes that program
|
|
23
|
+
deterministically, spawning subagents concurrently up to a cap, and the model
|
|
24
|
+
reads the aggregate return value when the program finishes. Planning is
|
|
25
|
+
front-loaded into one authoring act; parallelism is explicit in the code; every
|
|
26
|
+
worker receives an individually written, self-contained prompt and (usually) a
|
|
27
|
+
JSON schema its answer must satisfy. Re-planning happens either as ordinary
|
|
28
|
+
code (loops, conditionals) inside the script, or between scripts when the model
|
|
29
|
+
reads a result and authors the next one. **[SPEC]**
|
|
30
|
+
|
|
31
|
+
bullswarm's `workflow goal`, by contrast, runs an LLM *at every checkpoint*: a
|
|
32
|
+
`decide` step proposes JSON actions, the runtime validates and executes them,
|
|
33
|
+
then asks the LLM again. **[SPEC — bullswarm source]** The rest of this document
|
|
34
|
+
is about which of Claude's mechanics that loop can adopt without giving up its
|
|
35
|
+
one advantage — the user supplies a goal, never a graph.
|
|
36
|
+
|
|
37
|
+
## 1. Mechanics, one at a time
|
|
38
|
+
|
|
39
|
+
### 1.1 The control plane is code, authored once **[SPEC]**
|
|
40
|
+
|
|
41
|
+
- A script must begin with a pure-literal `export const meta = { name,
|
|
42
|
+
description, phases: [{ title, detail }] }`; the body uses `phase()`,
|
|
43
|
+
`agent()`, `pipeline()`, `parallel()`, `log()`, `args`, `budget`, and
|
|
44
|
+
`workflow()` (one level of nesting).
|
|
45
|
+
- The tool is explicitly for "multi-step orchestration where control flow
|
|
46
|
+
should be deterministic (loops, conditionals, fan-out) rather than
|
|
47
|
+
model-driven."
|
|
48
|
+
- The recommended pattern is **hybrid**: "scout inline first (list the files,
|
|
49
|
+
find the channels, scope the diff) to discover the work-list, then call
|
|
50
|
+
Workflow to pipeline over it. You don't need to know the shape before the
|
|
51
|
+
*task* — only before the *orchestration step*."
|
|
52
|
+
- Multi-phase work is several workflows in sequence: "run several in sequence —
|
|
53
|
+
read each result before deciding the next phase. You stay in the loop; each
|
|
54
|
+
workflow is one well-scoped fan-out."
|
|
55
|
+
|
|
56
|
+
Consequence: between two agents inside one workflow there is **no model
|
|
57
|
+
round-trip**. The next agent starts the instant its inputs exist. **[INFERRED
|
|
58
|
+
from spec; consistent with OBSERVED timings]**
|
|
59
|
+
|
|
60
|
+
### 1.2 Phases are progress groups, not barriers **[SPEC]**
|
|
61
|
+
|
|
62
|
+
- `phase(title)` starts a new phase; "subsequent agent() calls are grouped
|
|
63
|
+
under this title in the progress display". `opts.phase` on `agent()` assigns
|
|
64
|
+
the group explicitly and exists precisely "to avoid races on the global
|
|
65
|
+
phase() state" inside `pipeline()`/`parallel()` stages.
|
|
66
|
+
- Titles are matched exactly against `meta.phases`; an unmatched title "just
|
|
67
|
+
gets its own progress group".
|
|
68
|
+
- Nothing about a phase synchronises work. The **only** barrier is
|
|
69
|
+
`parallel()`.
|
|
70
|
+
|
|
71
|
+
### 1.3 Parallelism primitives **[SPEC]**
|
|
72
|
+
|
|
73
|
+
- `pipeline(items, stage1, stage2, …)`: "run each item through all stages
|
|
74
|
+
independently, NO barrier between stages. Item A can be in stage 3 while item
|
|
75
|
+
B is still in stage 1. This is the DEFAULT for multi-stage work. Wall-clock =
|
|
76
|
+
slowest single-item chain, not sum-of-slowest-per-stage." A stage that throws
|
|
77
|
+
drops that item to `null` and skips its remaining stages.
|
|
78
|
+
- `parallel(thunks)`: "run tasks concurrently. This is a BARRIER … Use ONLY
|
|
79
|
+
when you genuinely need all results together." A barrier is justified only
|
|
80
|
+
when stage N needs cross-item context from all of stage N−1 (dedup/merge,
|
|
81
|
+
early exit on zero count, "compare with the other findings").
|
|
82
|
+
- Anti-barrier guidance is explicit: "'I need to flatten/map/filter first' —
|
|
83
|
+
do it inside a pipeline stage"; "'The stages are conceptually separate' —
|
|
84
|
+
that's what pipeline() models. Separate stages ≠ synchronized stages."
|
|
85
|
+
- Caps: "Concurrent agent() calls are capped at min(16, available CPUs − 2) per
|
|
86
|
+
workflow — excess calls queue and run as slots free up." Lifetime cap 1000
|
|
87
|
+
agents; ≤ 4096 items per `parallel()`/`pipeline()` call.
|
|
88
|
+
|
|
89
|
+
### 1.4 One prompt per agent, structured return **[SPEC]**
|
|
90
|
+
|
|
91
|
+
- `agent(prompt, opts)`; "Subagents are told their final text IS the return
|
|
92
|
+
value (not a human-facing message), so they return raw data."
|
|
93
|
+
- `opts.schema` (JSON Schema): "the subagent is forced to call a
|
|
94
|
+
StructuredOutput tool and agent() returns the validated object — no parsing
|
|
95
|
+
needed … validation happens at the tool-call layer so the model retries on
|
|
96
|
+
mismatch."
|
|
97
|
+
- Other per-agent knobs: `label` (display), `phase` (group), `model`
|
|
98
|
+
(override; "default to omitting it"), `effort` (`low`…`max`), `isolation:
|
|
99
|
+
'worktree'` ("EXPENSIVE … use ONLY when agents mutate files in parallel and
|
|
100
|
+
would otherwise conflict"), `agentType` (custom subagent definition).
|
|
101
|
+
- `agent()` "returns null if the user skips the agent mid-run or the subagent
|
|
102
|
+
dies on a terminal API error after retries (filter with .filter(Boolean))."
|
|
103
|
+
|
|
104
|
+
### 1.5 Failure semantics live in code **[SPEC]**
|
|
105
|
+
|
|
106
|
+
- `parallel()` "never rejects" — a failing thunk becomes `null`.
|
|
107
|
+
- Retry, repair and convergence are ordinary loops: *loop-until-count*,
|
|
108
|
+
*loop-until-budget*, *loop-until-dry* ("keep spawning finders until K
|
|
109
|
+
consecutive rounds return nothing new"). No planner turn is spent deciding to
|
|
110
|
+
retry; the script already says so.
|
|
111
|
+
|
|
112
|
+
### 1.6 Determinism and resume **[SPEC]**
|
|
113
|
+
|
|
114
|
+
- `Date.now()`, `Math.random()`, argless `new Date()` throw in scripts ("they
|
|
115
|
+
would break resume"); timestamps come in via `args`.
|
|
116
|
+
- Every invocation persists its script; the run's `journal.jsonl` "records each
|
|
117
|
+
agent's actual return value". Resume = same script + `resumeFromRunId`: "the
|
|
118
|
+
longest unchanged prefix of agent() calls returns cached results instantly;
|
|
119
|
+
the first edited/new call and everything after it runs live."
|
|
120
|
+
|
|
121
|
+
### 1.7 Budget is a hard ceiling **[SPEC]**
|
|
122
|
+
|
|
123
|
+
- `budget.total / spent() / remaining()`; "The target is a HARD ceiling, not
|
|
124
|
+
advisory: once spent() reaches total, further agent() calls throw."
|
|
125
|
+
- bullswarm deliberately does the opposite (advisory targets, by user
|
|
126
|
+
decision: "a failed run is more costly than a slightly over budget run").
|
|
127
|
+
This difference is kept on purpose.
|
|
128
|
+
|
|
129
|
+
### 1.8 Observability **[SPEC]**
|
|
130
|
+
|
|
131
|
+
- Progress is rendered as a tree grouped by phase, with per-agent labels;
|
|
132
|
+
`log()` "emit[s] a progress message to the user (shown as a narrator line
|
|
133
|
+
above the progress tree)"; `/workflows` shows live progress; the tool result
|
|
134
|
+
carries the `runId` and transcript directory.
|
|
135
|
+
|
|
136
|
+
### 1.9 Quality patterns the orchestrator is told to compose **[SPEC]**
|
|
137
|
+
|
|
138
|
+
Adversarial verify (N skeptics prompted to refute; kill on majority),
|
|
139
|
+
perspective-diverse verify (distinct lenses instead of N identical refuters),
|
|
140
|
+
judge panel (N independent attempts → parallel judges → synthesis), loop-until-
|
|
141
|
+
dry, multi-modal sweep, completeness critic, and "no silent caps: if a workflow
|
|
142
|
+
bounds coverage (top-N, no-retry, sampling), log() what was dropped."
|
|
143
|
+
|
|
144
|
+
### 1.10 Ultracode **[SPEC]**
|
|
145
|
+
|
|
146
|
+
"When a system-reminder confirms ultracode is on, that opt-in is standing:
|
|
147
|
+
author and run a workflow for every substantive task by default … For
|
|
148
|
+
multi-phase work (understand → design → implement → review), that often means
|
|
149
|
+
several workflows in sequence — one per phase — so you stay in the loop between
|
|
150
|
+
them."
|
|
151
|
+
|
|
152
|
+
## 2. What that looks like from the outside **[OBSERVED]**
|
|
153
|
+
|
|
154
|
+
Filled from the experiment report as runs complete. Numbers here are copied
|
|
155
|
+
from `docs/experiments/2026-08-29-ultracode-vs-bullswarm.md`, never projected.
|
|
156
|
+
|
|
157
|
+
- bullswarm 0.10.9 smoke goal ("create hello.txt and verify it"), single pool
|
|
158
|
+
`claude-code` pinned to `claude-opus-5`: wall 553 s; **4 orchestrator turns =
|
|
159
|
+
438 s (79 % of wall)**; 2 worker attempts = 114 s; max concurrency 1; 6
|
|
160
|
+
dispatches; ~29 k tokens (bullswarm's byte/4 estimate). Two of the four turns
|
|
161
|
+
were spent recovering from a `verify` proposal whose `review` field carried
|
|
162
|
+
instructions instead of an artifact path — a shape the 0.10.9 planner
|
|
163
|
+
skeleton itself had suggested.
|
|
164
|
+
- bullswarm 0.10.9 on the 7-module fixture (baseline, `--concurrency 8`): the
|
|
165
|
+
orchestrator's first decision (152 s) proposed one serial chain
|
|
166
|
+
discover → implement → verify and wrote: "Implementation is deliberately NOT
|
|
167
|
+
fanned out: all fixes land in one shared working tree and converge on
|
|
168
|
+
src/index.js, so concurrent workers would violate the shared-target mutation
|
|
169
|
+
policy and race on the barrel file." The policy it cites was a caution line
|
|
170
|
+
in the planner prompt; a concurrency cap of 8 was available and unused.
|
|
171
|
+
Discovery alone then ran 458 s. (Final numbers: experiment report.)
|
|
172
|
+
- (comparison fixture final results: pending)
|
|
173
|
+
|
|
174
|
+
## 3. bullswarm today, mechanic by mechanic
|
|
175
|
+
|
|
176
|
+
| Mechanic | Claude `Workflow` | bullswarm ≤ 0.10.9 | Gap |
|
|
177
|
+
| --- | --- | --- | --- |
|
|
178
|
+
| Control plane | Code, authored once; no model call between agents | LLM `decide` turn at every checkpoint; each turn is a fresh `claude -p --resume` process reading the full durable context | Structural. Reachable target: **one planning turn per replan-worthy event** (initial DAG; then only on failure/completion), not per action |
|
|
179
|
+
| Phases | Labels for grouping; never synchronise | Forward-only kebab-case names per action; also just labels | None |
|
|
180
|
+
| 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 |
|
|
181
|
+
| 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 |
|
|
182
|
+
| 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 |
|
|
183
|
+
| 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 |
|
|
184
|
+
| Determinism / resume | Journal of return values; prefix cache | Durable `state.json` + `events.jsonl` + action ledger; resume skips durable outputs | Equivalent |
|
|
185
|
+
| Budget | Hard ceiling | Advisory targets (user decision) | Intentional difference |
|
|
186
|
+
| Observability | Progress tree, narrator, `/workflows` | `watch` heartbeat (semantic quiet + agent-output quiet since 0.10.9), `tui`, events | Comparable |
|
|
187
|
+
| Isolation | `isolation: 'worktree'` per agent | Shared `addDir`; planner-declared file ownership | Candidate |
|
|
188
|
+
|
|
189
|
+
## 4. Adopted into bullswarm 0.11.0
|
|
190
|
+
|
|
191
|
+
1. **Ready-set scheduler** (`src/workflow/runner.js`, `executeActions`).
|
|
192
|
+
Every action whose `dependsOn` have all succeeded is launched immediately;
|
|
193
|
+
a dependent action starts the moment *its own* dependencies finish, not when
|
|
194
|
+
the whole round finishes. The global dispatch limiter
|
|
195
|
+
(`settings.concurrency`) caps real concurrency. This gives `pipeline`
|
|
196
|
+
semantics to any DAG the planner proposes: verify-B overlaps fix-C.
|
|
197
|
+
Test: `dependency-ready sibling actions run concurrently and dependents
|
|
198
|
+
start as soon as their own inputs finish` (`tests/workflow-adaptive.test.js`).
|
|
199
|
+
2. **Planning doctrine in the planner prompt** (`src/workflow/runtime.js`) and
|
|
200
|
+
goal orchestrator prompt (`src/workflow/goal.js`): propose the complete graph
|
|
201
|
+
in one decision; independent actions run concurrently; per-item fix→verify
|
|
202
|
+
chains plus one final whole-system verify; explicit file ownership per
|
|
203
|
+
action and `dependsOn` for any same-file edits; self-contained worker
|
|
204
|
+
prompts with absolute paths and the exact acceptance command. The prompt
|
|
205
|
+
also states the cost of a planning turn so the model can weigh it. The
|
|
206
|
+
context exposes `executionConstraints.concurrency` and
|
|
207
|
+
`readySiblingsRunConcurrently: true`.
|
|
208
|
+
3. **Default `--concurrency` 8** for `workflow goal` (was 3; max 16).
|
|
209
|
+
4. **`verify.review` contract made survivable**: instructions placed in
|
|
210
|
+
`review` are moved to `prompt` and the single dependency's artifact is
|
|
211
|
+
inferred; a `review` that is not `outputs.<actionId>.outFile` is rejected at
|
|
212
|
+
validation (so the 0.10.9 corrective turn fixes it) instead of failing a
|
|
213
|
+
dispatch after a planning round trip.
|
|
214
|
+
|
|
215
|
+
## 5. Not adopted (yet), and why
|
|
216
|
+
|
|
217
|
+
- **Schema-enforced worker output.** bullswarm's content verification and the
|
|
218
|
+
JSON `verify` verdict cover the failure mode today; adding per-action
|
|
219
|
+
`outputSchema` is the next step if planners keep re-asking workers for
|
|
220
|
+
structure.
|
|
221
|
+
- **Per-action worktree isolation.** File ownership declared by the planner is
|
|
222
|
+
cheaper and matches Claude's own guidance ("EXPENSIVE … use ONLY when agents
|
|
223
|
+
mutate files in parallel and would otherwise conflict").
|
|
224
|
+
- **Hard budgets.** Deliberately advisory; see 1.7.
|
|
225
|
+
- **Retry policy in the graph.** Claude writes repair loops as code. A per-
|
|
226
|
+
action `retryPolicy` proposed by the planner would remove one planner turn per
|
|
227
|
+
transient failure; not yet built.
|
|
228
|
+
- **Identical control plane.** bullswarm's orchestrator remains an LLM per
|
|
229
|
+
checkpoint because the user supplies only a goal. The convergence target is
|
|
230
|
+
therefore "few planning turns, full width between them", measured as
|
|
231
|
+
`plannerSec / wallSec` and `maxConcurrentAttempts` in the experiment report.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Claude ultracode dynamic workflow vs bullswarm `workflow goal` — side-by-side (2026-08-29)
|
|
2
|
+
|
|
3
|
+
Status: IN PROGRESS — results sections are filled in as runs complete. Numbers
|
|
4
|
+
below are observed, not projected; anything estimated is labelled.
|
|
5
|
+
|
|
6
|
+
## Question
|
|
7
|
+
|
|
8
|
+
Same goal, same models, same fixture: how does bullswarm's autonomous
|
|
9
|
+
`workflow goal` loop behave next to Claude Code's native dynamic workflow
|
|
10
|
+
(the `Workflow` tool, "ultracode"), and what has to change in bullswarm to make
|
|
11
|
+
it feel as smooth?
|
|
12
|
+
|
|
13
|
+
## Setup (identical on both sides)
|
|
14
|
+
|
|
15
|
+
- **Fixture**: `swarmbench`, a zero-dependency Node ESM project with six small
|
|
16
|
+
pure-logic modules under `src/`, one `node:test` file per module under
|
|
17
|
+
`tests/`, plus an intentionally incomplete `src/index.js` barrel with its own
|
|
18
|
+
test. Bugs were planted by a separate subagent; the orchestrator of the Claude
|
|
19
|
+
side (this session) was told only file names and baseline counts, never the
|
|
20
|
+
bugs, so both orchestrators had to discover them.
|
|
21
|
+
- **Two byte-identical copies**, each `git init`-ed so the diff can be audited
|
|
22
|
+
afterwards. Test files are SHA-256 fingerprinted before and after each run to
|
|
23
|
+
prove neither side edited a test.
|
|
24
|
+
- **Goal text (verbatim, both sides)**: see "Goal" below.
|
|
25
|
+
- **Models locked to one account**: workers on both sides are `claude-opus-5`.
|
|
26
|
+
Orchestrator: Claude side = this Claude Code session (Fable 5, authoring and
|
|
27
|
+
running the workflow script); bullswarm side = `claude-code` connector pinned
|
|
28
|
+
with `--orchestrator claude-code` and `strategy assign high|medium|low --pool
|
|
29
|
+
claude-code --model claude-opus-5`, `claude-fable-5` excluded.
|
|
30
|
+
- **bullswarm** 0.10.9 (released and installed from npm for this experiment),
|
|
31
|
+
run from the installed binary with an isolated `BULLSWARM_HOME` in which every
|
|
32
|
+
pool except `claude-code` is disabled and the connector meter is `none` so a
|
|
33
|
+
5-hour burst gate cannot bench the only pool mid-run.
|
|
34
|
+
- **Sequential runs** on the same laptop so wall-clock numbers are not skewed
|
|
35
|
+
by contention for the same subscription and CPUs.
|
|
36
|
+
|
|
37
|
+
## Goal
|
|
38
|
+
|
|
39
|
+
Verbatim, passed as the single goal argument to bullswarm and as the task
|
|
40
|
+
statement the Claude-side workflow script decomposes:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
Make `npm test` pass in this repository. Every failing test is caused by a bug in a file under src/ (including an incomplete src/index.js barrel that must re-export every module's public API). Fix the source so all tests pass. You must not modify, delete, or add any file under tests/. Finish by running the full `npm test` and report, per file changed, what was wrong and what you changed.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Fixture baseline
|
|
47
|
+
|
|
48
|
+
`swarmbench`: `src/{csv,duration,index,intervals,lru,semver,slugify}.js`,
|
|
49
|
+
`tests/{csv,duration,index,intervals,lru,semver,slugify}.test.js`, `npm test`
|
|
50
|
+
= `node --test tests/*.test.js`. Baseline on both copies (Node v22.23.2):
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
# tests 52
|
|
54
|
+
# pass 37
|
|
55
|
+
# fail 15 (failures present in all 7 test files)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Test-file SHA-256 before any run (identical in both copies):
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
5106804683c7f6cef0dbc3629c4c14f4ab2e88319d934626d79bb18a080ae707 tests/csv.test.js
|
|
62
|
+
b7c7d77daad8bfe03dd0b755574cf59faaf677df7d2701a717cf8e44138bd108 tests/duration.test.js
|
|
63
|
+
27aa49f0f601d22748f36e161c963ac77bd9861fe84028a69e02a456a9b38302 tests/index.test.js
|
|
64
|
+
80abeecbea763df43b2260a7ee03df211db5b092ea87e418e0be740c59383cbd tests/intervals.test.js
|
|
65
|
+
2aa610981dcb9a6c65a0dd9b7b3362f23c95ef5c6436a43cb3d969fc0cc3dded tests/lru.test.js
|
|
66
|
+
d62bc016abe37f4400499f4a77b94c7b60cc8f473bac6b63f5a461f9a18b6a20 tests/semver.test.js
|
|
67
|
+
d611fd789caed8820eaf03b08aeb2799f564e5f0d320485f886fd9c5cf6ac112 tests/slugify.test.js
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Both bullswarm runs and the Claude workflow use a concurrency cap of 8
|
|
71
|
+
(Claude's own cap on this 10-CPU laptop is min(16, CPUs − 2) = 8; bullswarm
|
|
72
|
+
gets `--concurrency 8`), so parallelism differences come from the driving
|
|
73
|
+
loop, not the cap.
|
|
74
|
+
|
|
75
|
+
## Warm-up: bullswarm 0.10.9 smoke goal (observed)
|
|
76
|
+
|
|
77
|
+
Goal: "Create a file named hello.txt in this directory containing exactly the
|
|
78
|
+
single line 'hello from smoke test', then verify it exists with that exact
|
|
79
|
+
content." Run `srdwjs`, single pool `claude-code` pinned to `claude-opus-5`,
|
|
80
|
+
`--max-agents 5 --max-expansion-rounds 2 --concurrency 2`.
|
|
81
|
+
|
|
82
|
+
| metric | value |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| wall | 553 s (9m13s) |
|
|
85
|
+
| dispatches | 6 (4 orchestrator turns, 1 worker, 1 verifier) |
|
|
86
|
+
| orchestrator time | 438 s = 79 % of wall (turns: 108 s, 102 s, 151 s, 77 s) |
|
|
87
|
+
| worker time | 38 s (create) + 76 s (verify) |
|
|
88
|
+
| max concurrent attempts | 1 |
|
|
89
|
+
| tokens | ≈29.1 k (bullswarm estimate: utf8 bytes / 4) |
|
|
90
|
+
| outcome | `completed`, verified |
|
|
91
|
+
|
|
92
|
+
Two of the four planning turns were spent on `verify` actions that died inside
|
|
93
|
+
the runtime without dispatching: the planner put reviewer instructions (turn 2)
|
|
94
|
+
and then a filesystem path (turn 3) into `verify.review`, which the runtime
|
|
95
|
+
resolves as a dotted scope path to an artifact. The 0.10.9 planner skeleton had
|
|
96
|
+
shown exactly that wrong shape. Fixed in the 0.11.0 work (see below).
|
|
97
|
+
|
|
98
|
+
## Results
|
|
99
|
+
|
|
100
|
+
### bullswarm 0.10.9 `workflow goal` (baseline, installed binary)
|
|
101
|
+
|
|
102
|
+
Run `wf-mtd6lxfn-3912fd`, started 2026-08-28T16:44:18Z, `--orchestrator
|
|
103
|
+
claude-code --concurrency 8 --max-agents 30 --max-expansion-rounds 8`.
|
|
104
|
+
|
|
105
|
+
First planning turn: 152 s. It proposed one strictly serial chain —
|
|
106
|
+
`discover-failures` → `implement-fixes` → `verify-suite` — and explained why
|
|
107
|
+
(verbatim from `state.json` `decisions[0].reason`):
|
|
108
|
+
|
|
109
|
+
> Implementation is deliberately NOT fanned out: all fixes land in one shared
|
|
110
|
+
> working tree and converge on src/index.js, so concurrent workers would violate
|
|
111
|
+
> the shared-target mutation policy and race on the barrel file.
|
|
112
|
+
|
|
113
|
+
The "shared-target mutation policy" it cites is a caution line in the 0.10.9
|
|
114
|
+
planner prompt. `discover-failures` (read-only diagnosis of 7 test files) then
|
|
115
|
+
ran alone for 458 s before the single `implement-fixes` worker started.
|
|
116
|
+
|
|
117
|
+
Final numbers (run `xnujua`, 16:44:18Z → 17:19:26Z):
|
|
118
|
+
|
|
119
|
+
| metric | value |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| outcome | `completed`, verified; **52/52 tests pass**; `tests/` byte-identical (SHA-256 unchanged); 7 `src/` files changed, +20/−14 lines |
|
|
122
|
+
| wall | **2108 s (35m08s)** |
|
|
123
|
+
| dispatches | 7 = 3 orchestrator turns + 4 workers |
|
|
124
|
+
| orchestrator time | 431 s (20 %) — turns of 152 s, 189 s, 90 s |
|
|
125
|
+
| worker time | 1677 s: discover 458 s → implement 356 s → (verify-suite: runtime failure, 0 s) → independent-audit 618 s → final-verification 245 s |
|
|
126
|
+
| max concurrent attempts | **1** (parallelism 1.00 — every second of the run had exactly one process working) |
|
|
127
|
+
| planning rounds | 2 expansion rounds + 1 completion |
|
|
128
|
+
| tokens | ≈35.8 k by bullswarm's utf8-bytes/4 estimate of outputs only — **not comparable** to real provider usage (the Claude session's own status line showed ~49 k input tokens after its first four tool calls) |
|
|
129
|
+
|
|
130
|
+
Timeline of attempts (UTC):
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
16:44:18 → 16:46:50 orchestrator turn 1 (152 s) → serial chain of 3
|
|
134
|
+
16:46:50 → 16:54:28 discover-failures (458 s) read-only diagnosis
|
|
135
|
+
16:54:28 → 17:00:24 implement-fixes (356 s) one worker fixes all 7 files; 52/52
|
|
136
|
+
verify-suite failed inside runtime (review = instructions), never dispatched
|
|
137
|
+
17:00:24 → 17:03:33 orchestrator turn 2 (189 s) → independent-audit + final-verification
|
|
138
|
+
17:03:33 → 17:13:51 independent-audit (618 s) re-tests, fuzzes (3 000 interval sets, 2 000 LRU trials, …)
|
|
139
|
+
17:13:51 → 17:17:57 final-verification (245 s) verify verdict ok
|
|
140
|
+
17:17:57 → 17:19:26 orchestrator turn 3 (90 s) complete
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
What the orchestrator did well: it delivered a correct result with an honest,
|
|
144
|
+
detailed completion report, refused to fan out for a stated reason, and
|
|
145
|
+
recovered from its own `verify.review` mistake ("it was my control-plane
|
|
146
|
+
error") in one extra turn. What cost time: a strictly serial plan under a
|
|
147
|
+
concurrency cap of 8 that was never used; a 458 s read-only discovery step
|
|
148
|
+
before any fix; a 618 s audit that fuzzed every module; and one wasted
|
|
149
|
+
planning round on the `verify.review` shape.
|
|
150
|
+
|
|
151
|
+
### Claude Code, fresh interactive session (Opus, ultracode)
|
|
152
|
+
|
|
153
|
+
Set-up: a brand-new Claude Code 2.1.250 session started by `herdr` in its own
|
|
154
|
+
tab (`claude-compare`), `--model claude-opus-5 --dangerously-skip-permissions`
|
|
155
|
+
(the same permission posture bullswarm's connector uses), `/effort ultracode`
|
|
156
|
+
confirmed on screen, cwd = the twin fixture copy, no prior context. The goal
|
|
157
|
+
text above was submitted as the first and only prompt at 17:19:16 Z. The
|
|
158
|
+
bullswarm baseline's final single-process planning turn was still running
|
|
159
|
+
until 17:19:26 Z — a 10-second overlap, noted for honesty.
|
|
160
|
+
|
|
161
|
+
Result (session `309931b0…`, goal turn 17:19:16 Z → 17:22:45 Z):
|
|
162
|
+
|
|
163
|
+
| metric | value |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| outcome | **52/52 tests pass**; `tests/` byte-identical (SHA-256 unchanged); 7 `src/` files changed, +21/−14 lines; per-file bug explanation printed |
|
|
166
|
+
| wall | **209 s (3m29s)** — the session's own footer: "Baked for 3m 29s" |
|
|
167
|
+
| agents spawned | **0** — no `Workflow`, no `Agent` call; 11 `Bash` calls in total |
|
|
168
|
+
| planning round trips | 0 |
|
|
169
|
+
| how it decided | 17:19:19–17:20:34 read every `src/` and `tests/` file and ran `npm test` inline; 17:20:34 "I have a complete picture: 15 failures across 7 modules. Let me get a review before I start editing." → advisor: apply directly, "the ultracode carve-out for trivial mechanical edits applies now that diagnosis is done"; 17:22:09 one Python patch script over all 7 files; 17:22:14–17:22:29 `npm test`, `git diff -- tests/` (empty), `git diff -- src/` |
|
|
170
|
+
| tokens (real provider usage from the transcript) | output 41 042; cache-read 1 939 219; cache-write 119 052; uncached input 62 — 24 assistant messages |
|
|
171
|
+
|
|
172
|
+
**The finding that matters for this experiment:** a fresh Opus session with
|
|
173
|
+
ultracode on judged this goal *too small to orchestrate* and did it solo. The
|
|
174
|
+
"smoothness" here is a fast inline scout (75 s to a complete diagnosis) and
|
|
175
|
+
zero planning round trips — not parallel agents. bullswarm's orchestrator, by
|
|
176
|
+
contrast, cannot do anything inline: every observation costs a worker
|
|
177
|
+
dispatch and every decision a full planning turn, so the same 15 bugs took
|
|
178
|
+
7 dispatches and 35 minutes.
|
|
179
|
+
|
|
180
|
+
Goal #1 therefore cannot show the fan-out mechanic the user asked about. Goal
|
|
181
|
+
#2 below is sized to require it.
|
|
182
|
+
|
|
183
|
+
### Correctness audit — goal #1
|
|
184
|
+
|
|
185
|
+
Independent `npm test` after each run: bullswarm copy 52/52, Claude copy
|
|
186
|
+
52/52. All seven `tests/*.test.js` SHA-256 values identical to the pre-run
|
|
187
|
+
fingerprints on both copies. Diff stat: bullswarm +20/−14 across the same 7
|
|
188
|
+
`src/` files; Claude +21/−14. Both fixed the same bugs the same way.
|
|
189
|
+
|
|
190
|
+
## Goal #2 — sized to require fan-out
|
|
191
|
+
|
|
192
|
+
Base: the fixed fixture (52/52) committed clean, copied three times
|
|
193
|
+
(`g2-claude`, `g2-bs-v2`, `g2-bs-base`). Goal text (verbatim, both sides):
|
|
194
|
+
|
|
195
|
+
```text
|
|
196
|
+
This repository (swarmbench) has six modules under src/ — csv, duration, intervals, lru, semver, slugify — and a passing test suite. For EACH of the six modules deliver three things: (1) a new test file tests/<module>.edge.test.js using node:test and node:assert/strict with at least 6 additional edge-case tests that are not already covered by tests/<module>.test.js, all passing against the current implementation; (2) complete JSDoc on every exported function or class in src/<module>.js (parameter types, return type, thrown errors, one @example) without changing any behaviour; (3) a documentation page docs/<module>.md describing the public API, edge-case behaviour, and examples. Then write docs/README.md as an index table linking all six pages with a one-line summary each. Do not modify the existing tests/<module>.test.js files. Finish with the full `npm test` passing (existing plus new tests) and report what you created.
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Six independent items, three deliverables each, one cross-cutting index — the
|
|
200
|
+
shape `pipeline()` exists for.
|
|
201
|
+
|
|
202
|
+
### Claude Code, fresh interactive session #2 (Opus, ultracode)
|
|
203
|
+
|
|
204
|
+
New session (`0754e69f…`) in herdr tab `claude-compare-2`, same set-up as
|
|
205
|
+
session #1; goal submitted 17:24:4x Z. Observed driving sequence:
|
|
206
|
+
|
|
207
|
+
- 17:24:50–17:25:20 inline scouting (ls, `cat -n` every source and test file,
|
|
208
|
+
`npm test` → 52 passing). First words: "I'll start by orienting myself in the
|
|
209
|
+
repo before authoring the workflow."
|
|
210
|
+
- 17:25:20–17:26:44 advisor consultation ("Advisor sharpened the plan"); then
|
|
211
|
+
`mkdir docs` and a baseline snapshot of `src/` for behaviour-preservation
|
|
212
|
+
checks.
|
|
213
|
+
- 17:28:42 `Workflow` call: script `swarmbench-docs-and-edge-tests`, phases
|
|
214
|
+
Probe → Author → Verify, five `agent()` call sites in a per-module
|
|
215
|
+
`pipeline()` ("probe → author → adversarially verify → fix-loop, pipelined
|
|
216
|
+
per module"), ≈23 k characters, workers inherit the session model (Opus).
|
|
217
|
+
- The first call was **rejected by the harness**: "Invalid workflow script:
|
|
218
|
+
Script parse error: Unexpected token (48:15) … Workflow scripts must be plain
|
|
219
|
+
JavaScript". The session replied "Backtick inside a template literal in the
|
|
220
|
+
lru hints. Fixing and resending." — the same validate-and-correct loop
|
|
221
|
+
bullswarm 0.10.9 now has for planner decisions, except here the correction is
|
|
222
|
+
an inline retry inside one turn rather than a fresh planning process.
|
|
223
|
+
|
|
224
|
+
(execution numbers pending)
|
|
225
|
+
|
|
226
|
+
### bullswarm 0.11.0 code (`node bin/bullswarm.js` from the repo checkout)
|
|
227
|
+
|
|
228
|
+
(pending)
|
|
229
|
+
|
|
230
|
+
### bullswarm 0.10.9 (installed binary), if time allows
|
|
231
|
+
|
|
232
|
+
(pending)
|
|
233
|
+
|
|
234
|
+
## Behaviour differences observed
|
|
235
|
+
|
|
236
|
+
(pending)
|
|
237
|
+
|
|
238
|
+
## What to change in bullswarm
|
|
239
|
+
|
|
240
|
+
(pending)
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -332,9 +332,25 @@ optional work. The loop is durable:
|
|
|
332
332
|
execute -> observe -> decide -> validate proposal -> append -> execute -> observe
|
|
333
333
|
```
|
|
334
334
|
|
|
335
|
+
Execution of an accepted proposal is a ready-set scheduler: every action whose
|
|
336
|
+
`dependsOn` have succeeded starts at once (up to `settings.concurrency`, default
|
|
337
|
+
8 for `workflow goal`), and a dependent action starts the moment its own inputs
|
|
338
|
+
finish. The planner is instructed to propose the complete graph per decision —
|
|
339
|
+
per-item fix→verify chains plus one whole-system verify — because each planning
|
|
340
|
+
turn is a full orchestrator round trip. See
|
|
341
|
+
`docs/claude-dynamic-workflow-mechanics.md` for the model this follows.
|
|
342
|
+
|
|
335
343
|
Allowed planner decisions are `proceed`, `complete`, `needs_more_work`,
|
|
336
344
|
`retry`, `escalate`, `wait_for_approval`, and `stop`. Expansion decisions must
|
|
337
345
|
contain bounded actions; malformed or over-budget output executes nothing.
|
|
346
|
+
A malformed or non-JSON decision does not fail the run: the runtime returns
|
|
347
|
+
the exact validator issues to the same orchestrator thread for up to
|
|
348
|
+
`settings.maxPlannerCorrections` (default 2) corrective turns
|
|
349
|
+
(`decision.correction_requested` events), then benches that pool as
|
|
350
|
+
orchestrator for the rest of the run and tries one other eligible pool
|
|
351
|
+
(`decision.orchestrator_escalated`), and only then settles on a qualified
|
|
352
|
+
`completed_with_concerns`/`blocked` outcome. Safety bounds (`maxActions`,
|
|
353
|
+
`maxItemsPerExpansion`) still terminate immediately with a qualified outcome.
|
|
338
354
|
`complete` still requires successful delivery and verification. `stop` returns
|
|
339
355
|
`completed_with_concerns` with a ready best-effort artifact when useful work
|
|
340
356
|
exists, or `blocked` when it does not; neither hides failed verification.
|
package/src/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ import { release } from './lib/release.js';
|
|
|
17
17
|
import { cmdWorkflow } from './workflow/cli.js';
|
|
18
18
|
import { cmdStrategy, maybeRefreshStrategy } from './strategy-cli.js';
|
|
19
19
|
import { cmdIntegrate, installIntegration } from './integrate.js';
|
|
20
|
-
import { helpForArgs } from './help.js';
|
|
20
|
+
import { helpForArgs, usageLine } from './help.js';
|
|
21
21
|
import { resolveDispatchModel } from './lib/strategy.js';
|
|
22
22
|
|
|
23
23
|
export function getBullswarmDir() {
|
|
@@ -474,9 +474,7 @@ export async function main(argv) {
|
|
|
474
474
|
case 'release':
|
|
475
475
|
return cmdRelease(opts);
|
|
476
476
|
default:
|
|
477
|
-
console.error(
|
|
478
|
-
`unknown verb "${verb}". try: setup | integrate | run | health | pools | strategy | doctor | workflow | runs | version | release`,
|
|
479
|
-
);
|
|
477
|
+
console.error(`unknown verb "${verb}". Run "bullswarm --help" for the list of commands.`);
|
|
480
478
|
return 2;
|
|
481
479
|
}
|
|
482
480
|
}
|
|
@@ -486,7 +484,7 @@ export async function main(argv) {
|
|
|
486
484
|
function cmdRelease(opts) {
|
|
487
485
|
const kind = opts.rest[0];
|
|
488
486
|
if (!['patch', 'minor', 'major'].includes(kind)) {
|
|
489
|
-
console.error(
|
|
487
|
+
console.error(`usage: ${usageLine(['release'])}`);
|
|
490
488
|
return 2;
|
|
491
489
|
}
|
|
492
490
|
try {
|