Route bounded coding work across whichever installed agent CLI has quota to spare.
Purpose: iteration brief for making bullswarm’s workflow system behave like Claude Code’s dynamic workflows while preserving bullswarm’s provider routing, quota pacing, content verification, and agent-friendly CLI contracts.
Audience: the next implementation agent.
Status: historical implementation brief from 2026-08-21. The gaps and task
list below describe the state at that date; they are not a current capability
matrix. For current behavior use README.md, skill/SKILL.md,
docs/claude-dynamic-workflow-mechanics.md, and the contextual CLI help.
Claude’s dynamic workflow is fundamentally a durable control loop:
understand request
-> plan
-> execute actions
-> observe results
-> plan again
-> execute newly needed actions
-> repeat until complete
Bullswarm now has the control loop this brief proposed: a durable orchestrator
observes completed work, proposes a bounded program, deterministic validation
accepts or rejects it, and the runtime schedules ready actions before the next
checkpoint. Static JSON workflows remain supported alongside zero-graph
workflow goal execution. The rest of this document preserves the historical
evidence and build rationale that led to that implementation.
The target is not an uncontrolled mutable DAG and not an LLM that owns the runtime. The target is a hybrid:
deterministic runtime owns execution and safety
planner agent supplies semantic decisions only when needed
worker agents perform bounded actions
verifier checks evidence
dashboard and agents read the same durable state
Fleetlens session inspected:
http://localhost:3321/sessions/8f75ae10-9d92-4a24-8bca-202bf45d20af#workflows
The page contained the session’s serialized conversation, workflow output, event history, checkpoints, and action records. This is an observation of the runtime behavior, not a claim that every internal implementation detail is public or complete.
A representative run emitted this event sequence:
run.queued
run.started
kernel.checkpointed: intake
kernel.checkpointed: interpreted
kernel.checkpointed: gated
kernel.checkpointed: planning
kernel.checkpointed: executing
kernel.checkpointed: observing
kernel.checkpointed: planning
kernel.checkpointed: executing
kernel.checkpointed: observing
kernel.checkpointed: planning
kernel.checkpointed: executing
artifact.published
kernel.checkpointed: observing
kernel.checkpointed: planning
kernel.checkpointed: delivered
kernel.delivered
run.succeeded
The key fact is that planning appears multiple times in one run. The system
does not assume that the initial plan is enough. It executes, observes, then
re-enters planning when the result changes what should happen next.
Observed checkpoint concepts included:
{
"stage": "planning",
"intent": {},
"plan": {},
"state": {
"task": {},
"action": {},
"workspace": {},
"artifact": {},
"authorization": {},
"conversation": {}
},
"budget": {},
"gate": {}
}
The exact fields vary by run, but the separation matters:
intent describes what the user wants.plan describes proposed actions.state describes facts discovered so far.budget limits work.gate records whether the run may proceed.stage tells observers where the runtime is.Observed actions had stable IDs, operations, dependencies, inputs, status, and outputs. A simplified form is:
{
"id": "workspace-list-1",
"operation": "workspace_list",
"dependsOn": ["workspace-write-1"],
"inputs": { "path": "outputs" },
"status": "done",
"output": { "entries": ["sample.txt"] },
"error": null
}
The action ledger is separate from the plan. The plan says what should happen; the ledger says what actually happened.
The planner observes action outputs and can decide that the original plan is incomplete. For example:
list files
-> observe that auth files were missed
-> plan auth-file review actions
-> execute those actions
The decision is semantic. It is not based only on process exit code.
Possible decisions include:
proceed
complete
retry
escalate
needs_more_work
wait_for_approval
stop
Claude separates a logical run from attempts:
task
-> attempt 1: failed
-> attempt 2: succeeded
Observed statuses included concepts such as:
retry_scheduled
failed_retryable
failed_terminal
timed_out
waiting_for_approval
This is more informative than replacing one flat step result with another.
Observed event records had this shape:
{
"sequence": 32,
"type": "artifact.published",
"schemaVersion": 1,
"payload": {},
"committedAt": "2026-08-20T08:23:19.764Z"
}
Events use a monotonic sequence and can be resumed with a cursor, conceptually:
GET /events?after=31
This lets a UI or agent receive only events it has not seen yet.
Observed cancellation was a state transition:
running -> cancelling -> cancelled
The telemetry recorded cancellation request time, cancellation latency, and terminal status. The system also used worker/lease recovery so an interrupted worker could later be marked abandoned rather than silently remaining active.
Bullswarm workflows are declarative JSON documents validated before execution. The current structure is:
workflow
-> ordered phases
-> ordered steps
Supported step types:
run: one delegate invocation.fanout: one delegate invocation per item.verify: a skeptical review of a prior output artifact.decide: a durable adaptive planning gate whose proposal is validated before
any new action is appended or executed.The implementation is mainly in:
src/workflow/validate.js
src/workflow/runner.js
src/workflow/runtime.js
src/workflow/template.js
src/workflow/dashboard.js
src/workflow/cli.js
load JSON
-> validate
-> resolve inputs
-> phase 1
-> phase 2
-> phase N
-> report
The runtime persists state.json after steps and writes a final report.json.
Each dispatch produces task and output artifacts in the run directory.
Results are stored in:
state.outputs
A later step can reference them with templates:
For a structured run, declare an object outputSchema when a later step
needs typed data. The successful state record contains data and
schemaOk: true; after the single schema correction retry fails it retains the
output text and contains schemaOk: false and schemaErrors. A schema retry is
observable as action.output_schema_retry, followed by
action.output_validated only when the corrected object passes validation.
The task also supplies an exact local schema-preflight command. The worker uses
it on a temporary candidate before replying, while the runtime independently
revalidates the captured response before exposing data downstream.
A fan-out can use a prior output file as its item source:
{
"type": "fanout",
"itemsFrom": "outputs.discover.outFile",
"stepTemplate": {
"prompt": "Review ."
}
}
The runtime first consumes an already-recorded array such as
outputs.discover.data.items. For the legacy outputs.<id>.outFile form it
reads the referenced file and parses a JSON array. This is dynamic item
expansion, not dynamic workflow graph expansion. Fan-out schemas belong on
stepTemplate.outputSchema; each item stores its own data, schemaOk, and
possible schemaErrors, and resume re-runs only items whose verdict or schema
is incomplete.
Automatic routing currently filters pools by:
It then ranks candidates by:
time-adjusted quota surplus = elapsed percentage - used percentage
The highest-surplus capable pool normally wins, subject to incumbency and cost
guards in src/lib/route.js.
This does not infer model intelligence. --auto is not an intelligence or
quota selector. For OpenCode, --auto approves tool permissions in headless
mode. The connector controls the model. The QA connector now pins:
opencode run --auto --model relay/gpt-5.6-luna {taskFile}
Bullswarm currently has:
retryAttempts from 0 to 3maxAgents spend guardstate.jsonbullswarm doctor --json
bullswarm workflow capabilities --json
bullswarm workflow inspect <file-or-name>
bullswarm workflow runs result <shortId> --json
bullswarm workflow tui --json
bullswarm workflow tui --json <shortId>
bullswarm workflow tui --json --cancel <shortId>
The intended commands are:
bullswarm doctor --json
bullswarm workflow capabilities --json
bullswarm workflow inspect <file-or-name>
bullswarm workflow tui --json
This section is an as-built checklist from the original handoff. The adaptive decision loop, bounded graph expansion, structured decisions, attempt ledger, ordered event log, cancellation, and timeline/dashboard surfaces described below are implemented now. The past-tense gap text is retained so reviewers can trace requirements to the resulting runtime and tests; it must not be read as current product status.
This is the central gap. Bullswarm executes a predeclared plan but does not re-enter planning after observing results.
Required behavior:
execute action
-> persist result
-> observe state
-> decide next action
-> schedule accepted action
Bullswarm can create more calls for more fan-out items, but it cannot accept a structured decision such as:
{
"decision": "needs_more_work",
"actions": [
{
"type": "run",
"prompt": "Investigate the newly discovered auth issue."
}
]
}
The runtime must be able to append validated actions to a running plan while keeping hard budget limits.
verify can return ok: false, but that only marks a step failed. It does not
say what additional work should be scheduled.
The system needs a structured decision contract that can distinguish:
result is sufficient
result needs more work
result should be retried
result should be escalated
result needs human approval
result is terminally failed
Retries exist, but the run state should represent one logical action with many attempts. Each attempt must preserve its pool, model, status, timings, failure reason, task file, and output file.
The current callback stream and state snapshots are not enough for robust event replay, crash diagnosis, or a resume cursor. Add a sequence-numbered event log.
The dashboard now exposes current phase, current step, and active agents, but it does not yet provide a full action tree, dependency view, decision history, retry history, or per-agent event stream.
Pools declare lanes, but lanes are not model intelligence. Add explicit, connector-owned capability metadata so a workflow can require facts such as:
strong-analysis
code-editing
browser-use
artifact-publishing
workflow-planning
Quota surplus should rank eligible capable pools; it should not pretend to measure intelligence.
If a workflow requires a strong planner or verifier, the requirement must be
declared explicitly. The router can then choose among pools that satisfy the
requirement. Do not make --auto responsible for this.
Bullswarm supports cooperative cancellation, but not yet:
running -> cancelling -> cancelled lifecycle detailsUse three layers:
initiator
-> supplies user intent and initial plan
deterministic runtime
-> validates, schedules, routes, limits, persists, retries, and stops work
planner agent
-> judges semantic sufficiency and proposes additional actions
The planner proposes. The runtime decides whether the proposal is legal.
The planner must not directly:
Keep existing workflows working unchanged:
static mode:
validated JSON graph -> deterministic execution
adaptive mode:
validated initial graph
-> deterministic execution
-> planner gate
-> validated action additions
-> deterministic execution
Do not force an LLM planner onto every workflow. Predetermined pipelines should remain zero-extra-LLM orchestration.
Extend state toward:
{
"runId": "wf-...",
"status": "running",
"stage": "executing",
"currentPhase": {},
"currentStep": {},
"intent": {},
"budget": {
"dispatchesUsed": 8,
"dispatchLimit": 100,
"expansionRound": 1,
"expansionLimit": 3
},
"plan": {
"version": 2,
"actions": []
},
"activeAgents": {},
"attempts": [],
"actionLedger": [],
"decisions": [],
"events": [],
"outputs": {}
}
{
"id": "review-auth-files",
"parentId": "discover",
"kind": "fanout",
"status": "queued",
"dependsOn": ["discover"],
"items": [
"src/auth/login.js",
"src/auth/session.js"
],
"itemsTotal": 2,
"itemsCompleted": 0,
"itemsFailed": 0,
"attempts": []
}
Action status should be explicit and extensible:
queued
running
succeeded
failed_retryable
retry_scheduled
failed_terminal
waiting_for_approval
cancelled
abandoned
{
"actionId": "review-auth-files[0]",
"attemptNumber": 1,
"pool": "opencode2",
"model": "relay/gpt-5.6-luna",
"status": "running",
"startedAt": "...",
"lastHeartbeatAt": "...",
"taskFile": "...",
"outFile": "...",
"why": null
}
Add an adaptive decision step or equivalent runtime gate. The planner must return JSON only:
{
"decision": "needs_more_work",
"reason": "The first scan did not cover authentication files.",
"actions": [
{
"id": "review-auth-files",
"type": "fanout",
"items": [
"src/auth/login.js",
"src/auth/session.js"
],
"stepTemplate": {
"lane": "analyze",
"prompt": "Review for authentication defects."
}
}
]
}
Allowed decisions should be versioned and validated:
proceed
complete
needs_more_work
retry
escalate
wait_for_approval
stop
The runtime must reject malformed actions, unknown operation types, missing IDs, invalid dependencies, excessive item counts, and proposals over budget.
run, fanout, and verify valid.events.jsonl.type, schemaVersion, payload, and committedAt.Suggested events:
run.queued
run.started
phase.started
phase.completed
plan.created
plan.updated
action.queued
action.started
action.observed
action.completed
action.failed
decision.created
attempt.started
attempt.completed
artifact.published
run.cancelling
run.cancelled
run.completed
Extend connector metadata with facts, for example:
{
"name": "opencode2",
"model": "relay/gpt-5.6-luna",
"capabilities": [
"strong-analysis",
"code-reading",
"file-editing",
"workflow-planning"
]
}
The router should filter by required capability and then rank by quota surplus. If no pool satisfies the requirement, return a clear reason; never silently choose a weaker pool.
decide step type or an adaptive workflow setting.Implement the loop explicitly:
load initial plan
-> execute ready actions
-> persist outputs and events
-> invoke planner at a decision gate
-> validate planner proposal
-> append accepted actions
-> execute newly ready actions
-> repeat
The loop must terminate on:
completestopSupport and validate limits such as:
{
"settings": {
"maxAgents": 100,
"maxExpansionRounds": 3,
"maxActions": 100,
"maxItemsPerExpansion": 50,
"retryAttempts": 2,
"maxWorkflowSeconds": 3600
}
}
Enforce limits in deterministic code before accepting planner actions.
cancelling before cancelled.The TUI and JSON output must read the same state and event log.
Human view should expose:
left: phases and action tree
center: selected action/item
right: pool, model, attempt, status, timing
bottom: event stream, decisions, artifacts, failure reason
Agent commands should expose equivalent information:
bullswarm workflow tui --json <runId>
bullswarm workflow events --json <runId> --after 20
bullswarm workflow action show --json <runId> <actionId>
bullswarm workflow capabilities --json
bullswarm workflow inspect <workflow>
Add checked-in examples for:
Every example must state whether it is static or adaptive.
Do not spend an LLM call on deterministic facts.
Use a hybrid policy:
initiator supplies initial intent and plan
runtime handles normal execution
planner is called only at explicit decision gates
The planner may be the initiating agent or a dedicated worker, but the runtime must not depend on the initiating process remaining alive. For long-running or resumable workflows, a dedicated planner role is cleaner.
Planner selection should be capability-aware and quota-aware:
required capability: workflow-planning + strong-analysis
-> eligible pools
-> exclude unavailable/quarantined/burst-gated pools
-> rank by quota surplus
Do not use quota surplus as a proxy for intelligence.
The adaptive implementation is ready only when all of these are true.
needs_more_work with new actions.maxAgents cannot be exceeded.maxExpansionRounds cannot be exceeded.maxActions cannot be exceeded.maxItemsPerExpansion cannot be exceeded.Run at least these end-to-end cases with a real configured provider and with the deterministic test connector:
proceed.needs_more_work and adds a fan-out.Do not add these as part of the first adaptive implementation:
The implementation should preserve this division:
facts -> deterministic code
meaning -> planner model
execution -> deterministic runtime
work -> worker agents
trust -> verifier
visibility -> durable state and events
The most important code path to build is:
execute
-> observe
-> decide
-> validate
-> schedule
-> execute again
Once that loop exists, dynamic graph expansion is no longer a special trick. It is simply the runtime accepting a bounded, validated set of new actions after a planner observes that the previous result was insufficient.