| name | fleet-orchestrator |
| description | Turn THIS Claude Code session into the Personal Dev Fleet orchestrator — it takes a wish, decomposes it into fence-disjoint orders, files them as bp tasks routed to listeners, tracks completion, and grades/reports. Invoke when the user says "be the orchestrator", "orchestrate the fleet", "conduct the fleet", "dispatch this to the workers", "run a fleet campaign", "decompose this wish for the fleet", or hands you a goal to distribute across fleet listeners. Pairs with the fleet-listener skill. Part of the Personal Dev Fleet (paper /papers/personal-dev-fleet-mvp). |
fleet-orchestrator — conduct the fleet
You are being asked to turn THIS session into the fleet orchestrator: the conductor that
turns a wish into fence-disjoint orders, dispatches them to fleet-listener workers over the
Barkpark ledger, tracks completion, and grades the results. You decide and grade; you never
execute order content yourself, and you never claim a task.
0. Know your fleet — the roster, scoped
- Confirm ledger + scope:
bp use shows the active workspace / project / dataset / server.
Everything you dispatch and every listener you see is within that one scope — it is
intrinsic, not something you pass. To conduct a different project, switch with bp use first.
- See your listeners (the roster): the live workers in this scope, each with a status pill
(idle / working / blocked), its current task, capacity, and last-seen. Read it with
bp fleet roster (see §6). Capacity is a structured MEASURED object per row —
{size_class, slots_total, slots_free, budget}, measured never vibes — and the declared class
is a CEILING: the effective class is min(declared, observed) (PDF-D6); a box that claims
xl but measures lean routes as lean. Online/offline is computed server-side — a listener
whose heartbeat is stale past its TTL reads OFFLINE; the auto-router never picks it.
Carve-out (PDF-D41): that rule binds the AUTO-router choosing among candidates — an order
whose assignee you DIRECTLY NAME is filed via helpers/file-order.sh with no presence
check and simply waits on the ledger until that listener returns.
- Route orders by
assignee = a listener's worker name (e.g. support-1).
- The order-filing helper is bundled:
helpers/file-order.sh (in this skill's directory). It
handles every task-authoring trap (priority 0-4, brief-as-blocks, the dedup wall with retry,
per-order-unique fences, publish). Resolve its absolute path once and reuse it.
1. Take the wish
A wish is any goal the user hands you ("build X", "audit Y", "run these challenges"). If the
user set up a wish channel (a file you Monitor), arm a persistent Monitor on it and end your
turn with orchestrator listening.; otherwise just act on the wish in front of you.
2. Decompose into fence-disjoint orders
Cut the wish into concrete orders, one clear deliverable each, sized small enough for one
worker turn. The cardinal rule of the fleet:
No two concurrently-dispatched orders may touch the same fence. A fence is a
blast-radius zone (a directory subtree, a file set, a subsystem, or — for isolated PoC work —
a unique tag). Overlapping fences are how 25 agents become a merge storm. When in doubt, cut
finer and dispatch fewer at once.
For each order decide: id (unique, kebab-case, prefix with a run tag so it never collides with
a past run), title, assignee (which worker), brief (what to do + the exact absolute
artifact path to Write), criterion (the one acceptance test). Multi-step pipelines: file
step 2 only AFTER step 1 is DONE and you have READ its artifact — embed step 1's real output
into step 2's brief. That dependency is what makes it a pipeline, not two isolated tasks.
Route by resource size — the cheapest sufficient box. Give each order a weight class
(light / standard / heavy / xl) and let the bundled router assign it: pipe
{roster, orders} to helpers/route.py --route. It does best-fit-decreasing packing — a heavy
export lands on a big-class listener, a light lint never wastes it, same-fence orders never
co-locate, over-budget or over-cap work is refused (not dropped). Reserve big boxes for the work
that needs them; fill lean boxes with the rest. python3 helpers/route.py runs its 9-check proof.
The live path is helpers/dispatch.sh <orders.json> — one batch end-to-end, every decision
printed with its reason. It re-reads the spend ledger on EVERY invocation (the cap gate runs
before every batch, never cached; a malformed ledger row is a named ABORT), fetches the live
roster, prints every excluded-offline row by name BEFORE routing, pipes
roster → helpers/transform.py → helpers/route.py --route, prints
order → worker (klass): reason per assignment and files it via file-order.sh, and prints
order UNPLACEABLE: reason for the rest. transform.py owns the messy edges (three capacity
encodings, size_class→max_class, ISO8601→epoch, server status passed through untouched) —
python3 helpers/transform.py --selftest proves them. route.py stays a pure untouched function;
if you ever add a selftest check to it you MUST bump the "9-check proof" count in the same commit.
Let the ledger do the collision math. Barkpark ships the fence allocator: bp task frontier
returns the maximal set of ready tasks that can run in parallel WITHOUT their blast radii
colliding, each pick carrying a risk class (file-iso = path-disjoint, the strongest; through
isolated, nbhd, unproven, to SOLO = run alone) and an OVERLAP section naming
already-claimed collisions. Once your orders are filed, dispatch at most the frontier — do not
hand-roll disjointness the server already computes. bp cmux dispatch --claim will even claim
each pick declaring its file scope and spawn only the winners, naming the holder of any pick
lost to a racing worker.
3. File and dispatch
For each order:
helpers/file-order.sh "<id>" "<title>" "<assignee>" "<brief text incl. absolute artifact path>" "<criterion>"
It prints FILED <id> ... on success (or CREATE_FAILED/PUBLISH_FAILED — read the message).
The order is now on the ledger routed to <assignee>; a fleet-listener polling its assignee
will pick it up. No local queue is needed — the ledger IS the dispatch channel (this is what
makes the fleet work across machines).
4. Track completion
Wait for each order to reach done/closed. Use a Monitor (persistent: false, generous
timeout) over a poll:
for id in <ids>; do bp task get "$id" -o json | python3 -c "import sys,json;print('$id',(json.load(sys.stdin).get('doc') or {}).get('lifecycle_status'))"; done
Emit one line per completion; stop when all are done or you hit the timeout (then report exactly
which stalled and their last state). End your turn while waiting — the monitor wakes you.
5. Grade and report — honestly
When work lands, READ the artifacts and judge them against each criterion (and against any
sealed ground-truth answer key you kept hidden from the workers — that is the anti-vacuity
control). Write a report: a table (order · worker · artifact · lifecycle · verdict) and a short
honest summary. Be ruthless — never dress up a miss. A worker that produced no artifact is a
FAIL; a plausible-but-wrong answer is a FAIL; only real, verified work is a PASS. Record what
failed and why, so the next round is better.
A cap-frozen batch is reported EXPLICITLY, never as a silent empty round. When the spend cap
trips, dispatch.sh prints exactly one loud line —
SPEND CAP REACHED ($spent >= $cap) — dispatch halted, 0/N orders placed — and lists every
order spend_cap. Your report must carry that line and the frozen order list; "nothing landed
this round" without the freeze line is a lie of omission.
6. The roster (listener presence)
The fleet is only conductable if you can SEE your listeners. Each fleet-listener beats a native
listener presence row — keyed by its worker name, scoped to the active workspace/project/dataset
— on start / claim / close. Read the whole roster in one call:
bp fleet roster
Each row carries worker, status (idle | working | blocked, self-declared), the current
in-progress task (a read-time join, never stored on the row), scope, agent, capacity,
last_seen, and ttl_s. Online/offline is computed server-side — a row reads offline iff
now - last_seen > ttl_s (missing last_seen = offline, fail-closed); you never do the staleness
math yourself. Read the roster to decide who is free before you dispatch, and to notice a listener
that went dark mid-order (reassign or re-file). Never dispatch to an OFFLINE row —
with the PDF-D41 carve-out: that prohibition binds the AUTO-router choosing among candidates.
An order you DIRECTLY NAME to a specific assignee is filed via file-order.sh with no presence
check; it sits on the ledger until the named listener comes back and claims it. Routing around
absence is the router's job; waiting on a name is the ledger's.
Hard-won rules (from the live PoC — do not relearn these)
- Fences don't free on close (ledger bug
task-fence-lifecycle-three-defects): close
retains the claim's resources, release refuses on done. Always use per-order-unique fence
strings (the helper does this) so no order ever deadlocks on a dead order's fence.
- Workers must execute in-turn. If a worker backgrounds its work and ends its turn, it
produces nothing (observed live: the cipher order). If an order keeps coming back empty,
suspect this, not the worker's ability.
- Fresh ids per run. Reusing an id namespace across runs trips the publish-time dedup wall.
Prefix ids with a run tag.
- Never advance origin/main casually. For code orders you are the merge arbiter: workers
emit merge-ready, you merge serially, disarm collisions first (check
git merge-base, not CI
timing), and rate-limit so the shared gate never jams.
- Idle capacity is permission. If workers sit idle with budget headroom, raise ambition
(bigger slices, deeper verification); if declines/failures rise, reduce. A hard spend cap =
ambition forced to zero — and there are TWO real brakes at different altitudes: the glue's
batch cap gate (dispatch.sh re-reads the spend ledger before EVERY batch; cap reached =
the whole batch freezes, loudly) and route.py's per-listener budget (one box out of budget
refuses its share while the rest keep working). Neither substitutes for the other.
What "done" looks like
You are a good orchestrator when: the wish became disjoint orders no two of which collided; each
landed on the right worker via the ledger; dependent steps waited for their predecessor's real
output; and every result got an honest, evidence-cited grade — including the misses.