Marshal orchestrate
Drive a fleet of headless agents through Marshal's MCP server: decompose a goal into independent tasks (implementation, research, review, audit, summarise), run them in parallel in isolated git worktrees, collect each run's product (diff or text), and integrate the good diffs. Use when you have multi-part work to delegate to worker agents (Cursor, OpenCode, Codex, Antigravity, Claude Code) instead of doing it all yourself. The engine is mechanism; this playbook is the judgment - decomposition, prompt-writing, and merge decisions live here.From its SKILL.md
npx -y skills add chiruu12/marshal --skill marshal-orchestrateAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
16.0 KB, as published. Nobody here has run it
Driving the Marshal fleet
You are the driver. You keep the expensive thinking - planning, review, merge decisions - and Marshal spawns cheaper or specialized worker agents that each execute one task autonomously in its own git worktree. Your job is to decide who works, on what, with how much context, and whether the result was worth keeping. Marshal is a fleet primitive, not a diff factory: a run's product may be a DIFF or TEXT. Marshal is exposed as MCP tools; the loop is plan → spawn → monitor → collect → (integrate when there is a diff).
Targeting a workspace (when the server has more than one repo)
One Marshal server can be wired to several repos at once. Call list_workspaces first to see them
(name, path, configured, client_count). Every action tool takes an optional workspace -
pass a name to target that repo; omit it to use the default (primary) workspace. Workspaces are
fully isolated: each has its own clients (list_clients(workspace=...)), its own worktrees, its own
run ledger.
- Each run record you get back carries a
workspacefield. When you latercollect_run,integrate, orcancel_run, pass that sameworkspaceso the call routes to the right repo (it still resolves correctly if you omit it - the id is looked up across workspaces). status()with noworkspacelists runs across all workspaces (each tagged); pass a name to scope to one.run_manyaccepts optional per-jobworkspace— one call can fan out across registered repos under a sharedmax_concurrency. Call-levelworkspaceis the default for jobs that omit it. Ledgers stay per-workspace (no merged usage across repos).- Need a repo that isn't registered yet? Registration is an operator decision: the
add_workspace(name, path, scaffold?)tool is disabled by default and only works when the server was started withMARSHAL_ALLOW_MCP_WORKSPACE_REGISTRATION=1. If your call is refused, do NOT retry or look for another way in - report the exact command to the user instead:marshal workspace add <name> <path>. It hot-reloads into the running server (no reconnect), so you can continue as soon as they've run it. When the server does permit the tool, passscaffold=trueto drop a starterfleet.config.yamlif the repo has none; then checklist_clients(workspace=name)and have the user fill in clients before routing real work.
If list_workspaces shows only default, ignore all of this: it behaves exactly like the
single-repo server.
0. Know your clients
Run doctor before the first batch (read-only). It now verifies auth, not just that a CLI is
on PATH: a logged-out backend that still answers --version is reported CLI present but not authenticated (with the login command), instead of a green "available" that then dies one second
into a real run. Treat a backend's login as standing setup you confirm up front — cheap to check,
expensive to skip across a whole fan-out.
Call list_clients to see the configured workers (name, backend, model, permission,
permission_fidelity). Each client is a routing choice the user set up (a cheap bulk worker, a
careful reviewer, etc.). You route tasks to clients by name - you never choose backends
directly. Use permission_fidelity when routing sensitive work (it is resolved from that
client's (backend, permission) — a yolo client is never enforced-denies):
enforced-denies— this client's tier has a backend/Marshal restriction beyond the worktree (still not a sandbox; prefer these for secrets-adjacent or destructive-risk tasks).boundary-only— treat as worktree isolation only; do not assume a deny guarantee (Command Code, Goose, Antigravity, Claude Code). Doctor warns on these backends' safe-edit.unrestricted—permission: yolo; deny/sandbox overlay dropped by design. Do not route sensitive work here. To decide which client a task should go to (by task weight - heavy/standard/light - and cost), seedocs/model-playbook.md.
1. Plan - decompose into INDEPENDENT tasks
Split the goal into tasks that can run in parallel without colliding. Tasks may be write work (implementation) or read-and-reason work (research a question across sources, review a diff, audit a codebase, summarise) - Marshal runs the agents either way; the product is a diff or text.
- For write tasks: give each a disjoint set of files where possible. Two tasks editing the same file will conflict at integrate time - separate their scope or run them in different rounds.
- Size each task so one worker can finish it autonomously. Workers are headless: they cannot ask you anything mid-run. The prompt must contain everything needed to finish.
- Write a self-contained prompt per task: the goal, acceptance criteria, and the minimal files and context the worker needs. The worker sees only what you give it plus its worktree - not your whole session. Scope tightly; that is the point (less drift, less token waste).
- When you know the intended file scope, pass
context_fileson the task so the worker sees only what it needs.
Never fan out a dependency chain in one run_many. Marshal shines on independent work. If task
B needs A's output, batching them in parallel makes each branch off the same base, blind to the
others - they re-invent the same scaffolding and collide at integrate. For sequential work, do one of:
- Rounds (simplest): integrate A into your branch (when A produced a diff), then plan B against
the new state. For text-only A, read
collect_run/textand put the findings in B's prompt. - Pass a report forward with artifacts (multi-round review/fix loops): tell A in its goal to
write its findings to
.marshal-artifacts/<name>.mdin its worktree. Marshal harvests that when the run ends, so it survives the worktree, andget_run(A)lists it underartifacts. Then spawn B withartifacts_from: [A_run_id]and it lands read-only at.marshal-context/artifacts/<A_run_id>/. Say in B's goal that the file is there - the copy is not announced to the agent by itself. Prefer this over pasting a long report into B's prompt: it keeps prompts short, and the next round reads what A actually wrote instead of your summary of it. - Chain off A's branch (no integrate yet):
commit_run(A)freezes A's work as a commit on its own branch (your branch stays untouched), thenspawn/run_agentB withbase_branch= A's branch so B builds on A's actual output. Withoutcommit_run, basing B on A's branch sees only the spawn base - the agent leaves its work uncommitted, so the branch ref never moved. - When dependence is unavoidable, ship the contract in the prompt: the exact signatures/imports of the foundation plus "this already exists - import it, do not redefine it."
2. Spawn
- One task:
run_agent(client, goal, task_id?). - Several independent tasks:
run_many(jobs, max_concurrency?), wherejobsis a list of{client, goal, task_id?, workspace?}. They run in parallel, each in its own worktree, capped atmax_concurrency(default 4 - each agent CLI is heavy; do not uncap a large fan-out). Per-jobworkspacefans out across registered repos in one call; omit it to use the call-level default. - Every run returns a record with a unique
run_id, itsworktree,status,cost_usd, andworkspace.
3. Monitor
status()lists every run with status + cost;get_run(run_id)fetches one.- A
runningrecord means "no outcome recorded yet", which is not the same as "the agent is working". Readagent_aliveto tell them apart:true= still working,false= the process is gone and the outcome is about to be written (re-read shortly),null= unknown, not dead. Do not probe the pid yourself — a pid alone is not an identity, since the OS reuses them. - A run ends in
exited_clean,empty(exited 0 with neither text nor file changes - an outcome, not a fault; nothing to integrate),failed,timed_out,cancelled, orverify_failed(file changes exist but the workspace'sverify:gate rejected them - collect the diff and read the record'sverify_outputbefore deciding; not an integration candidate as-is). Onlyexited_cleanruns with a diff are integration candidates; text-onlyexited_cleanwork lives intext(see collect). When the workspace configures averify:command,exited_cleanalso means that gate passed for runs that had changes.
4. Collect - review before you trust
collect_run(run_id)returns the run's product read-only. Branch onproduced:diff— uncommitted (changed_files,diff) and/or committed (committed_changed_files,committed_diff,commit_count) file changes. Read both sections. An empty uncommitted diff does not mean no work — check the committed section too.text— no file changes; the agent's final message is the artifact (texton the result). This is the expected product of research/review/audit/summarise runs. Do not treat it as failure, and do not integrate.nothing— neither text nor file changes (matches run statusempty).
exited_cleanmeans "the process exited cleanly," not "the work is correct."- Reject work that is wrong or off-scope by not integrating it: the worktree stays isolated and main is untouched. Then record it (step 6) - not integrating is invisible on its own, and an unrecorded rejection is a run that looks like nobody ever reviewed it.
- When your own read isn't enough - a migration, a public API, a security-sensitive path -
run_team(name, target="run", run_id=...)puts the same diff in front of several independent read-only reviewers (each a different model, each holding one lens) and hands you their reports. It computes no verdict: you still collect the objections and decide. Seemarshal-adversarial-review, andlist_teams()for the declared panels.
5. Integrate - merge the good diffs
integrate(run_id, cleanup?) merges the run's branch into the branch you currently have checked out.
Only for runs whose product is a diff. Handle the outcome:
merged- landed;merged_intoandchanged_filessay what/where. Passcleanup=trueto remove the worktree when you're done with it.conflict- the merge was aborted and the repo left clean;conflictslists the files. Resolve by re-planning the task (or integrating the other runs first), then retry.blocked- the target checkout is dirty/colliding or on a detached HEAD; nothing changed. Fix the target (commit/stash your edits, check out a branch) and retry - the work is safe on its branch.empty- no file changes to merge (an outcome, not a fault; common for text-only runs).- A
failedrun has two possible meanings: the agent/backend failed, or the run was orphaned at startup because the process supervising it died before an outcome was recorded. Readerrorto tell them apart - an orphan says so, and its work may still be sitting in the worktree. error- a git operation failed in a way that needs a human (readmessage); do not blindly retry.base_branch_drift(onmerged) - the run was spawned from a different branch than the one you currently have checked out (messagenames both). The merge still landed; this is a warning, not a block. Integrating into a different branch is sometimes deliberate — read the branches and decide whether the result is what you intended.
Integrate one run at a time, reviewing each. Worktree isolation means main is never touched until this step.
6. Record the outcome - what you rejected, not just what you merged
integrate records integrated for you. Nothing records the other half, so call
set_outcome(run_id, "rejected"|"abandoned", note?) on every run you decide not to merge:
rejected- you reviewed it and the work was wrong, off-scope, or worse than a sibling's.abandoned- you gave up on it (superseded, no longer needed, the task was wrong).
Why it matters: declining to integrate leaves no trace, so a run you reviewed and threw away is
indistinguishable from one nobody has looked at. routing computes every rate over judged runs
only - if you record integrations and never rejections, it reports a 100% success rate for every
client and tells you nothing.
Add a note when the reason is not obvious from the diff; it is what your future self reads out of
routing. integrated is sticky (a merge commit is a fact, not an opinion) - trying to overwrite
it returns conflict and changes nothing.
Then routing(task_kind?) pays it back: it reports which client's work you actually kept for this
kind of task, ranked, with the sample size attached to every rate. Read it before step 1 on
your next fan-out instead of guessing from model names. It ranks on integration rate first and only
breaks ties on measured cost, so a backend that reports no cost is never scored as if it were free.
Ranking is per task_kind — pass the filter for the work you are about to fan out. Without it
you get every kind at once, each with its own #1 in recommended_by_task_kind, and recommended
is null: a client that tops your docs history has no measured claim on a refactor.
7. Clean up - reclaim the worktrees
A long session leaves a worktree + branch per run. When you're done, clean(scope?, dry_run?) tears
them down in one call (the usage ledger and run-state history are kept; only the disk-heavy worktrees
and branches go). It never touches a running run. Scopes:
merged- only runs you already integrated. Safest.finished(default) - merged runs plus failed/timed_out/cancelled/empty/verify_failed ones; protects un-integratedexited_cleanruns (a candidate you might still want to review). Averify_failedrun's worktree holds reviewable work - collect/review it before cleaning.all- every finished run, including un-integrated exited_clean work.
Scope-mode cleans also reap orphans automatically - worktree dirs whose run record was pruned or
corrupted (they are invisible to ledger-driven cleanup and would otherwise leak on disk forever);
they show up under orphans_removed.
Run clean(dry_run=true) first to see what would go, or clean(run_ids=[…]) to tear down specific
runs. Don't clean a run whose work you haven't collected/integrated unless you're sure you're done
with it.
Cost
usage() shows per-provider cost (totals and by backend/client/model, with $/run and
$/succeeded). Every figure is tagged by source (native / admin-api / unavailable) -
never treat unavailable as free. To compare routing strategies head-to-head on a real task, use the
marshal-benchmark skill.
Invariants to respect
- When several repos are wired, pick the right
workspaceper call; a run integrates into its own workspace's repo, never another. - Workers are headless - prompts must be self-sufficient (no questions are possible).
- Review what a run produced before integrating;
exited_cleanis notcorrect. Text-only runs need no integrate;emptyis an outcome, not a failure. - Keep tasks independent to avoid merge conflicts; never fan out a dependency chain - sequence it
in rounds, or chain off a committed branch with
commit_run+base_branch. - Confirm backends are authenticated (
doctor) before the first batch, not after a wasted run. - Worktree isolation is the safety net - main is untouched until you integrate.
- Clean up finished runs with
cleanwhen done; it never removes a running run or the usage ledger.