/ro:planner-worker (alias /ro:swarm)
Multi-agent coding swarm. Sibling to /ro:ralph. Where Ralph is one agent doing stories in series, planner-worker is N agents doing stories in parallel under a planner and a merger, with optional judge-agent termination.
Part of the local factory
This skill is one part of the local factory — the family of agent-loop skills that run autonomously on Ronan's machine. Siblings: /ro:ralph, /ro:matt-pocock-coding-workflow, /ro:night-shift, /ro:day-shift. They share artefact shape, gitignore rules, and PR conventions. See /ro:ralph § "Run artefacts (the canonical shape)" for the canonical reference.
The companion is the remote factory — the Factory app (tracked separately) that runs equivalent loops as a cloud service. Story formats and PR conventions are kept compatible so a queue from one can be consumed by the other.
For parallel workers specifically: every worker MUST write its scratch to .ralph/sessions/<session-id>/<worker-id>.md (gitignored). The planner harvests learnings into .ralph/patterns.md, writes the per-session detail log to .ralph/sessions/<session-id>.md, and updates the per-phase rolling aggregate at .ralph/<phase>.session.md at end-of-wave. At the same end-of-wave point the planner also harvests .swarm/shared-learnings.md into .ralph/patterns.md and clears .swarm/claims.jsonl (see US-3c2). No worker touches a shared file. No merge conflicts on the progress log.
Close-with-summary (mandatory for every shipped slice): every worker MUST post a structured comment on the GH issue BEFORE merging via gh issue comment <N>. Sections: Shipped, Surprises encountered, Patterns promoted to patterns.md, Local learnings, Follow-ups. See /ro:ralph § "Close-with-summary comment (required for every story)" for the canonical body shape. The merger should reject any PR whose linked issue is missing this comment.
When to use
- A PRD has been grilled via
/ro:grill-meand written via/ro:write-a-prdand now has 5+ vertical-slice issues to ship - You want overnight autonomous execution (
--afk) - The issues are independent enough that parallelisation actually helps (the planner emits
depends_onto serialise hot-file work) - You're on the Max plan and want Agent-tool fan-out, not API-billed Sandcastle
When NOT to use
- One-shot single-story task (use
/ro:ralph --mode single) - Cross-repo / monorepo coordination (out of scope v1)
- Hostile-code or untrusted-input runs (no container isolation; future
--sandcastleflag) - You don't have a PRD yet (run
/ro:grill-methen/ro:write-a-prdfirst)
Quick start
# Interactive: grills you for config, then runs against a named PRD
/ro:planner-worker --prd lekkertaal-pwa-bootstrap
# Autonomous night-shift (AFK)
/ro:planner-worker --prd lekkertaal-pwa-bootstrap --afk
# Real GitHub issues + PRs
/ro:planner-worker --prd lekkertaal-pwa-bootstrap --github
# Resume after a human-resolved escalation
/ro:planner-worker --resume
Filter / scope: prd:draft is NEVER picked up
prd:draft issues are NEVER picked up by this skill. They represent ideas captured in the agent-native repo's "inbox", not ready work. Drafts have freeform bodies, NOT Pocock's 7-section template, and have not been grilled. The planner picking one up would mean dispatching workers against an unfinished spec.
To promote a draft into ready work, the user runs /grill on the issue. The grill-with-docs flow rewrites the body into the Pocock 7-section template, then the user swaps the label from prd:draft to the repo's gate label (ready-for-agent by default, or the project synonym like Sandcastle / swarm configured in docs/agents/triage-labels.md).
When the planner (or any phase of this skill) queries GitHub for backlog issues — typically via --source github:<label> from /agentic-e2e-flow or /ro:night-shift — ALWAYS exclude prd:draft. gh label semantics are tricky: --label <gate> matches issues that have the gate label, but an issue can have BOTH prd:draft AND the gate label (mis-labelling drift). Defence in depth:
- Pass
--label <gate>to scope the initial query. - Post-filter the JSON: drop any issue whose
labels[].namecontainsprd:draft. - Equivalently, use
gh issue list --label <gate> --search "-label:prd:draft" ...to push the exclusion server-side.
Tip: if you're not sure what's queued vs what's still an idea, run /ro:list-draft-prds first to see the drafts inbox before kicking off the swarm.
US-0: Interactive config grilling
Unless --skip-grill (or --afk which implies it) is passed, the skill opens with a project-aware grilling phase. Each missing flag is asked via AskUserQuestion, one at a time, with a recommendation derived from project state.
Order and project-awareness rules:
GitHub issues + PRs for this run?
- Resolve repo mode first via the 4-line snippet from
/ro:repo-mode. Ifmode == work→ recommend "no" (hard rule, do NOT prompt; work-mode repos must stay invisible to the work GH/Jira/ADO project). Ifmode == personal→ recommend "yes" when a gh remote resolves. Ifmode == unset→ run the first-run prompt from/ro:repo-mode§ "First-run prompt", persist, then re-resolve. - Probe:
gh repo view --json visibility,nameWithOwner 2>/dev/null - For
personalmode: recommend "yes" when a remote resolves and visibility isPRIVATE; "no" when no remote configured or repo is public + you don't want noise. - CLI equivalent:
--github
- Resolve repo mode first via the 4-line snippet from
Worker count?
- Probe: cores via
sysctl -n hw.ncpu(macOS) ornproc(linux); free disk on.swarm/worktrees/parent viadf -k . - Recommend
min(5, cores - 2)and warn if free disk <(repo_size_mb * worker_count * 2) - Rationale: default 5 on a fresh usage window. Scale DOWN to 2-3 when sharing the window with a long interactive session (2026-06-11 lesson: 5 workers + a heavy main session exhausted the weekly cap mid-run). Scale UP via
--workers N,.swarm.jsonworkers, or--unsafe-manypast 10. - CLI equivalent:
--workers N
- Probe: cores via
Run mode?
- Recommend "interactive" by default
- Offer "AFK" if the user said anything like "kick off and ping me", "run overnight", "wake me when done"
- CLI equivalent:
--afkfor AFK, nothing for interactive
Merge target?
- Probe:
git rev-parse --abbrev-ref HEAD - Recommend the current branch; offer a staging branch (
swarm-staging) as alt - CLI equivalent:
--staging-branch <name>to override; default = current branch
- Probe:
Worker model?
- Recommend
sonnet; offeropusonly if the PRD is flagged as "research-heavy" (more than 3 issues mention "investigate" or "explore") - CLI equivalent: encoded in
.swarm.jsonmodels.worker
- Recommend
Add a Judge agent?
- Heuristic: count concrete acceptance criteria in the PRD. If the PRD looks "closed" (>= 3 numbered acceptance criteria), recommend "no". If open-ended ("we'll know when we see it"), recommend "yes"
- CLI equivalent:
--judge-agent
Trust local pre-push CI; skip waiting for GitHub Actions?
- Probe:
test -f .husky/pre-pushAND grep that file for any ofpnpm quality,pnpm test,pnpm build,pnpm lint,pnpm typecheck - Recommend "yes" when a pre-push hook with at least build + test + lint is present (the case in any repo scaffolded by
/ro:new-tanstack-app) - Recommend "no" when no pre-push hook, OR when the repo's
.github/workflows/*.ymlruns checks that are NOT in the pre-push hook (e.g. cross-OS matrix, integration tests against a managed service) - CLI equivalent:
--trust-local-ci(yes) or--wait-for-gh-ci(no) - Persisted in
.swarm.jsonastrust-local-ci: true|false - Workers configured with
trust-local-ci=truepush, verify the push hook ran clean, and squash-merge immediately. They do NOT pollgh api .../check-runs. - If the repo has branch-protection rules that require status checks, the planner detects this via
gh api repos/<owner>/<repo>/branches/main/protectionand overrides towait-for-gh-ci=true(printing why)
- Probe:
Each answer is echoed back as the CLI flag the user could have passed next time, e.g. "noted, equivalent to --workers 5". CLI flags supplied at invocation skip their corresponding prompt.
--skip-grill short-circuits to defaults: workers=5, github=<true if repo-mode=personal AND gh remote present, else false>, mode=interactive, merge-target=current-branch, worker-model=sonnet, judge-agent=false, trust-local-ci=true. The github default specifically defers to /ro:repo-mode — work-mode repos always run with github=false so nothing leaks to the work GH/Jira/ADO project. If repo-mode is unset and --skip-grill is in play (e.g. --afk), default github=false (safe failure mode); a separate session will catch the unset state and prompt.
US-1: Planner
The planner is invoked once at the start of a run via the Agent tool, with the Opus model.
Planner prompt template (see scripts/planner-prompt.sh for the canonical version):
You are the PLANNER agent for /ro:planner-worker.
Read the PRD at .ralph/<name>/prd.md.
Explore the codebase enough to size the work.
Emit a backlog of vertical-slice issues to .ralph/<name>/issues/<NNN>-<slug>.md.
Each issue file MUST have frontmatter:
id: 001
title: <one-line>
status: ready
depends_on: [] # array of issue ids that must merge first
estimate: <S|M|L>
dod:
- <pnpm test passes>
- <pnpm typecheck passes>
- <any extra acceptance check>
Body: 5-30 lines describing what to build + why, file pointers, test plan.
Every body MUST end with the verbatim `### Close-the-loop tests` block
(unit + integration + e2e + live smoke) from `/ro:slice-into-issues`.
The dispatcher refuses to send a worker against a slice missing it
(see US-2a "Close-the-loop AC gate"); pre-emit it so we never hit
that gate.
Rules:
- Vertical slices only. No "scaffold the schema" issues without a UI touchpoint.
- Mark depends_on for hot-file conflicts (two issues touching the same component).
- 3-12 issues for v1. If you'd plan more than 12, split the PRD instead.
- Exit cleanly when issues are written. Do not implement anything.
The planner is only re-invoked under US-7 (worker failure re-decomposition).
US-2: Confirm the backlog
After the planner exits, the skill prints a one-screen summary:
Backlog plan ready (.ralph/<name>/issues/):
001 [S] schema: add points column deps: -
002 [M] service: award points on completed task deps: 001
003 [M] dashboard: tile rendering points trend deps: 002
004 [S] hook: usePoints deps: 001
005 [M] integration test: points e2e deps: 003,004
Dispatch order: [001] -> [002, 004] -> [003] -> [005]
Workers will run up to N=3 in parallel.
[y] dispatch [n] re-plan with feedback [q] abort
Skipped under --auto-approve or --afk. Rejecting with feedback re-invokes the planner Agent with the feedback inlined.
US-2a: Close-the-loop AC gate
kind:research issues BYPASS this gate. A research/investigation issue legitimately has no test matrix — its close-the-loop is a doc in docs/research/ + the LLM wiki, per [[canon:research-tasks]]. When a candidate issue carries kind:research, skip the test-AC check below, dispatch the research-worker prompt (see "US-3d: Research-worker mode"), and let the merger assert the research close-the-loop matrix (doc exists + index updated + wiki ingested + bidirectional issue↔doc links) instead of tests.
Before dispatching ANY non-research worker, the planner parses each candidate slice body for the marker ### Close-the-loop tests (the narrower predecessor) OR ### Close-the-loop verification matrix (the full 7-row matrix per [[close-the-loop-verification-matrix]]). Slices missing both are guaranteed leaks: the night-shift swarm only implements what the AC list spells out, so a slice without an explicit e2e AC ships with no Playwright coverage (lesson captured at [[close-the-loop-tests-acs]]; canonical example is dataforce #229 / #231).
Behaviour is controlled by the repo-local .ronan-skills.json flag swarm.missing_test_acs:
refuse(default, safe): print the offending slice ids + bodies, skip dispatching them, fail-loud if zero dispatchable slices remain. Forces the human to either re-run/ro:slice-into-issuesagainst the parent PRD (which now emits the section automatically) or hand-edit the slice body and re-queue.inject: auto-prepend the boilerplate### Close-the-loop testsblock (verbatim from/ro:slice-into-issues) before dispatch AND tag the issue with theacs-auto-injectedlabel (so reviewers know to read the diff for actual test files). Faster but riskier: a worker may implement the slice without writing the tests if the issue body doesn't already reference them in the "What to build" prose.
Parse logic (cheap, no LLM needed) — accepts EITHER the narrower ### Close-the-loop tests marker OR the full ### Close-the-loop verification matrix marker:
issue_body="$(gh issue view "$NUM" --json body --jq .body)"
if ! grep -qE '^### Close-the-loop (tests|verification matrix)' <<< "$issue_body"; then
case "$(jq -r '.swarm.missing_test_acs // "refuse"' .ronan-skills.json)" in
inject) inject_block_and_label_acs_auto_injected ;;
*) mark_slice_skipped_with_reason "missing-close-the-loop-tests" ;;
esac
fi
This gate runs in both --prd <name> mode (planner Agent-emitted slices) AND --queue <path> mode (ranked night-shift input). The night-shift loop should not be expected to backfill ACs into pre-existing issues; if a slice arrives without the section, the gate is the last line of defence.
US-2b: Ranked queue input from /ro:night-shift
When invoked with --queue <path>, the skill skips US-1 (planner runs) and US-2 (confirmation) and reads the ranked slice list straight from the file. Format is a one-slice-per-line plaintext list of <issue-number> (when --github) or <issue-id> (when local). Used by /ro:night-shift to pass its ranker output through and prevent every wave from re-running the planner Agent.
The file is consumed in order — top of file = highest priority. The skill still respects depends_on and the US-3b file-area graph; the ranked order only sets dispatch priority among work that's otherwise dispatchable.
--queue and --prd are mutually exclusive. When --queue is set the planner Agent is NOT re-invoked; that's the night-shift loop's job between waves.
US-3: Worker dispatch
For each unblocked issue (depends_on all merged or empty), up to the worker cap:
gh issue develop <gh-issue-number> --name swarm/<id>— creates the branch with the issue↔branch dev-link (canonronan-skills/canon/labels.md§ Branch flow). NEVER plaingit checkout -bhere; the dev-link is what makes theCloses #Npropagation reliable and lets the night-shift retro walk issue↔branch↔PR without title-matching.git worktree add .swarm/worktrees/<id> swarm/<id>— check the just-created branch out into the isolated worktree.- Append
.swarm/to.gitignoreif not already present (commit only on first run, on achore(swarm): gitignore .swarmcommit) - Spawn a Claude Code Agent (default Sonnet) with the issue body as instructions and working directory pinned to the new worktree
Workers are dispatched as multiple Agent tool calls in a single assistant message so the runtime fans out. They are independent and do not coordinate.
US-3b: File-area conflict detection (hybrid static + scout)
Two parallel workers touching the same files race each other at merge time. The skill mitigates this with a hybrid graph, separate from the depends_on graph.
Canonical-label hints (read first, free): before doing any path-set analysis, read these labels from the canonical label system (~/Dev/ronan-skills/canon/labels.md):
parallel-eligible— the slicer asserted this slice is file-disjoint from its siblings. The planner treats this as "safe to fan out unless the static pass disagrees with a path overlap".repo-lock— the slice takes a repo-wide lock (lockfile churn, schema reset, top-level config rewrite). Mark it serial-only: it runs in its own wave after every parallel-eligible slice drains.hitl-likely— reviewer expected to escalate (ORM, schema-migration, billing, OAuth, secret rotation). The planner counts these against the HITL concurrency cap (default 1 in flight at a time) to keep reviewer attention focused.
These hints are advisory: the static pass below still runs and a path conflict still serialises. The labels exist so the planner doesn't need to re-derive the slicer's intent.
Static pass (every wave, free): read each candidate slice body for:
- A
## Files touchedor## Touchessection listing paths - An
estimate:orpaths:frontmatter array - Inline mentions matching
src/...,app/...,apps/...,packages/...,drizzle/..., etc.
Two slices conflict if their declared path sets intersect by exact file OR share a directory at depth ≥ 2 (e.g. src/routes/settings.tsx and src/routes/settings.test.ts collide via src/routes/). Conflicting slices are never dispatched in the same wave, even if they're both unblocked.
Scout pass (only after a wave hits a cross-worker merge conflict): spawn a one-shot scout sub-agent per remaining candidate slice with the prompt:
You are a SCOUT sub-agent for /ro:planner-worker.
Slice: <issue body inlined>
Task: read the slice and predict the SET of file paths the implementation will touch.
Output a JSON object on stdout, nothing else:
{"paths": ["src/routes/...", "src/lib/..."], "confidence": "high|medium|low"}
Rules:
- Do not edit anything.
- Do not run pnpm install or any side-effects.
- Read the codebase enough to be accurate. ~2-3 min budget.
- If you cannot tell, output paths: [] with confidence: low.
Scout outputs are merged into the file-area graph and persist for subsequent waves of the same run.
--no-scout disables the scout pass entirely (purely static). Use when you'd rather waste worker time on conflicts than spend planner time on probing.
US-3c2: Live claims registry + shared learnings (every wave)
The static + scout passes above predict conflicts BEFORE dispatch. This protocol catches the conflicts prediction cannot: scope drift, where a worker legitimately wanders outside its predicted territory mid-implementation. Proof case: nutmeg #167 and #168 (2026-06-11) both created admin/sources.tsx in the same wave — the predicted territories were disjoint, the actual work wasn't, and the collision only surfaced at merge time. A live claims file would have surfaced it mid-run.
Two append-only files in .swarm/, active in every wave. Critical: they live in the PRIMARY checkout's .swarm/, NOT the worker's worktree. A worker runs inside .swarm/worktrees/<id>/ (or .claude/worktrees/agent-<id>/ under the Agent tool's isolation:"worktree"), and .swarm/ is gitignored — so a relative .swarm/... write from a worktree creates a private file in that worktree, isolated from every wave-mate, silently breaking the "benefit immediately" promise (observed nutmeg 2026-06-15). Workers MUST target the shared path: source skills/planner-worker/scripts/swarm-shared.sh (exposes swarm_learn / swarm_claim / swarm_read_learnings / swarm_read_claims, all pointed at the primary .swarm/), or compute it inline once with SWARM="$(cd "$(git rev-parse --git-common-dir)/.." && pwd)/.swarm" (robust across both worktree layouts; the older ../../ relative hop assumed .swarm/worktrees/<id> and breaks under the Agent-tool layout).
.swarm/claims.jsonl (i.e. $SWARM/claims.jsonl) — the live claims registry. One JSON object per line:
{"worker": "003", "issue": 168, "paths": ["src/routes/admin/sources.tsx", "src/lib/sources/"], "ts": "2026-06-11T21:14:02Z"}
Appends are atomic — swarm_claim '<json>', or flock-guarded when available (flock "$SWARM/.lock" ...), else a single-line printf '%s\n' "$line" >> "$SWARM/claims.jsonl" (atomic under PIPE_BUF on a local FS; macOS has no flock, so single-line append is the default path). Protocol:
- A worker appends its claim BEFORE editing an area — not after.
- A worker reads all existing claims on arrival AND again before any scope expansion (touching a path it didn't originally claim).
- On a clash with another live claim: do NOT edit the contested path. Flag the clash in
.swarm/status.mdand in the final report so the planner arbitrates (serialise, re-scope, or hand the path to one worker).
.swarm/shared-learnings.md (i.e. $SWARM/shared-learnings.md) — append-only one-liner discoveries: build quirks ("run pnpm build first to regenerate routeTree.gen"), flaky tests, migration-journal gotchas. Workers append discoveries with swarm_learn "..." (or printf '%s\n' "- ..." >> "$SWARM/shared-learnings.md") as they hit them, and swarm_read_learnings on arrival and between phases — wave-mates benefit immediately instead of waiting for the next wave's prompts.
Honest framing: this is cooperative, not enforced — LLM workers follow it because the prompt says so. The merge-time rebase + full DoD re-run remains the hard gate. Claims exist to catch scope drift early, not to replace the gate. It pays off most at 4+ workers; at 2-3 the static graph already does well.
Planner duties: at wave end, harvest .swarm/shared-learnings.md into .ralph/patterns.md (alongside the existing worker-scratch harvest) and clear .swarm/claims.jsonl — stale claims from a finished wave must not block the next one.
Relation to the factory: this is the local, cooperative sibling of the remote factory's enforced DO file-lock graph (github.com/RonanCodes/factory). Keep the claim record shape (worker, issue, paths, ts) compatible so a queue can move between local swarm and cloud factory.
US-3c: Canonical lifecycle transitions
The planner is the lifecycle-label authority while a wave is in flight, per the canonical label system (~/Dev/ronan-skills/canon/labels.md):
- Slice pickup query (GH mode):
gh issue list --label kind:slice --label ready-for-agent --state open. The planner picks from this set, applying the conflict graph + label hints above. - On dispatch: swap
ready-for-agent→in-progresson the chosen issue (singlegh issue edit --add-label in-progress --remove-label ready-for-agent). - On reviewer reject: swap
in-progress→ready-for-agent(back into the queue for the next wave). - On HITL escalation: swap
in-progress→needs-humanand post a structured comment naming the human action required. - On PR merge: the worker MUST explicitly
gh issue edit <NUM> --remove-label in-progressBEFORE the squash-merge call. TheCloses #Nautoclose flips the issue from open to closed but does NOT strip labels — without the explicit remove, the closed issue keeps a stalein-progresslabel and the lifecycle-label invariant breaks. Closed state is the absence of any lifecycle label.
Branch flow: workers branch off their assigned issue with gh issue develop <issue-number> --name <slug> --checkout rather than plain git checkout -b. This produces the GitHub issue→branch dev-link, which makes Closes #N automatic in the PR and lets the night-shift retro walk issue→PR without title-matching.
US-3d: Research-worker mode (kind:research)
When the dispatched issue carries kind:research, the planner routes to the research-worker prompt instead of the code-worker template below, and the merger asserts the research close-the-loop matrix instead of tests. The full prompt + matrix + folder convention live in [[canon:research-tasks]]. In short: the worker does deep web research (WebSearch + WebFetch + /ro:perplexity-research), writes a cited doc to docs/research/<slug>/README.md with bidirectional issue↔doc front-matter, updates the docs/research/README.md index, ingests the doc into the designated LLM wiki vault via /ro:wiki, posts a close-with-summary comment, and opens a docs-only PR. CI gate is lint/build only — research PRs touch docs/**, never application code, so there are no unit/integration/e2e tests to wait on. Research issues are always parallel-eligible (they only touch docs/research/, never conflicting with code workers' file-areas).
Worker prompt template (code workers; for kind:research see US-3d above and [[canon:research-tasks]]):
You are a WORKER agent for /ro:planner-worker.
Issue: .ralph/<name>/issues/<id>-<slug>.md (full body inlined here)
OR a GH issue number; in that case, FIRST run:
gh issue develop <issue-number> --name <slug> --checkout
to branch off the issue (NOT git checkout -b). This produces the
issue->branch dev-link required by the canon.
Worktree: .swarm/worktrees/<id>/
You are pinned to this worktree. Do NOT touch sibling worktrees.
Shared state lives in the PRIMARY checkout's .swarm/, NOT your worktree
(.swarm/ is gitignored, so a relative .swarm/... path writes a private copy
nobody else sees). FIRST resolve it once:
source <repo>/skills/planner-worker/scripts/swarm-shared.sh # if available
# else: SWARM="$(cd "$(git rev-parse --git-common-dir)/.." && pwd)/.swarm"
Then use swarm_read_learnings / swarm_read_claims / swarm_learn / swarm_claim
(or $SWARM/shared-learnings.md and $SWARM/claims.jsonl directly).
Workflow:
1. On arrival: swarm_read_learnings (repo quirks your wave-mates already hit)
and swarm_read_claims (which paths other live workers hold)
2. Read the issue body
3. swarm_claim '{"worker","issue","paths","ts"}' BEFORE editing an area — not
after. Re-read claims and append a fresh claim BEFORE any scope expansion
(touching a path you didn't originally claim)
4. NEVER edit a path another live claim holds. On a clash: do NOT edit —
flag it in $SWARM/status.md and your final report; the planner arbitrates
5. Implement the feature/fix
6. swarm_learn "..." (build quirks, flaky tests, migration gotchas) as
one-liners AS YOU HIT THEM, not at the end
7. Run the DoD commands (auto-detected or .swarm.json `dod`)
8. Only exit successfully when ALL DoD commands pass
9. Commit on the worktree branch with an emoji-conventional message
10. Append a one-line summary to $SWARM/logs/<id>.log (primary checkout, not your worktree)
11. Before squash-merge (or just before exit, whichever applies in your dispatch shape): `gh issue edit <NUM> --remove-label in-progress`. The `Closes #N` autoclose strips the issue's open state but does NOT strip labels; without this step the closed issue keeps its stale `in-progress` label and the canonical lifecycle invariant breaks.
Worktree guard — run BEFORE every git command (including `git status` / `git rev-parse`):
```bash
test "$(git rev-parse --abbrev-ref HEAD)" != "main" \
|| { echo "ERROR: on main branch, refusing to commit"; exit 1; }
test "$(pwd)" != "<MAIN_REPO_ABSOLUTE_PATH>" \
|| { echo "ERROR: in main checkout, refusing to commit"; exit 1; }
If either check fails, exit "stuck" immediately — do NOT recover by switching branches. Lesson #5a documents the failure mode: the Agent tool's isolation: "worktree" flag occasionally fails to take effect and the worker lands in the shared main checkout; once there, every subsequent git op mutates the main checkout's index instead of the worktree's. Branch protection on main is the load-bearing backstop (rejects the eventual push), but the in-prompt guard catches the drift earlier and avoids a "stuck on rejected push" loop.
If git push is rejected by GitHub's branch-protection hook (e.g. remote: error: GH006: Protected branch update failed), do NOT retry, do NOT use --force, --force-with-lease, or --admin. That rejection means you drifted out of your worktree onto main. Exit "stuck" with the rejection message verbatim so the orchestrator can intervene.
Rules:
- One commit per logical change, but ALL must land before exit
- Commit early and often: commit each coherent unit AS SOON as it exists, so progress survives a kill or crash. A worker that dies mid-task should leave commits behind, not a pile of uncommitted edits (lesson: a nutmeg wave-5 worker died with 111 uncommitted lines; attempt 2 with this protocol shipped clean)
- NO silent waits over 5 minutes on builds/tests. If a command hasn't returned in 5 minutes: kill it, diagnose verbosely (re-run with verbose flags, read the logs), and report what you found — never sit quietly
- Do not push (the merger handles that)
- Do not edit issues/, prd.md, or other worktrees
- If you fail DoD after one retry with the failure context, exit with status "stuck" and a one-line cause to .swarm/logs/.log
## US-4: DoD detection + override
Auto-detect from `package.json` in this order:
1. `.swarm.json` `dod` block (highest precedence)
2. If `package.json` has `scripts.test` and `scripts.typecheck`: use them
3. Else if `tsconfig.json` present: `tsc --noEmit`
4. Else: `pnpm test` if `pnpm-lock.yaml` present; `npm test` if `package-lock.json` present
If `.swarm.json` lists `dod.extras`, append those (e.g. `pnpm lint`, `./verify-quick.sh`).
Worker exits successfully **only** when every DoD command passes (non-zero exit = stuck).
## US-5: Merger agent
When a worker reports green, dispatch a **merger Agent** (Opus) for that worktree:
You are the MERGER agent for /ro:planner-worker, issue .
Worktree: .swarm/worktrees// Merge target:
Workflow:
- cd into the worktree
- Read the diff (git diff ...HEAD)
- Sanity check vs the issue spec: does the diff match the DoD? Any obvious red flags?
- git rebase
- Re-run the DoD commands on the rebased branch
- If clean: cd back to repo root and
git merge --no-ff swarm/<id>(orgh pr merge --squashif --github) - On success:
git worktree remove .swarm/worktrees/<id>andgit branch -D swarm/<id> - On any failure (rebase conflict, DoD red, or red flags in the diff): STOP, write one-line cause to .swarm/status.md, exit "escalated"
MANDATORY stale-squash guard (--github mode): after ANY
git push --force-with-lease (e.g. post-rebase), do NOT merge blind.
Re-read the PR head:
remote_sha=$(gh pr view <PR#> --json headRefOid --jq .headRefOid) local_sha=$(git rev-parse HEAD)
Only run gh pr merge --squash when remote_sha == local_sha (your rebased
HEAD). On mismatch: wait a few seconds and re-check — GitHub's view of the
branch lags the push. NEVER merge a stale head. Lesson: nutmeg night-shift
ns-20260611-1624 — a merge fired immediately after a force-push, the squash
captured the pre-push branch state, main landed red, and fix-forward #170
was needed. Applied manually for waves 5-6: zero recurrence.
The merger has merge authority on the merge-target branch. Default = the branch the skill was invoked from. `--staging-branch swarm-staging` routes merges to a staging branch instead; main is updated only when the user explicitly merges staging.
## US-6: Escalation
If the merger exits `escalated`:
1. Leave the worktree intact (forensics)
2. Append a one-line summary to stdout AND `.swarm/status.md` (under `Escalations:`)
3. Mark the issue `status: escalated` in its file
4. The dispatcher continues with the next unblocked issue
5. On all-issues-resolved-or-escalated, the run pauses for the user
6. User fixes the conflict, then `/ro:planner-worker --resume` picks up where we left off
## US-7: Worker failure -> planner re-plan (retry-and-split state machine)
Implements [[close-the-loop-verification-matrix]] § "Retry-and-split state machine". If a worker exits stuck:
1. Mark the issue `status: stuck`
2. **Attempt 2** — Retry ONCE with the failure context inlined in the worker prompt (single retry, fresh context, same models). Capture the failure-class (the matrix row that failed) so the retro can track patterns.
3. If the retry hits a **different** failure-class, treat that as attempt 2.b (reset, no counter bump): re-dispatch fresh with both failure contexts injected as constraints.
4. **Attempt 3** — If still stuck on the same failure-class, re-dispatch with model bump (Opus implementer + Opus reviewer; both fresh contexts).
5. **Auto-split** — If attempt 3 fails: re-invoke the **planner Agent** with:
- The stuck issue body
- All three workers' failure logs
- The matrix-row(s) that failed across attempts
- Instructions to decompose this single issue into 2-4 smaller sub-issues that route around the failure mode
6. New sub-issues are appended to `.ralph/<name>/issues/` with ids like `001a`, `001b` and `depends_on` set sensibly. When `--github`, they also get GH issue numbers via the standard slicer flow and the parent gets the `split-by-loop` label.
7. Original stuck issue is marked `status: replaced-by: [001a, 001b]`
8. Dispatcher picks up the new sub-issues next cycle
9. **Hard escalation** — If the same parent ancestry triggers auto-split twice (e.g. `001` → `001a` + `001b`, then `001a` → `001a-i` + `001a-ii`), STOP and escalate to HITL via Pushover + Telegram. Anthropic's "0% across many trials = broken task" rule says the spec is wrong, not the agent.
### Emit `failures[]` for the retro
For every auto-split, EVERY model-bumped attempt, AND every successful merge that required >= 2 attempts, append a JSON line to `.swarm/failures.jsonl`:
```json
{
"slice": 234,
"mode": "thrashing|proxy-gaming|misalignment|non-convergence|context-drift|runaway-resource|other",
"evidence_ref": "<PR url or .swarm/logs/<id>.log>",
"narrative": "<one to two sentences>"
}
/ro:night-shift-retro reads .swarm/failures.jsonl as part of US-1 (Gather run data) and folds entries into the retro JSON's failures[] block. The retro classifier may re-classify the mode based on cross-slice patterns (a single thrashing looks like non-convergence; three across slices is a SYSTEM signal).
This is the only path that re-invokes the planner mid-run.
US-8: --github flag
When --github is set:
- The planner ALSO calls
gh issue create --title <title> --body <body> --label swarmfor each backlog issue and stores the issue number in the issue file's frontmatter (github-issue: 42) - Workers
git push origin swarm/<id>andgh pr create --base <merge-target> --head swarm/<id> --title <title> --body "Closes #42" - Merger uses
gh pr merge --squash --delete-branchinstead of local merge — gated by the MANDATORY stale-squash guard: after anygit push --force-with-lease, re-readgh pr view <PR#> --json headRefOidand only merge when it equals the local rebased HEAD SHA; on mismatch wait and re-check, never merge a stale head (nutmeg #168 incident: the squash captured pre-push state, main landed red, fix-forward #170 required) - On escalation, the PR is left open with a
swarm:escalatedlabel added - Prereq:
gh auth statusmust be green;gh repo viewmust resolve; the skill fails loudly otherwise
US-9: --judge-agent
Default termination: empty backlog = exit.
With --judge-agent, after each dispatch cycle (one cycle = "dispatch all unblocked, wait for all merged or escalated"), invoke a judge Agent (Opus):
You are the JUDGE agent for /ro:planner-worker.
PRD: .ralph/<name>/prd.md (re-read it)
Backlog state: .ralph/<name>/issues/ (re-read all)
This cycle: <summary of merged + escalated>
Decide: KEEP_GOING or STOP.
KEEP_GOING means: the PRD has goals not yet covered by the merged backlog, and you can describe what's missing in 1-3 bullets. Returning KEEP_GOING re-invokes the planner with those bullets.
STOP means: the PRD's acceptance criteria are met, or all remaining work is escalated, or you're going in circles.
Hard caps (regardless of judge): --max-cycles (default 10), --max-runtime (default 4h).
KEEP_GOING re-invokes the planner with the judge's missing-work bullets; STOP exits cleanly.
US-10: --afk
--afk is the autonomous combination flag. It implies:
--auto-approve(skip US-2 confirmation)--judge-agent(Cursor-style termination)--skip-grill(use defaults for unspecified flags)
Always fire completion-report + Pushover at end (load-bearing default)
ANY /ro:planner-worker run against a real backlog ends with TWO tail calls, in this order:
/ro:completion-report --prs <merged-pr-list> --title "<prd-name> swarm" --no-open— writes a browsable HTML report to<repo>/.completion-reports/<ts>-<slug>.htmlwith per-PR cards (CI status, file stats, summary), per-file diffs, per-PR revert command, per-file rollback command, and a risk panel (schema migrations, env changes, large deletions, lockfile-only PRs, no-tests-on-feat). Capture the absolute path it prints. Pass--prsrather than--prdbecause the swarm tracks the merged PR list directly; falls back to--prd <name>if PR list isn't available./ro:pushover --url file://<path-from-step-1>— sends the done / paused / blocked / crashed ping with the report path as a deep link.
If step 1 reports an empty range, skip --url on step 2 but STILL send the ping with state.
Pushover message anatomy:
night shift done: <N> stories merged, <M> stuck, <duration>
with the report URL attached so tapping the notification opens the diff browser.
Skip BOTH tail calls ONLY when:
--plan-only(planner runs but no workers dispatch)- User explicitly passed
--no-ping
Per the global Pushover firing rule (see ~/CLAUDE.md § Pushover Notifications, condition 4) and the completion-report skill at ~/Dev/ronan-skills/skills/completion-report/SKILL.md.
US-11: --workers N
N defaults to 5. Hard ceiling of 10 without --unsafe-many. Above 10:
WARNING: >10 worktrees will likely thermal-throttle your laptop and consume
<estimate_mb> MB of disk via duplicate node_modules. Pass --unsafe-many to
override.
The skill computes estimate_mb as repo_size_mb * worker_count.
US-12: --resume
After a human-resolved escalation, --resume:
- Re-reads
.swarm/status.mdand.ralph/<name>/issues/*.md - Re-dispatches any issue with
status: readyand unmetdepends_onnow met - Re-attempts any issue with
status: escalatedwhose worktree was modified since the escalation timestamp - Idempotent: running
--resumeon a clean state is a no-op
US-13: Live status + logs
During the run:
.swarm/logs/<id>.logis the worker's own append-only log.swarm/status.mdis rewritten on every state change with this template:
# planner-worker live status
PRD: <name>
Started: <ISO8601>
Cycle: <N> of <max>
## Workers active
- 002 (worker, 4 min)
- 003 (merger, rebasing)
## Merged
- 001 schema: add points column <commit-hash>
## Escalated
- (none)
## Stuck (retrying)
- (none)
## Last 5 events
- 12:14:02 worker-001 GREEN
- 12:14:05 merger-001 dispatched
- 12:14:18 merger-001 MERGED abc1234
- 12:14:19 worker-002 dispatched
- 12:14:19 worker-003 dispatched
Tail with watch -n 2 cat .swarm/status.md or code -r .swarm/status.md.
US-14: Postmortem on exit
On final exit, the skill writes .swarm/run-<ISO8601>.md:
# Swarm run postmortem
PRD: <name>
Duration: 47m 12s (started 2026-05-14T07:30Z, ended 2026-05-14T08:17Z)
Workers cap: 5
## Planned issues: 5
- 001 schema: add points column
- 002 service: award points on completed task
- 003 dashboard: tile
- 004 hook: usePoints
- 005 integration test
## Merged: 4
- 001 abc1234 (4 min worker, 2 min merger)
- 002 def5678 (8 min worker, 1 min merger)
- 003 ghi9abc (12 min worker, 3 min merger)
- 004 jkl0def (6 min worker, 1 min merger)
## Stuck: 1
- 005 integration test: failed e2e setup after retry; planner re-decomposed into 005a,
…(truncated)