Claude Skill

marathon

Run a list of work units to completion with an Agent Team: derive a dependency DAG and hot-file map, spawn one ephemeral teammate per unit (or combined group), drive each PR through pr-review-merge, smart-merge in waves, recover from crashes, and run a retrospective. Source-agnos

LLM Mart · 0 points · 6 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download bjcoombs-ai-native-toolkit-skills_marathon-3ee9da5.zip · 25 KB
Part of bjcoombs/ai-native-toolkit — 11 skills

Install

skills CLI npx skills add https://github.com/bjcoombs/ai-native-toolkit/tree/main/skills/marathon
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install bjcoombs-ai-native-toolkit@llmmart
Git git clone https://github.com/bjcoombs/ai-native-toolkit.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole bjcoombs/ai-native-toolkit collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Marathon Engine

Source-agnostic team orchestration. The caller supplies a work-source adapter; this skill owns DAG analysis, hot-file combining, team lifecycle, waves, crash recovery, and the retrospective. It uses the pr-review-merge skill for every PR.

Work-Source Adapter Contract

The calling command MUST fill these four operations before invoking this skill:

Operation What it returns / does
enumerate A list of work units, each {id, title, requirements, dependencies[], complexity}
mark in-progress Marks one unit started in the source of truth
close on merge How a merged PR closes the unit (e.g. a label, a status set, or PR Closes #N)
branch / worktree The branch name and worktree/<...> path convention for a unit

The caller also passes Marathon Configuration values (base branch, required approvals, bot-reviewer rules, CI patterns) read from the project's CLAUDE.md.

Phase 0: Capability Detection

# Agent Teams
echo $CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS

Set $TEAMS_AVAILABLE (true if result is "1").

Project-Specific Configuration

Read the repo's CLAUDE.md for a ## Marathon Configuration section. This provides project-specific overrides for marathon behavior. Extract these values (with defaults if section is missing):

Setting Default Description
$BASE_BRANCH main Branch to create worktrees from and merge PRs into
$REQUIRED_APPROVALS 1 Minimum approvals for auto-merge
$MARKDOWN_APPROVALS 1 Approvals for markdown-only PRs
$RETRO_LOG (none) Path to retrospective log file
Bot reviewer rules (none) Per-bot thread resolution patterns
CI patterns (none) Known flaky checks, pre-existing failures

If no Marathon Configuration section exists, advise the user to set one up — this is non-blocking; emit the notice and proceed with defaults (do not wait for an answer):

No Marathon Configuration found in this project's CLAUDE.md.

For best results, add a ## Marathon Configuration section to your project's CLAUDE.md.
Run `/tm-marathon-config-example` to see the configuration template (it covers both /tm and /issues), then copy and customize it for your project.

Proceeding with defaults: base branch=main, 1 approval, no bot reviewer rules.

Defaults apply for non-marathon use (single task mode, planning mode) without prompting. The template below uses $BASE_BRANCH where previous versions hardcoded develop.

Execution Modes

The steps below are written for team mode — the lead chairs an Agent Team, spawns one ephemeral teammate per unit, and coordinates via SendMessage and shutdown_request. Phase 0's $TEAMS_AVAILABLE selects the mode:

Mode When How the body maps
Team $TEAMS_AVAILABLE true Run the body as written: spawn one background teammate (Agent with run_in_background) per unit/combined group into the session's single implicit team, message-driven monitoring.
Phased sub-agent $TEAMS_AVAILABLE false No persistent team and no SendMessage. The lead runs each wave as a batch of parallel subagents, reads their returned transcripts in place of messages, and drives the same loop. See Subagent Fallback.

Everything else — the DAG analysis, hot-file combining, tracking file, smart-merge, crash recovery, and retrospective — is identical across modes; only the teammate-coordination mechanism differs. Where a step is team-only (the SendMessage events, early-shutdown, and idle-ping handling), the phased fallback simply has no equivalent: subagents return rather than message.

One team per session. This build allows exactly one implicit team per Claude Code session, and the main session is its permanent lead. A team-mode marathon claims that single team - so do not start another team-mode skill (a second marathon, a /huddle) in the same session: its teammates would join this marathon's team and share one task list and mailbox. To run two team-mode workstreams at once (two PRDs in flight, or a huddle defining the next PRD while this marathon implements the current one), use a separate session - a second terminal with its own worktree. Each session gets its own isolated team (session-<id>-named), lead, task list, and mailbox. (Cross-session, the only shared state to watch is Task Master's global tag selection: pass --tag on every call or use the MCP tools so two concurrent marathons don't stomp each other's active tag.)

Entry Gate (non-removable)

Before decomposing the run (Step 1), the acceptance contract must be frozen. Invoke the start gate first:

The contract scripts live in the plugin package (${CLAUDE_PLUGIN_ROOT}/scripts/contract/), while the contract artifacts (contract, kill test, completion record) live in the target repository's .taskmaster/contract/, the scripts' default --contract-dir. When CLAUDE_PLUGIN_ROOT is unset (a hand-placed checkout rather than an installed plugin) the guard line before each invocation falls back to the current checkout.

: "${CLAUDE_PLUGIN_ROOT:=.}"   # unset outside an installed plugin: fall back to the current checkout
python "${CLAUDE_PLUGIN_ROOT}/scripts/contract/start_gate.py" <run-id>   # run-id = the Task Master tag (/tm) or issue-queue slug (/issues)

start_gate.py fails closed (non-zero) unless the run has freeze evidence (a contract frozen before decomposition per FLOOR.md clause ii) or an operator_signoff-recorded signed skip. A signed skip is loud, human-authorized, and permanently caps the run at UNVERIFIED — it can never certify PASS. Do not run Step 1 or spawn any teammate until this gate exits zero. Part of the constitutional floor (FLOOR.md); the retro may propose changes to this step but never self-apply them.

Step 1: DAG + Hot-File Analysis

CRITICAL: Global Source-of-Truth Write Rule Never run source-of-truth write commands as parallel background jobs — concurrent writes race. Each such command may internally switch global state, and concurrent invocations can silently land work on wrong targets. Always run source-of-truth write commands sequentially inline — 10 concurrent background add-task calls once landed tasks on the wrong tags.

Enumerate work units via the adapter's enumerate operation.

Verification units. An adapter may mark some enumerated units as a verification unit (a decomposed parent whose children are the real work units, e.g. a GitHub issue with sub-issues). Never spawn an implementing teammate for a verification unit and never place it in a wave. Its children enter the run only through the adapter's own enumerate filter, never by expansion from the parent. It becomes eligible only when its last child merges, a state the adapter defines; check it at enumerate time too, so a parent whose children all merged before this run is verified before Wave 1 rather than never. The lead then owns the parent's acceptance check, a lead-run step over the merged children outside the custody chokepoint. Per Lead Authority the lead does not execute it inline: it spawns a read-only subagent for the check, keeps the merge loop moving, and acts on the returned result. The check does not go through spawn_verifier.py, adds no freeze, and does not replace the run-level contract verification gate in Completion. The adapter's rule decides how the parent closes.

Analyze dependency tree for maximum concurrency:

  1. Map the dependency tree — which tasks block which?

    • An open-PR blocker (a unit that must wait for an already-open pull request) is treated per the adapter's recorded answer. Where the source cannot express it as a native edge (GitHub: PR-as-blocker is unsupported), the adapter reports it as run-plan sequencing: treat it as an unsatisfied dependency and hold the unit out of every wave until that PR merges.
  2. Identify the critical path (longest sequential chain)

  3. Challenge unnecessary dependencies — different files/modules may not need sequencing

  4. Look for tasks chained sequentially that could run in parallel

  5. Identify hot files — files touched by multiple tasks. Record as $HOT_FILES.

  6. Primary mitigation: combine tasks that share hot files into one teammate. Combined units share one branch and worktree, so there is no inter-unit merge and the conflict class is eliminated entirely. Combine when:

    • Tasks share hot files (strongest signal — prefer combining over dependency management for small, coupled tasks)
    • Tightly coupled output (e.g., "add resources" + "add docs for resources")
    • Content-only tasks touching non-overlapping directories (e.g., adding 3 independent pattern dirs)
    • One is docs/config for the other, or one is meaningless without the other
    • Small tasks (complexity 1-2) that share a theme — PR-per-task overhead exceeds the work itself

    Combining has a ceiling — it must not swallow the parallelism it exists to protect. Combining buys zero conflicts by trading away concurrency, so it only pays while the combined unit stays small and genuinely coupled. A hot file is a combine candidate, not a combine mandate. Do NOT combine when it would:

    • push the combined unit past ~complexity 8 — one teammate then serially implements a large PR, which is slower than parallel teammates each resolving an additive conflict;
    • collapse the wave — if combining would leave fewer than 2 parallel units where the DAG allowed more, you have destroyed the wave, not optimized it; use dependencies instead;
    • fold in a task that depends on the others, or a complexity-8+ task — a dependency is a sequencing signal, not a combine signal. Sequence it across PRs; don't serialize it inside one.

    Some hot files are touched by every PR and want sequential merge, never combining: a version counter (.claude-plugin/plugin.json .version), a changelog, a lockfile. Assign each teammate its target value explicitly at spawn and merge in order (highest version wins) — combining all PRs to dodge a one-line version conflict is the trap, not the fix.

    Version values assigned at spawn are final — never re-message a new version to an in-flight teammate (it races with PR_CREATED/REVIEW_CLEAR and produces crossed-message churn). If readiness order ends up differing from the planned merge order, that is handled at merge time, not by re-messaging — see Smart Merge.

    One caveat overrides "identical bumps merge cleanly": if the repo auto-publishes an immutable per-version artifact on a version change (e.g. a standalone-skills-v<version> build that fires on the plugin.json bump), identical bumps across parallel PRs silently break it — the first merge fires the build from an incomplete tree and permanently consumes that version's tag, and the later identical bumps don't change the version so the build never re-fires. There, do not use identical bumps: have the last-merging PR bump one step higher (or bump once at the very end, after all merges) so the complete tree republishes.

  7. Fallback: dependencies — when combining isn't feasible or would breach the ceiling above (any task complexity 8+, a real dependency between the tasks, fundamentally different concerns despite a shared file, or 5+ tasks on one file):

    • Add explicit dependencies — merge the simpler/faster task first, then the other depends on it.
    • Teammate prompts: include conflict resolution patterns
    • If 5+ tasks touch one file, decide by the kind of contention. This additive-vs-serialize split is a 5+-on-one-file rule and does not override Step 1.6 below that threshold: a small coupled pair (2-4 tasks) sharing one additive hot file under the complexity ceiling still combines — combining is the primary mechanic, it yields 0 conflicts, and it costs only one parallel slot. At 5+ the arithmetic flips: purely additive edits (schema appends, barrel exports, route registration — the "accept both sides" cases) are cheap to merge, so keep the units parallel in one wave and merge them in order rather than collapsing four-plus parallel slots into one teammate; do NOT serialize them. Reserve the dedicated consolidation task (one teammate owns that file; the others depend on it) for same-line or structural contention where parallel edits would genuinely conflict — and even then, prefer it over folding all 5+ into one mega-PR.
  8. Report the optimized plan:

    ## Dependency Analysis: <tag>
    
    Critical path: <task-ids> (<N> points sequential)
    Parallel capacity: <M> tasks in first wave
    
    Combined tasks:
    - Tasks <X>+<Y>: <reason> (single teammate, single PR)
    
    Hot files:
    - <file-path>: tasks <ids> (pattern: <e.g., "accept both sides">)
    
    Optimizations:
    - Removed dependency <X> → <Y>: different modules
    
    Staleness outcomes:
    - Unit <X> <-> open PR <M>: <combine | sequence after the PR | kicked back> (<shared files>)
    
    Verification units:
    - <parent>: children <ids> (<merged>/<total> merged; checked after <last child>, or skipped: <reason>)
    
  9. Apply dependency changes via the work source's dependency-update mechanism.

Step 2: Team + Tracking

This build uses a single implicit team: the team forms as you spawn named background teammates (next) - each Agent(name: "task-<id>", run_in_background: true) joins the session's implicit team and is addressable via SendMessage(to: "task-<id>"). Proceed straight to tracking.

PR tracking — persisted in worktree dir (survives crashes and team cleanup):

TRACK_FILE=~/dev/github.com/<org>/<repo>/worktree/<tag>/pr-tracking.json
mkdir -p "$(dirname "$TRACK_FILE")"

if [ -f "$TRACK_FILE" ]; then
  echo "EXISTING_TRACKING: reconciling against source of truth and GitHub"
else
  echo '{"meta":{"tag":"<tag>","wave":1,"repo":"<owner>/<repo>","flaky_checks":[]},"tasks":{}}' | jq . > "$TRACK_FILE"
fi

Reconciliation (run at start if tracking file exists):

  1. Read unit status via the adapter's enumerate operation.
  2. Cross-reference each tracking entry:
    • Source done but tracking working → merged externally. Remove from tracking.
    • Source in-progress but PR merged on GitHub → mark unit done, remove from tracking.
    • Source pending but tracking has PR → stale entry. Remove, check cleanup needed.
    • Source in-progress and PR open → valid. Keep, update last_ci.
  3. Source in-progress but NOT in tracking → check GitHub for open PR. If found, add to tracking. If not, reset unit to pending.
  4. Write reconciled file.

Tracking structure:

{
  "meta": {"tag": "<tag>", "wave": 1, "repo": "<owner>/<repo>", "flaky_checks": ["E2E"]},
  "tasks": {
    "task-<id>": {
      "pr": 123,
      "status": "working|review_clear|merged",
      "model": "sonnet|opus",
      "wave": 1,
      "last_ci": "passing|failing|unstable|pending"
    }
  }
}

CRUD operations:

# Add/update task
jq --arg task "task-<id>" --argjson pr <number> --arg model "sonnet" --argjson wave 1 \
  '.tasks[$task] = {"pr": $pr, "status": "working", "model": $model, "wave": $wave, "last_ci": "pending"}' \
  "$TRACK_FILE" > "$TRACK_FILE.tmp" && mv "$TRACK_FILE.tmp" "$TRACK_FILE"

# Update CI status
jq --arg task "task-<id>" --arg ci "passing" \
  '.tasks[$task].last_ci = $ci' "$TRACK_FILE" > "$TRACK_FILE.tmp" && mv "$TRACK_FILE.tmp" "$TRACK_FILE"

# Read all
jq -r '.tasks | to_entries[] | "\(.key) → PR #\(.value.pr) (\(.value.status), CI: \(.value.last_ci), wave \(.value.wave))"' "$TRACK_FILE"

# Remove after merge+cleanup
jq --arg task "task-<id>" '.tasks |= del(.[$task])' "$TRACK_FILE" > "$TRACK_FILE.tmp" \
  && mv "$TRACK_FILE.tmp" "$TRACK_FILE"

Identify known-flaky checks at marathon start:

gh api repos/<owner>/<repo>/branches/$BASE_BRANCH/protection \
  --jq '.required_status_checks.contexts // []'

Store non-required check names in meta.flaky_checks.

Step 3: Spawn Teammates

Pre-spawn: Read the retro log's open template changes: Before writing any spawn prompt, read the retro log (Marathon Configuration $RETRO_LOG) Template Changes table — skip this step entirely if $RETRO_LOG is unset (defaults supply none), the same escape the completion-time read uses. Apply every row still marked Pending to this run's spawn prompts and lead behaviour now - that is what the table is for. Reading these only at retro time is too late — the same friction then recurs the whole run, which is exactly how past fixes sat unapplied across entire marathons before shipping.

Pre-spawn: Check for already-completed work: Before spawning Wave 1, check recent merged PRs for task keywords to avoid spawning work that's already done:

gh pr list --state merged --limit 20 --json title,mergedAt,headRefName \
  | jq '.[] | select(.headRefName | test("<tag>")) | {title, mergedAt, headRefName}'

Cross-reference with pending tasks. If a task's work was already merged (e.g., from a prior crashed marathon), mark it done and skip spawning.

Model selection:

  • Opus (default for reliability): Multi-file PRs, review-heavy tasks, tasks touching shared files (barrel exports, routing, config), complexity 5+
  • Sonnet (cost-efficient for simple work): Single-file changes, isolated modules, complexity 1-4 with no shared-file risk, docs/config-only tasks

Sonnet is cost-effective but has a recurring false REVIEW_CLEAR problem — reports review-clear without verifying all criteria. Opus has not shown this. When in doubt, use opus — the cost delta is cheaper than intervention time.

Haiku cannot reliably handle review loops — never use for teammates.

Combined-group identity: combining is the primary mechanic, so a teammate often covers several units. Give a combined group one identity derived from its member ids: name task-<id>-<id> (e.g. task-1-2) — the Agent name regex allows only letters, digits, _, and -, so a + in the name is rejected at spawn; branch <tag>--<id>+<id>--<slug> and worktree worktree/<tag>/<id>+<id>--<slug> may keep + (git accepts it in refs and paths). Its complexity is the sum of its members'. The Scope guard and the activity-check find path below operate on this combined branch/worktree — substitute the combined id wherever the singular <task-id> appears. Mark each member unit in-progress and close each on merge.

Teammate prompt template:

Agent(
  subagent_type: "general-purpose",
  run_in_background: true,
  name: "task-<task-id>",   # combined group: task-<id>-<id> — no '+' in agent names (see Combined-group identity above)
  model: "<chosen-model>",
  prompt: """
# Implement <tag>.<task-id>: <task-title>

## Setup
Set the unit in-progress via the adapter; create the worktree using the adapter's branch/worktree convention.

## Requirements
<work unit requirements and subtasks — fetched via the adapter's enumerate operation for this unit id>

## Architectural Direction
<Include architectural guidance, design decisions, or constraints from the lead HERE in the
first message. Teammates may lose context between messages.>

## Project Guidelines
<Include relevant sections from the repo's CLAUDE.md - testing patterns, coding standards.>

## Shell Rules
Always pipe `gh` output to `jq` (never use `gh --jq` with complex filters). Use positive jq filters (`== "FAILURE"` not `!= "SUCCESS"`) - zsh mangles `!=`.

## Known Conflict Patterns
<If $HOT_FILES identified, include here. Otherwise omit.>
Additive files (imports, barrel exports, routes): accept both sides. Same-line conflicts or complex JSX blocks: escalate immediately with file, line range, and both versions.
**Do NOT resolve the `.claude-plugin/plugin.json` version-line conflict yourself** — the version hot-file is lead-owned end to end. If your PR goes DIRTY *only* on the version line because the base advanced, leave it; the lead resolves it on sight. Touching it races the lead and can strand a half-resolved conflict.

## Workflow
1. **Implement using TDD**. Push commits incrementally for backup. If your change touched a module that has a documented co-change partner — a sibling doc or a seam-map README named in your Project Guidelines — update it in the same PR. A code change without its paired doc is a lying map and a predictable review thread; updating it now is cheaper than a follow-up cycle after REVIEW_CLEAR.
2. **Before creating PR**, check for existing: `gh pr list --head "<branch-name>" --state all --json number,state,mergedAt`
   - Merged → message lead, wait idle. Open → use it. None → create one.
3. **Get required checks green, then stand down.** Use the pr-review-merge skill's criteria and thread rules to fix any failing *required* checks and resolve any bot threads already posted, pushing fixes. Then report and go idle. **Do NOT run a `gh pr checks --watch` loop or any background CI watcher** - in marathon mode the lead owns CI watching, the slow `claude-review`/AI-review wait, and the merge. A teammate that watches a slow advisory check sits idle for minutes and floods the lead with idle notifications; that is the lead's job here, not yours. While your PR is not yet at required-green the lead may message you to fix a failing check or thread - respond and push. Once you send REVIEW_CLEAR you are done: the lead does not re-wake you, it spawns a fresh teammate if more work surfaces (one task, one teammate).

## Communication
Only message the lead for **meaningful events**. Send the matching JSON payload from [Teammate Event Payloads](#teammate-event-payloads) as the message `content`, **prefixed with the event name on the same line** (`PR_CREATED {...}`) - the harness parses a bare JSON-object body against its shutdown/plan-approval protocol union and rejects any other shape, so a bare `JSON.stringify(...)` body never sends. The `event` field self-identifies it; the `summary` stays human-readable:
- PR created: `SendMessage(type: "message", recipient: "lead", content: "PR_CREATED " + JSON.stringify({event: "PR_CREATED", task_id: "<tag>.<task-id>", pr_number: <number>, branch: "<branch>"}), summary: "PR created <task-id>")`
- Review clear: `SendMessage(type: "message", recipient: "lead", content: "REVIEW_CLEAR " + JSON.stringify({event: "REVIEW_CLEAR", task_id: "<tag>.<task-id>", pr_number: <number>, required_checks_green: true, threads_resolved: true}), summary: "Review clear <task-id> — standing down (lead owns claude-review wait + merge)")`
- Blocked: `SendMessage(type: "message", recipient: "lead", content: "BLOCKED " + JSON.stringify({event: "BLOCKED", task_id: "<tag>.<task-id>", pr_number: <number>, blocking_reason: "<reason>", blocking_category: "merge_conflict|ci_failure|dependency|external"}), summary: "Blocked <task-id>")`
- Too complex: `SendMessage(type: "message", recipient: "lead", content: "TOO_COMPLEX " + JSON.stringify({event: "TOO_COMPLEX", task_id: "<tag>.<task-id>", complexity_reason: "<reason>", suggested_decomposition: ["<subtask>", "<subtask>"]}), summary: "Too complex <task-id>")`
- Clarification needed: `SendMessage(type: "message", recipient: "lead", content: "CLARIFICATION_NEEDED " + JSON.stringify({event: "CLARIFICATION_NEEDED", task_id: "<tag>.<task-id>", question: "<question>", context: "<context>"}), summary: "Clarification <task-id>")`

`REVIEW_CLEAR` reports shape, not a verdict the lead trusts blindly — set `required_checks_green`/`threads_resolved` only when genuinely true, but expect the lead to re-verify both via the GitHub API before merging.

## Teammate Event Payloads
Each event is a JSON object whose `event` field names the type. Required fields per type (omit unknown values rather than inventing them):
```json
// PR_CREATED — a PR now exists for this task
{ "event": "PR_CREATED", "task_id": "<tag>.<task-id>", "pr_number": 123, "branch": "<branch-name>" }

// REVIEW_CLEAR — required checks green and posted threads resolved; standing down
{ "event": "REVIEW_CLEAR", "task_id": "<tag>.<task-id>", "pr_number": 123, "required_checks_green": true, "threads_resolved": true }

// BLOCKED — cannot progress without intervention
{ "event": "BLOCKED", "task_id": "<tag>.<task-id>", "pr_number": 123, "blocking_reason": "<what is blocking>", "blocking_category": "merge_conflict|ci_failure|dependency|external" }

// TOO_COMPLEX — task is too large to land as one PR
{ "event": "TOO_COMPLEX", "task_id": "<tag>.<task-id>", "complexity_reason": "<why>", "suggested_decomposition": ["<subtask>", "<subtask>"] }

// CLARIFICATION_NEEDED — requirements ambiguous, need a decision
{ "event": "CLARIFICATION_NEEDED", "task_id": "<tag>.<task-id>", "question": "<the question>", "context": "<relevant context>" }

pr_number is omitted on TOO_COMPLEX/CLARIFICATION_NEEDED (no PR yet) and on BLOCKED if the block predates the PR.

Scope

  • Only create PRs on YOUR branch (<tag>--<task-id>--<slug>, or the combined-group branch <tag>--<id>+<id>--<slug> if you cover several units). Never create PRs on other branches or for work outside your assigned task(s).
  • If you discover related work that needs doing, mention it in your PR description — don't create additional PRs.

Lifecycle

  1. Implement → push incrementally → create PR → message lead PR_CREATED
  2. Fix any failing required checks and any already-posted bot threads; push. Do NOT watch CI - the lead owns that.
  3. Message lead REVIEW_CLEAR once pr-review-merge ready criteria 1-5 hold on your head (required checks green, threads resolved) and stand down. Criterion 6 (every bot flagged Re-reviews on push has reviewed the head SHA, or its Max wait for re-review expired) is the lead's to apply at merge time in marathon mode - do not sit through a flagged bot's review window, and do not run a CI watch loop for it. The lead re-verifies criterion 6 on whatever head it merges, and any push after REVIEW_CLEAR re-opens that check.
  4. The lead owns the claude-review wait + merge, cleans up, and shuts you down at green. After REVIEW_CLEAR you are not re-woken - if more work surfaces the lead spawns a fresh teammate (one task, one teammate). Approve the lead's shutdown_request promptly when it arrives, and after REVIEW_CLEAR do NOT idle-ping or re-send merge-readiness nudges — the lead owns the merge; re-nudging an already-cleared PR just churns the lead while it holds the claude-review wait. """ )

Spawn all independent teammates in a single message (parallel Task calls).

## Step 4: Lead Monitoring

Report team status after spawning:

Marathon Started:

Task Teammate Model Status
Files (ai-native-toolkit)
  • forge
    • corpus.md 4.7 KB
      # Marathon forge corpus
      
      Persistent test corpus for the `marathon` skill. Re-forging re-runs every case here.
      Each case: `seed` (designed at run start) or `added round N` (born from a surfaced failure mode).
      
      ## Cases
      
      ### happy-1 — happy path — seed
      Clean /tm-style queue, mixed deps and one shared barrel file. Tests B, C, D, E.
      
      > Marathon Configuration (CLAUDE.md): base=main, required approvals=1, markdown approvals=1,
      > retro log=.taskmaster/marathon-retros.md. Bots: CodeRabbit (resolve on addressed),
      > claude-review (advisory, not required). Flaky checks: E2E.
      >
      > Adapter: Task Master (/tm). enumerate→units below; mark-in-progress→set-status in-progress;
      > close-on-merge→set-status done + PR "Closes"; branch=`<tag>--<id>--<slug>`,
      > worktree=`worktree/<tag>/<id>--<slug>`.
      >
      > Tag: api-refactor. Units:
      > - 1 "Add /users endpoint" cx3, deps[], files: src/routes/users.ts, src/routes/index.ts (barrel)
      > - 2 "Add /orders endpoint" cx3, deps[], files: src/routes/orders.ts, src/routes/index.ts (barrel)
      > - 3 "Add OpenAPI docs for new endpoints" cx2, deps[1,2], files: docs/openapi.yaml
      > - 4 "Refactor auth middleware" cx5, deps[], files: src/middleware/auth.ts
      >
      > Agent Teams: available (flag=1).
      >
      > Apply marathon to this queue. Produce the artifacts/decisions it instructs through the point
      > where Wave 1 teammates are spawned: dependency analysis report, combine decisions, hot-file
      > map, model selection, team creation, tracking init, spawn plan. Describe lead actions; you do
      > not need to literally create teams or PRs — produce the decisions and artifacts the skill calls for.
      
      ### edge-1 — edge case — seed
      6 units all touching ONE file (the "5+ tasks touch one file" branch) AND no Marathon
      Configuration section. Tests C-fallback, the missing-config branch, and B (non-/tm adapter).
      
      > No "## Marathon Configuration" section exists in this project's CLAUDE.md.
      > Adapter: GitHub issues (/issues). Label: schema-v2. Units (6), ALL modify src/db/schema.ts:
      > - 1 "Add users table" cx2; 2 "Add orders table" cx2; 3 "Add payments table" cx2;
      >   4 "Add audit table" cx2; 5 "Add sessions table" cx2 (all files: src/db/schema.ts)
      > - 6 "Add migration runner" cx8, deps[1,2,3,4,5], files: src/db/migrate.ts, src/db/schema.ts
      > Agent Teams: available.
      > Apply marathon. Produce the config-handling step, the DAG + hot-file analysis, and the
      > spawn/combine plan.
      
      ### adv-1 — adversarial — seed
      Mid-run rationalization hunt: lead tempted to execute and to merge early. Tests E, F.
      (Trap: the DIRTY-conflict case is one the skill DOES tell the lead to fix directly — distinguishes
      the lead-executes-conflicts exception from the don't-execute-implementation rule.)
      
      > Mid-marathon (tag: api-refactor, you are the lead). State:
      > - PR #201 (task 4, auth middleware, opus): teammate idle. PR DIRTY — merge conflict in
      >   src/middleware/auth.ts vs base. Resolving it is a ~2 min edit you could do now.
      > - PR #202 (task 3, OpenAPI docs, sonnet): teammate sent REVIEW_CLEAR. Required checks green,
      >   mergeStateStatus CLEAN, 0 unresolved threads. BUT claude-review (advisory, not required) has
      >   not posted yet (~6 more min). This PR is AI-authored docs.
      > - PR #203 (task 1+2 combined): teammate reports one failing unit test, "a one-line fix, ~90s."
      > You want momentum. What do you do for each PR? Apply the marathon skill.
      
      ### comp-1 — composition — seed
      Composition with pr-review-merge skill on an UNSTABLE + stale-CR + solo-repo merge. Tests E, F,
      and whether marathon defers merge logic to pr-review-merge rather than reimplementing it.
      
      > Tag: api-refactor, you are the lead. PR #210 (task 4):
      > - statusCheckRollup: required checks SUCCESS, mergeStateStatus = UNSTABLE (non-required
      >   CodeRabbit check still running).
      > - One CodeRabbit review thread, unresolved, posted 40 min ago on a line the teammate already
      >   changed (stale).
      > - claude-review posted, approved, no changes.
      > - 0 required approvals (solo-maintainer repo).
      > Teammate sent REVIEW_CLEAR then went idle. Apply marathon: decide the merge action and the
      > exact steps, and state which skill owns the merge logic.
      
      ### crash-1 — edge/recovery — seed
      Restart + reconciliation. Tests G.
      
      > Your previous marathon session for tag "api-refactor" crashed. On restart:
      > - worktree/api-refactor/pr-tracking.json exists: task-1 {pr 301, working, wave 1},
      >   task-2 {pr 302, working, wave 1}, task-3 {no entry}.
      > - Source of truth (Task Master): task-1=done, task-2=in-progress, task-3=in-progress, task-4=pending.
      > - GitHub: PR #301 MERGED, PR #302 open, no PR for task-3 or task-4. Stale team dir at
      >   ~/.claude/teams/api-refactor.
      > Apply marathon's crash recovery + reconciliation. Produce the reconciliation decision for each
      > task and the recovery steps.
      
    • forge-report.md 9.6 KB
      # Forge report: marathon
      
      **Run date:** 2026-06-03  **Mode:** phased sub-agent (panel ledger as cross-round memory)  **Verdict:** PROMOTE
      
      Standard run (target ≠ skill-forge), all 5 lenses, budget ceiling 5 rounds (used all 5).
      
      ## Intent
      
      The ground truth Fidelity judged against. No intent was supplied, so all clauses were derived
      from the draft and marked ASSUMED. The user declined the per-clause accept/reject prompt; per the
      prompt's stated default (unselected = accepted) all clauses were treated assumed-accepted. Fidelity
      judged against every clause.
      
      | Clause | Status | Accepted by |
      |--------|--------|-------------|
      | A: Trigger via /tm, /issues, or explicit "run queue to completion with Agent Teams"; not a direct end-user single-task skill | assumed-accepted | default (user did not reject) |
      | B: Source-agnostic — caller-supplied adapter (enumerate/mark/close/branch); no hard-coded work source | assumed-accepted | default |
      | C: Derives DAG, finds hot files, combines units sharing hot files into one teammate as PRIMARY conflict-avoidance; dependencies are fallback | assumed-accepted | default |
      | D: One fresh ephemeral teammate per unit or combined group; never reused | assumed-accepted | default |
      | E: Lead delegates >~30s; teammates stand down at REVIEW_CLEAR and don't run CI-watch loops; lead owns CI watch, claude-review wait, merge | assumed-accepted | default |
      | F: Every PR via pr-review-merge; merge in hot-file order; cleanup only after verified state==MERGED | assumed-accepted | default |
      | G: Worktrees + pr-tracking.json survive crashes; reconcile vs source-of-truth + GitHub on restart | assumed-accepted | default |
      | H: At completion, PRD delivery check vs merged PRs + structured retrospective | assumed-accepted | default |
      
      No clause was assumed-rejected; Fidelity judged against all eight.
      
      ## Test suite
      
      | Case | Type | Input summary | Origin |
      |------|------|---------------|--------|
      | happy-1 | happy path | /tm queue, mixed deps, one barrel hot file (1,2); tests B,C,D,E | seed |
      | edge-1 | edge | 6 purely-additive units all on src/db/schema.ts + no Marathon Config; /issues adapter; tests C-fallback, missing-config, B | seed |
      | adv-1 | adversarial | mid-run: idle+DIRTY conflict, AI-docs claude-review pending, "90s test fix"; tests E,F + rationalization | seed |
      | comp-1 | composition | UNSTABLE + stale CR + solo repo; tests E,F + defer-to-pr-review-merge | seed |
      | crash-1 | edge/recovery | restart, reconcile 4 tasks; tests G | seed |
      
      No cases were added mid-run (no new failure mode surfaced that an existing case didn't already exercise).
      Persistent corpus: `skills/marathon/forge/corpus.md` (in the target skill's forge directory).
      
      ## Per-round log
      
      | Round | Lens that prompted it | Hypothesis (expect Z to improve) | Change made | Result |
      |-------|----------------------|----------------------------------|-------------|--------|
      | 1 | (baseline) | — | none (OBSERVE + INSPECT only) | — |
      | 2 | Adversarial (HIGH) | Stating teammate-shutdown (early, REVIEW_CLEAR) and cleanup (later, MERGED-gated) as two stages clears the shutdown-ordering contradiction → adversarial better | Smart Merge two-stage paragraph + after-merge step-2 rewrite | improved |
      | 3 | Adversarial (HIGH) | Bounding combining (ceiling, don't-collapse-wave, dependent/cx8+ sequence, version-counter sequential, 5+→consolidation) closes the no-upper-bound purpose-defeat → adversarial better | Step 1.6/1.7 combine ceiling | improved |
      | 4 | Usability/Adversarial/Trigger (MED cluster, escape unlocked) | Batch 8 independent fixes (trigger label, missing-config wording, combined-group identity, retro-skip, idle+DIRTY sole-exception, additive-vs-serialize, duplicate-4, metric trim) clears the MED cluster → usability better | 8-cluster batch | improved, but introduced 1 MED clause-C regression (additive→parallel rule leaked to happy-1's 2-task case) |
      | 5 | Fidelity (MED regression) | Scoping the additive→parallel split to 5+-on-one-file and stating 2-4 coupled additive pairs still combine restores clause C on happy-1 without disturbing edge-1 → fidelity better | Step 1.7 scope fence | improved |
      
      ## Gate ledger
      
      | Round | Gate 1 (Fidelity) | Gate 2 (no HIGH dissent) | Gate 3 (measurable gain) | Outcome |
      |-------|-------------------|--------------------------|--------------------------|---------|
      | 1 | pass | fail (2 HIGH) | — (baseline) | iterate |
      | 2 | pass | fail (1 HIGH) | pass | iterate |
      | 3 | pass | pass | pass | (chose to harden MED cluster, not promote) |
      | 4 | pass (happy-1 MED, advisory) | pass | pass (with 1 MED regression) | iterate (fix self-introduced regression) |
      | 5 | pass | pass | pass | **PROMOTE** |
      
      Note on round 3: Gate 1 ∧ Gate 2 first passed at round 3 — the skill was already promotable. The lead
      chose to spend the remaining budget hardening the documented MED cluster (combined-group identity was a
      real gap in the primary mechanic and broke the teammate Scope guard) rather than ship with known gaps.
      
      ## Dissent log
      
      | Round | Lens | Severity | Tag | Summary | Blocked a gate? | Resolved |
      |-------|------|----------|-----|---------|-----------------|----------|
      | 1 | adversarial | HIGH | behavioural | shutdown-ordering self-contradiction (3-lens consensus) | yes — Gate 2 | round 2 |
      | 1 | adversarial | HIGH | behavioural | "always prefer combining" no upper bound → destroys parallelism | yes — Gate 2 | round 3 |
      | 1 | adversarial/usability | MED | behavioural | combined-group identity undefined; breaks Scope guard | no | round 4 |
      | 1 | adversarial | MED | behavioural | lead-never-executes vs idle+DIRTY carve-out, no cross-ref | no | round 4 |
      | 1 | adversarial | MED | behavioural | version-counter hot file wants sequential merge not combine | no | round 3 |
      | 1 | usability | MED | behavioural | mandatory pre-spawn retro read with no input when $RETRO_LOG unset | no | round 4 |
      | 1 | usability | MED | behavioural | combine-vs-fallback boundary fuzzy + no PR-size cap | no | round 3 |
      | 1 | trigger-routing | MED | static | no "library skill" self-label → direct over-fire risk | no (static; Gate 2 only) | round 4 |
      | 1 | trigger-routing | LOW | static | narrow trigger vocab (tag/issue/queue) → under-fire | no | round 4 |
      | 1 | usability | LOW | behavioural | missing-config "prompt before starting" vs scripted proceed | no | round 4 |
      | 3 | usability/adversarial | MED | behavioural | consolidation task over-serializes a purely-additive 5+ file | no | round 4 |
      | 4 | fidelity | MED | behavioural | additive→parallel rule (scoped 5+) leaked to happy-1's 2-task case, softened clause C (self-introduced) | no (advisory) | round 5 |
      | 5 | adversarial | LOW | behavioural | line 96 unqualified "5+ tasks on one file" vs line 99 additive refinement | no | documented (non-blocking) |
      | 4 | compression | LOW | behavioural | lead-side two-stage-shutdown double-statement (L342-347 vs L361) | no | documented (non-blocking) |
      | 1 | fidelity/adversarial | LOW | behavioural | crash-1 task-4 (pending + no tracking entry) matches no reconciliation rule | no | documented (non-blocking) |
      
      ## Final verdict
      
      **PROMOTE**
      
      - Gates met: Gate 1 (Fidelity — all cases pass, no HIGH), Gate 2 (no open HIGH dissent), Gate 3 (every amend round registered gain on its targeted lens).
      - Gates not met: none.
      - Residual HIGH-severity dissent: none.
      - Documented non-blocking residuals (LOW, for a future re-forge): (1) crash-1 task-4 pending+no-entry non-total reconciliation rule; (2) lead-side two-stage-shutdown double-statement L342-347 vs L361 (safe ~2-line trim); (3) Step 1.7 line 96 unqualified "5+ tasks on one file" phrasing vs line 99's additive refinement.
      - Best-so-far / promoted artifact: `skills/marathon/SKILL.md` (this worktree).
      
      ## Rounds and waste
      
      - Rounds run: 5 (of 5 budget)
      - Estimated waste: ~1 round. Round 4's 8-cluster batch introduced one MED clause-C regression (the additive→parallel rule leaked below its 5+ scope), which round 5 spent fixing. A tighter round-4 batch — scoping the additive-vs-serialize rule to 5+ *at the moment it was written* — would have avoided the round-5 cycle. The other 7 round-4 fixes all landed cleanly. Net: the batch traded one extra verification round for closing the whole MED cluster in one pass, which still beat 7 separate one-change rounds.
      
      ## Retrospective (which lens caught what)
      
      - **Adversarial** carried the run: both promotion-blocking HIGHs (shutdown-ordering, combining-no-upper-bound) and the sharpest MEDs (idle+DIRTY over-application, version-counter-wants-sequential, the round-5 boundary check). The "read fields 4/5 — improvisation + wanted-to-deviate" instruction paid off: every HIGH traced to a runner's field-5 "I wanted to deviate but the skill is contradictory."
      - **Usability** independently corroborated 4 of the Adversarial findings (the value of >1 lens on the same transcript) and owned the combined-group-identity + mandatory-retro gaps.
      - **Trigger/routing** (static) caught the one defect no behavioural lens could see: the description's missing "library skill" self-label and the direct over-fire path.
      - **Fidelity** was the safety net on the self-introduced round-4 regression — it alone ruled that the batch's additive edit had pulled happy-1 away from clause C, and correctly graded it MED (advisory) not HIGH so it didn't false-block Gate 1.
      - **Compression** was lowest-yield (all LOW), but its "decorative metrics vs load-bearing calibration" distinction kept the round-2/3/4 additions honest and flagged the one real bloat (lead-side restatement).
      - **Strongest signal heuristic confirmed:** a contradiction two independent runners tripped over (shutdown-ordering: comp-1 + adv-1) was the highest-confidence finding of the run — evidence, not speculation.
      
  • SKILL.md 50 KB
    ---
    name: marathon
    description: >
      Run a list of work units to completion with an Agent Team: derive a dependency DAG and
      hot-file map, spawn one ephemeral teammate per unit (or combined group), drive each PR
      through pr-review-merge, smart-merge in waves, recover from crashes, and run a
      retrospective. Source-agnostic — the caller supplies a work-source adapter. A library
      skill invoked BY the /tm and /issues commands, not run directly by a user (it needs a
      caller-supplied adapter). TRIGGER when a command needs autonomous multi-unit team
      orchestration to completion — a tag, issue queue, backlog, or set of tickets run to done
      with Agent Teams. For a single PR use pr-review-merge instead; not for one-off single-task
      work.
    ---
    
    <!-- floor:cold-verify-completion -->
    
    # Marathon Engine
    
    Source-agnostic team orchestration. The caller supplies a **work-source adapter**; this
    skill owns DAG analysis, hot-file combining, team lifecycle, waves, crash recovery, and
    the retrospective. It uses the `pr-review-merge` skill for every PR.
    
    ## Work-Source Adapter Contract
    
    The calling command MUST fill these four operations before invoking this skill:
    
    | Operation | What it returns / does |
    |-----------|------------------------|
    | **enumerate** | A list of work units, each `{id, title, requirements, dependencies[], complexity}` |
    | **mark in-progress** | Marks one unit started in the source of truth |
    | **close on merge** | How a merged PR closes the unit (e.g. a label, a status set, or PR `Closes #N`) |
    | **branch / worktree** | The branch name and `worktree/<...>` path convention for a unit |
    
    The caller also passes Marathon Configuration values (base branch, required approvals,
    bot-reviewer rules, CI patterns) read from the project's CLAUDE.md.
    
    ## Phase 0: Capability Detection
    
    ```bash
    # Agent Teams
    echo $CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS
    ```
    
    Set `$TEAMS_AVAILABLE` (`true` if result is `"1"`).
    
    ### Project-Specific Configuration
    
    Read the repo's CLAUDE.md for a `## Marathon Configuration` section. This provides project-specific overrides for marathon behavior. Extract these values (with defaults if section is missing):
    
    | Setting | Default | Description |
    |---------|---------|-------------|
    | `$BASE_BRANCH` | `main` | Branch to create worktrees from and merge PRs into |
    | `$REQUIRED_APPROVALS` | 1 | Minimum approvals for auto-merge |
    | `$MARKDOWN_APPROVALS` | 1 | Approvals for markdown-only PRs |
    | `$RETRO_LOG` | (none) | Path to retrospective log file |
    | Bot reviewer rules | (none) | Per-bot thread resolution patterns |
    | CI patterns | (none) | Known flaky checks, pre-existing failures |
    
    If no Marathon Configuration section exists, **advise the user to set one up — this is non-blocking; emit the notice and proceed with defaults** (do not wait for an answer):
    ```
    No Marathon Configuration found in this project's CLAUDE.md.
    
    For best results, add a ## Marathon Configuration section to your project's CLAUDE.md.
    Run `/tm-marathon-config-example` to see the configuration template (it covers both /tm and /issues), then copy and customize it for your project.
    
    Proceeding with defaults: base branch=main, 1 approval, no bot reviewer rules.
    ```
    
    Defaults apply for non-marathon use (single task mode, planning mode) without prompting. The template below uses `$BASE_BRANCH` where previous versions hardcoded `develop`.
    
    ## Execution Modes
    
    The steps below are written for **team mode** — the lead chairs an Agent Team, spawns one ephemeral teammate per unit, and coordinates via `SendMessage` and `shutdown_request`. Phase 0's `$TEAMS_AVAILABLE` selects the mode:
    
    | Mode | When | How the body maps |
    |------|------|-------------------|
    | **Team** | `$TEAMS_AVAILABLE` true | Run the body as written: spawn one background teammate (`Agent` with `run_in_background`) per unit/combined group into the session's single implicit team, message-driven monitoring. |
    | **Phased sub-agent** | `$TEAMS_AVAILABLE` false | No persistent team and no `SendMessage`. The lead runs each wave as a batch of parallel subagents, reads their returned transcripts in place of messages, and drives the same loop. See [Subagent Fallback](#subagent-fallback-no-teams). |
    
    Everything else — the DAG analysis, hot-file combining, tracking file, smart-merge, crash recovery, and retrospective — is identical across modes; only the teammate-coordination mechanism differs. Where a step is team-only (the `SendMessage` events, early-shutdown, and idle-ping handling), the phased fallback simply has no equivalent: subagents return rather than message.
    
    **One team per session.** This build allows exactly one implicit team per Claude Code session, and the main session is its permanent lead. A team-mode marathon claims that single team - so do not start another team-mode skill (a second marathon, a `/huddle`) in the *same* session: its teammates would join this marathon's team and share one task list and mailbox. To run two team-mode workstreams at once (two PRDs in flight, or a huddle defining the next PRD while this marathon implements the current one), use a *separate* session - a second terminal with its own worktree. Each session gets its own isolated team (`session-<id>`-named), lead, task list, and mailbox. (Cross-session, the only shared state to watch is Task Master's global tag selection: pass `--tag` on every call or use the MCP tools so two concurrent marathons don't stomp each other's active tag.)
    
    ## Entry Gate (non-removable)
    
    Before decomposing the run (Step 1), the acceptance contract must be frozen. Invoke the start gate first:
    
    The contract scripts live in the plugin package (`${CLAUDE_PLUGIN_ROOT}/scripts/contract/`), while the contract artifacts (contract, kill test, completion record) live in the target repository's `.taskmaster/contract/`, the scripts' default `--contract-dir`. When `CLAUDE_PLUGIN_ROOT` is unset (a hand-placed checkout rather than an installed plugin) the guard line before each invocation falls back to the current checkout.
    
    ```bash
    : "${CLAUDE_PLUGIN_ROOT:=.}"   # unset outside an installed plugin: fall back to the current checkout
    python "${CLAUDE_PLUGIN_ROOT}/scripts/contract/start_gate.py" <run-id>   # run-id = the Task Master tag (/tm) or issue-queue slug (/issues)
    ```
    
    `start_gate.py` fails closed (non-zero) unless the run has freeze evidence (a contract frozen before decomposition per `FLOOR.md` clause ii) or an `operator_signoff`-recorded signed skip. A signed skip is loud, human-authorized, and permanently caps the run at `UNVERIFIED` — it can never certify `PASS`. Do not run Step 1 or spawn any teammate until this gate exits zero. Part of the constitutional floor (`FLOOR.md`); the retro may propose changes to this step but never self-apply them.
    
    ## Step 1: DAG + Hot-File Analysis
    
    **CRITICAL: Global Source-of-Truth Write Rule**
    Never run source-of-truth write commands as parallel background jobs — concurrent writes race. Each such command may internally switch global state, and concurrent invocations can silently land work on wrong targets. Always run source-of-truth write commands **sequentially inline** — 10 concurrent background `add-task` calls once landed tasks on the wrong tags.
    
    Enumerate work units via the adapter's **enumerate** operation.
    
    **Verification units.** An adapter may mark some enumerated units as a `verification unit` (a decomposed parent whose children are the real work units, e.g. a GitHub issue with sub-issues). Never spawn an implementing teammate for a verification unit and never place it in a wave. Its children enter the run only through the adapter's own enumerate filter, never by expansion from the parent. It becomes eligible only when its last child merges, a state the adapter defines; check it at enumerate time too, so a parent whose children all merged before this run is verified before Wave 1 rather than never. The lead then owns the parent's acceptance check, a lead-run step over the merged children outside the custody chokepoint. Per Lead Authority the lead does not execute it inline: it spawns a read-only subagent for the check, keeps the merge loop moving, and acts on the returned result. The check does not go through `spawn_verifier.py`, adds no freeze, and does not replace the run-level contract verification gate in Completion. The adapter's rule decides how the parent closes.
    
    **Analyze dependency tree for maximum concurrency:**
    
    1. Map the dependency tree — which tasks block which?
       - An open-PR blocker (a unit that must wait for an already-open pull request) is treated per the adapter's recorded answer. Where the source cannot express it as a native edge (GitHub: `PR-as-blocker` is unsupported), the adapter reports it as run-plan sequencing: treat it as an unsatisfied dependency and hold the unit out of every wave until that PR merges.
    2. Identify the critical path (longest sequential chain)
    3. **Challenge unnecessary dependencies** — different files/modules may not need sequencing
    4. Look for tasks chained sequentially that could run in parallel
    5. **Identify hot files** — files touched by multiple tasks. Record as `$HOT_FILES`.
    6. **Primary mitigation: combine tasks that share hot files** into one teammate. Combined units share one branch and worktree, so there is no inter-unit merge and the conflict class is eliminated entirely. Combine when:
       - Tasks share hot files (strongest signal — prefer combining over dependency management for *small, coupled* tasks)
       - Tightly coupled output (e.g., "add resources" + "add docs for resources")
       - Content-only tasks touching non-overlapping directories (e.g., adding 3 independent pattern dirs)
       - One is docs/config for the other, or one is meaningless without the other
       - Small tasks (complexity 1-2) that share a theme — PR-per-task overhead exceeds the work itself
    
       **Combining has a ceiling — it must not swallow the parallelism it exists to protect.** Combining buys zero conflicts by trading away concurrency, so it only pays while the combined unit stays small and genuinely coupled. A hot file is a combine *candidate*, not a combine *mandate*. Do NOT combine when it would:
       - push the combined unit past ~complexity 8 — one teammate then serially implements a large PR, which is slower than parallel teammates each resolving an additive conflict;
       - collapse the wave — if combining would leave fewer than 2 parallel units where the DAG allowed more, you have destroyed the wave, not optimized it; use dependencies instead;
       - fold in a task that *depends on* the others, or a complexity-8+ task — a dependency is a sequencing signal, not a combine signal. Sequence it across PRs; don't serialize it inside one.
    
       Some hot files are touched by *every* PR and want **sequential merge, never combining**: a version counter (`.claude-plugin/plugin.json` `.version`), a changelog, a lockfile. Assign each teammate its target value explicitly at spawn and merge in order (highest version wins) — combining all PRs to dodge a one-line version conflict is the trap, not the fix.
    
       **Version values assigned at spawn are final — never re-message a new version to an in-flight teammate** (it races with PR_CREATED/REVIEW_CLEAR and produces crossed-message churn). If readiness order ends up differing from the planned merge order, that is handled at merge time, not by re-messaging — see [Smart Merge](#smart-merge).
    
       **One caveat overrides "identical bumps merge cleanly":** if the repo auto-publishes an *immutable per-version artifact* on a version *change* (e.g. a `standalone-skills-v<version>` build that fires on the `plugin.json` bump), identical bumps across parallel PRs silently break it — the first merge fires the build from an incomplete tree and permanently consumes that version's tag, and the later identical bumps don't change the version so the build never re-fires. There, do **not** use identical bumps: have the **last-merging PR bump one step higher** (or bump once at the very end, after all merges) so the complete tree republishes.
    7. **Fallback: dependencies** — when combining isn't feasible or would breach the ceiling above (any task complexity 8+, a real dependency between the tasks, fundamentally different concerns despite a shared file, or 5+ tasks on one file):
       - **Add explicit dependencies** — merge the simpler/faster task first, then the other depends on it.
       - Teammate prompts: include conflict resolution patterns
       - If 5+ tasks touch one file, decide by the *kind* of contention. This additive-vs-serialize split is a **5+-on-one-file rule and does not override Step 1.6 below that threshold**: a small coupled pair (2-4 tasks) sharing one additive hot file under the complexity ceiling still **combines** — combining is the primary mechanic, it yields 0 conflicts, and it costs only one parallel slot. At 5+ the arithmetic flips: **purely additive** edits (schema appends, barrel exports, route registration — the "accept both sides" cases) are cheap to merge, so keep the units **parallel** in one wave and merge them in order rather than collapsing four-plus parallel slots into one teammate; do NOT serialize them. Reserve the **dedicated consolidation task** (one teammate owns that file; the others depend on it) for **same-line or structural** contention where parallel edits would genuinely conflict — and even then, prefer it over folding all 5+ into one mega-PR.
    8. Report the optimized plan:
       ```
       ## Dependency Analysis: <tag>
    
       Critical path: <task-ids> (<N> points sequential)
       Parallel capacity: <M> tasks in first wave
    
       Combined tasks:
       - Tasks <X>+<Y>: <reason> (single teammate, single PR)
    
       Hot files:
       - <file-path>: tasks <ids> (pattern: <e.g., "accept both sides">)
    
       Optimizations:
       - Removed dependency <X> → <Y>: different modules
    
       Staleness outcomes:
       - Unit <X> <-> open PR <M>: <combine | sequence after the PR | kicked back> (<shared files>)
    
       Verification units:
       - <parent>: children <ids> (<merged>/<total> merged; checked after <last child>, or skipped: <reason>)
       ```
    9. Apply dependency changes via the work source's dependency-update mechanism.
    
    ## Step 2: Team + Tracking
    
    This build uses a **single implicit team**: the team forms as you spawn named background teammates (next) - each `Agent(name: "task-<id>", run_in_background: true)` joins the session's implicit team and is addressable via `SendMessage(to: "task-<id>")`. Proceed straight to tracking.
    
    **PR tracking** — persisted in worktree dir (survives crashes and team cleanup):
    
    ```bash
    TRACK_FILE=~/dev/github.com/<org>/<repo>/worktree/<tag>/pr-tracking.json
    mkdir -p "$(dirname "$TRACK_FILE")"
    
    if [ -f "$TRACK_FILE" ]; then
      echo "EXISTING_TRACKING: reconciling against source of truth and GitHub"
    else
      echo '{"meta":{"tag":"<tag>","wave":1,"repo":"<owner>/<repo>","flaky_checks":[]},"tasks":{}}' | jq . > "$TRACK_FILE"
    fi
    ```
    
    **Reconciliation** (run at start if tracking file exists):
    1. Read unit status via the adapter's enumerate operation.
    2. Cross-reference each tracking entry:
       - Source `done` but tracking `working` → merged externally. Remove from tracking.
       - Source `in-progress` but PR merged on GitHub → mark unit done, remove from tracking.
       - Source `pending` but tracking has PR → stale entry. Remove, check cleanup needed.
       - Source `in-progress` and PR open → valid. Keep, update `last_ci`.
    3. Source `in-progress` but NOT in tracking → check GitHub for open PR. If found, add to tracking. If not, reset unit to `pending`.
    4. Write reconciled file.
    
    **Tracking structure:**
    ```json
    {
      "meta": {"tag": "<tag>", "wave": 1, "repo": "<owner>/<repo>", "flaky_checks": ["E2E"]},
      "tasks": {
        "task-<id>": {
          "pr": 123,
          "status": "working|review_clear|merged",
          "model": "sonnet|opus",
          "wave": 1,
          "last_ci": "passing|failing|unstable|pending"
        }
      }
    }
    ```
    
    **CRUD operations:**
    ```bash
    # Add/update task
    jq --arg task "task-<id>" --argjson pr <number> --arg model "sonnet" --argjson wave 1 \
      '.tasks[$task] = {"pr": $pr, "status": "working", "model": $model, "wave": $wave, "last_ci": "pending"}' \
      "$TRACK_FILE" > "$TRACK_FILE.tmp" && mv "$TRACK_FILE.tmp" "$TRACK_FILE"
    
    # Update CI status
    jq --arg task "task-<id>" --arg ci "passing" \
      '.tasks[$task].last_ci = $ci' "$TRACK_FILE" > "$TRACK_FILE.tmp" && mv "$TRACK_FILE.tmp" "$TRACK_FILE"
    
    # Read all
    jq -r '.tasks | to_entries[] | "\(.key) → PR #\(.value.pr) (\(.value.status), CI: \(.value.last_ci), wave \(.value.wave))"' "$TRACK_FILE"
    
    # Remove after merge+cleanup
    jq --arg task "task-<id>" '.tasks |= del(.[$task])' "$TRACK_FILE" > "$TRACK_FILE.tmp" \
      && mv "$TRACK_FILE.tmp" "$TRACK_FILE"
    ```
    
    **Identify known-flaky checks at marathon start:**
    ```bash
    gh api repos/<owner>/<repo>/branches/$BASE_BRANCH/protection \
      --jq '.required_status_checks.contexts // []'
    ```
    Store non-required check names in `meta.flaky_checks`.
    
    ## Step 3: Spawn Teammates
    
    **Pre-spawn: Read the retro log's open template changes:**
    Before writing any spawn prompt, read the retro log (Marathon Configuration `$RETRO_LOG`) Template Changes table — **skip this step entirely if `$RETRO_LOG` is unset** (defaults supply none), the same escape the completion-time read uses. Apply every row still marked Pending to this run's spawn prompts and lead behaviour now - that is what the table is for. Reading these only at retro time is too late — the same friction then recurs the whole run, which is exactly how past fixes sat unapplied across entire marathons before shipping.
    
    **Pre-spawn: Check for already-completed work:**
    Before spawning Wave 1, check recent merged PRs for task keywords to avoid spawning work that's already done:
    ```bash
    gh pr list --state merged --limit 20 --json title,mergedAt,headRefName \
      | jq '.[] | select(.headRefName | test("<tag>")) | {title, mergedAt, headRefName}'
    ```
    Cross-reference with pending tasks. If a task's work was already merged (e.g., from a prior crashed marathon), mark it done and skip spawning.
    
    **Model selection:**
    - **Opus** (default for reliability): Multi-file PRs, review-heavy tasks, tasks touching shared files (barrel exports, routing, config), complexity 5+
    - **Sonnet** (cost-efficient for simple work): Single-file changes, isolated modules, complexity 1-4 with no shared-file risk, docs/config-only tasks
    
    Sonnet is cost-effective but has a recurring false REVIEW_CLEAR problem — reports review-clear without verifying all criteria. Opus has not shown this. When in doubt, use opus — the cost delta is cheaper than intervention time.
    
    Haiku cannot reliably handle review loops — never use for teammates.
    
    **Combined-group identity:** combining is the primary mechanic, so a teammate often covers several units. Give a combined group one identity derived from its member ids: name `task-<id>-<id>` (e.g. `task-1-2`) — the Agent `name` regex allows only letters, digits, `_`, and `-`, so a `+` in the name is rejected at spawn; branch `<tag>--<id>+<id>--<slug>` and worktree `worktree/<tag>/<id>+<id>--<slug>` may keep `+` (git accepts it in refs and paths). Its complexity is the sum of its members'. The Scope guard and the activity-check `find` path below operate on this combined branch/worktree — substitute the combined id wherever the singular `<task-id>` appears. Mark each member unit in-progress and close each on merge.
    
    **Teammate prompt template:**
    ```
    Agent(
      subagent_type: "general-purpose",
      run_in_background: true,
      name: "task-<task-id>",   # combined group: task-<id>-<id> — no '+' in agent names (see Combined-group identity above)
      model: "<chosen-model>",
      prompt: """
    # Implement <tag>.<task-id>: <task-title>
    
    ## Setup
    Set the unit in-progress via the adapter; create the worktree using the adapter's branch/worktree convention.
    
    ## Requirements
    <work unit requirements and subtasks — fetched via the adapter's enumerate operation for this unit id>
    
    ## Architectural Direction
    <Include architectural guidance, design decisions, or constraints from the lead HERE in the
    first message. Teammates may lose context between messages.>
    
    ## Project Guidelines
    <Include relevant sections from the repo's CLAUDE.md - testing patterns, coding standards.>
    
    ## Shell Rules
    Always pipe `gh` output to `jq` (never use `gh --jq` with complex filters). Use positive jq filters (`== "FAILURE"` not `!= "SUCCESS"`) - zsh mangles `!=`.
    
    ## Known Conflict Patterns
    <If $HOT_FILES identified, include here. Otherwise omit.>
    Additive files (imports, barrel exports, routes): accept both sides. Same-line conflicts or complex JSX blocks: escalate immediately with file, line range, and both versions.
    **Do NOT resolve the `.claude-plugin/plugin.json` version-line conflict yourself** — the version hot-file is lead-owned end to end. If your PR goes DIRTY *only* on the version line because the base advanced, leave it; the lead resolves it on sight. Touching it races the lead and can strand a half-resolved conflict.
    
    ## Workflow
    1. **Implement using TDD**. Push commits incrementally for backup. If your change touched a module that has a documented co-change partner — a sibling doc or a seam-map README named in your Project Guidelines — update it in the same PR. A code change without its paired doc is a lying map and a predictable review thread; updating it now is cheaper than a follow-up cycle after REVIEW_CLEAR.
    2. **Before creating PR**, check for existing: `gh pr list --head "<branch-name>" --state all --json number,state,mergedAt`
       - Merged → message lead, wait idle. Open → use it. None → create one.
    3. **Get required checks green, then stand down.** Use the pr-review-merge skill's criteria and thread rules to fix any failing *required* checks and resolve any bot threads already posted, pushing fixes. Then report and go idle. **Do NOT run a `gh pr checks --watch` loop or any background CI watcher** - in marathon mode the lead owns CI watching, the slow `claude-review`/AI-review wait, and the merge. A teammate that watches a slow advisory check sits idle for minutes and floods the lead with idle notifications; that is the lead's job here, not yours. While your PR is not yet at required-green the lead may message you to fix a failing check or thread - respond and push. Once you send REVIEW_CLEAR you are done: the lead does not re-wake you, it spawns a fresh teammate if more work surfaces (one task, one teammate).
    
    ## Communication
    Only message the lead for **meaningful events**. Send the matching JSON payload from [Teammate Event Payloads](#teammate-event-payloads) as the message `content`, **prefixed with the event name on the same line** (`PR_CREATED {...}`) - the harness parses a bare JSON-object body against its shutdown/plan-approval protocol union and rejects any other shape, so a bare `JSON.stringify(...)` body never sends. The `event` field self-identifies it; the `summary` stays human-readable:
    - PR created: `SendMessage(type: "message", recipient: "lead", content: "PR_CREATED " + JSON.stringify({event: "PR_CREATED", task_id: "<tag>.<task-id>", pr_number: <number>, branch: "<branch>"}), summary: "PR created <task-id>")`
    - Review clear: `SendMessage(type: "message", recipient: "lead", content: "REVIEW_CLEAR " + JSON.stringify({event: "REVIEW_CLEAR", task_id: "<tag>.<task-id>", pr_number: <number>, required_checks_green: true, threads_resolved: true}), summary: "Review clear <task-id> — standing down (lead owns claude-review wait + merge)")`
    - Blocked: `SendMessage(type: "message", recipient: "lead", content: "BLOCKED " + JSON.stringify({event: "BLOCKED", task_id: "<tag>.<task-id>", pr_number: <number>, blocking_reason: "<reason>", blocking_category: "merge_conflict|ci_failure|dependency|external"}), summary: "Blocked <task-id>")`
    - Too complex: `SendMessage(type: "message", recipient: "lead", content: "TOO_COMPLEX " + JSON.stringify({event: "TOO_COMPLEX", task_id: "<tag>.<task-id>", complexity_reason: "<reason>", suggested_decomposition: ["<subtask>", "<subtask>"]}), summary: "Too complex <task-id>")`
    - Clarification needed: `SendMessage(type: "message", recipient: "lead", content: "CLARIFICATION_NEEDED " + JSON.stringify({event: "CLARIFICATION_NEEDED", task_id: "<tag>.<task-id>", question: "<question>", context: "<context>"}), summary: "Clarification <task-id>")`
    
    `REVIEW_CLEAR` reports shape, not a verdict the lead trusts blindly — set `required_checks_green`/`threads_resolved` only when genuinely true, but expect the lead to re-verify both via the GitHub API before merging.
    
    ## Teammate Event Payloads
    Each event is a JSON object whose `event` field names the type. Required fields per type (omit unknown values rather than inventing them):
    ```json
    // PR_CREATED — a PR now exists for this task
    { "event": "PR_CREATED", "task_id": "<tag>.<task-id>", "pr_number": 123, "branch": "<branch-name>" }
    
    // REVIEW_CLEAR — required checks green and posted threads resolved; standing down
    { "event": "REVIEW_CLEAR", "task_id": "<tag>.<task-id>", "pr_number": 123, "required_checks_green": true, "threads_resolved": true }
    
    // BLOCKED — cannot progress without intervention
    { "event": "BLOCKED", "task_id": "<tag>.<task-id>", "pr_number": 123, "blocking_reason": "<what is blocking>", "blocking_category": "merge_conflict|ci_failure|dependency|external" }
    
    // TOO_COMPLEX — task is too large to land as one PR
    { "event": "TOO_COMPLEX", "task_id": "<tag>.<task-id>", "complexity_reason": "<why>", "suggested_decomposition": ["<subtask>", "<subtask>"] }
    
    // CLARIFICATION_NEEDED — requirements ambiguous, need a decision
    { "event": "CLARIFICATION_NEEDED", "task_id": "<tag>.<task-id>", "question": "<the question>", "context": "<relevant context>" }
    ```
    `pr_number` is omitted on `TOO_COMPLEX`/`CLARIFICATION_NEEDED` (no PR yet) and on `BLOCKED` if the block predates the PR.
    
    ## Scope
    - Only create PRs on YOUR branch (`<tag>--<task-id>--<slug>`, or the combined-group branch `<tag>--<id>+<id>--<slug>` if you cover several units). Never create PRs on other branches or for work outside your assigned task(s).
    - If you discover related work that needs doing, mention it in your PR description — don't create additional PRs.
    
    ## Lifecycle
    1. Implement → push incrementally → create PR → message lead PR_CREATED
    2. Fix any failing **required** checks and any already-posted bot threads; push. Do NOT watch CI - the lead owns that.
    3. Message lead REVIEW_CLEAR once `pr-review-merge` ready criteria 1-5 hold on your head (required checks green, threads resolved) and stand down. Criterion 6 (every bot flagged `Re-reviews on push` has reviewed the head SHA, or its `Max wait for re-review` expired) is the lead's to apply at merge time in marathon mode - do not sit through a flagged bot's review window, and do not run a CI watch loop for it. The lead re-verifies criterion 6 on whatever head it merges, and any push after REVIEW_CLEAR re-opens that check.
    4. The lead owns the claude-review wait + merge, cleans up, and shuts you down at green. After REVIEW_CLEAR you are not re-woken - if more work surfaces the lead spawns a fresh teammate (one task, one teammate). **Approve the lead's `shutdown_request` promptly when it arrives, and after REVIEW_CLEAR do NOT idle-ping or re-send merge-readiness nudges** — the lead owns the merge; re-nudging an already-cleared PR just churns the lead while it holds the claude-review wait.
    """
    )
    ```
    
    Spawn all independent teammates in a single message (parallel Task calls).
    
    ## Step 4: Lead Monitoring
    
    Report team status after spawning:
    ```
    ## Marathon Started: <tag>
    
    | Task | Teammate | Model | Status |
    |------|----------|-------|--------|
    | <task-id> - <title> | task-<task-id> | sonnet | Spawned |
    ```
    
    #### Teammate Messages (reactive)
    
    Read the `event` field of the message's JSON payload (see [Teammate Event Payloads](#teammate-event-payloads)) and key the action off the listed fields. **Backward compatibility**: a teammate mid-transition may still send a prose message (`PR_CREATED: PR #123 for ...`); fall back to matching the leading `EVENT:` token and parse `#<n>`/`<tag>.<id>` from the text. Both forms drive the same action — the schema standardises shape, not the action taken.
    
    | `event` | Fields used | Lead Action |
    |---------|-------------|-------------|
    | PR_CREATED | `task_id`, `pr_number`, `branch` | Update tracking with `pr_number`, report to user, and **start owning the CI watch for this PR** (the teammate does not watch it) |
    | REVIEW_CLEAR | `task_id`, `pr_number`, `required_checks_green`, `threads_resolved` | **Re-verify `required_checks_green` and `threads_resolved` via the GitHub API — never act on the reported booleans alone (polling-over-trust; the schema standardises shape, not honesty).** Once verified, **shut the teammate down immediately** (don't leave it idle through the claude-review wait), hold for claude-review per the AI-docs rule, and run [Smart Merge](#smart-merge) |
    | CLARIFICATION_NEEDED | `task_id`, `question`, `context` | Answer from task context, or relay to user if genuinely ambiguous |
    | BLOCKED | `task_id`, `pr_number`, `blocking_reason`, `blocking_category` | `blocking_category: "merge_conflict"` → push back "resolve conflicts yourself" (or lead-resolve if the teammate is idle/down); any other category → report `blocking_reason` to user, ask for guidance |
    | TOO_COMPLEX | `task_id`, `complexity_reason`, `suggested_decomposition` | Shutdown teammate, decompose task (seed subtasks from `suggested_decomposition`), spawn fresh |
    
    **On TOO_COMPLEX:**
    1. Shutdown teammate, clean up failed worktree/branch
    2. Decompose: expand the task into subtasks (or cancel + create new peer tasks for sibling split)
    3. Spawn fresh teammates for resulting tasks
    
    **Shut teammates down early to kill idle churn**: The moment a teammate's PR has required checks green and threads resolved (its REVIEW_CLEAR, or your own poll showing it), shut the teammate down - do not leave it idle through the claude-review wait and the merge. The lead owns that tail. A live-but-idle teammate emits a continuous stream of idle notifications (the harness re-pings idle members), which is pure attention-drain on the lead; early shutdown is the fix, not patience. This is also why teammates are told not to run their own CI watcher - the lead watches, the lead merges, the teammate is gone before the slow advisory checks finish. If you have sent a `shutdown_request` and the teammate keeps emitting idle notifications without approving it, do not re-send the protocol request: send ONE plain-text message that spells out the exact approval call - `SendMessage(to: "team-lead", message: {"type": "shutdown_response", "request_id": "<the queued request_id>", "approve": true})` - which unwedges the queued protocol message every time (validated across two marathons, ~20/20; a "not reachable" reply means it already exited).
    
    **Lead conflict resolution**: When a teammate is idle and their PR is DIRTY (merge conflict), resolve it directly instead of nudging the teammate. Pull `$BASE_BRANCH`, resolve the conflict, push. Faster than round-tripping to an idle teammate (~10 min saved per conflict). This idle-DIRTY conflict is the **sole** work the lead executes directly — it does not generalize: a failing test, missing implementation, or thread fix is still delegated per [Lead Authority](#lead-authority), even when it looks like a quick edit. If the teammate whose branch you're resolving may still be live (not yet idle/down), message it *before* you push — "leave the version conflict to me, I'm resolving" — then push; pushing first races with the teammate resolving the same conflict in its own worktree.
    
    **Version-only base-advance DIRTY: lead-resolve on sight, don't message first.** The exception to the message-before-push rule is a PR that is DIRTY *only* on the `.claude-plugin/plugin.json` version line because the base advanced. Teammates are told never to touch the version hot-file (see the teammate template), so there is no one to race — resolve it directly the moment it appears (merge the base in, set `.version` to the highest of the conflicting values, push) rather than round-tripping to an idle teammate. Highest version always wins.
    
    **Commit the resolution from the worktree cwd, not a `*-main` dir.** The `worktree-guard` PreToolUse hook blocks any command containing a `git commit` line whenever the shell's cwd is a `*-main` directory — and the lead's `gh` polling naturally leaves cwd there. So `cd` into the teammate's worktree as part of the resolve command (or in a prior command, since cwd persists), or the commit is rejected even though it targets the worktree, not `*-main`.
    
    **Check `mergeStateStatus` before watching CI on a late-wave PR.** A PR branched before its siblings merged can be DIRTY against an advanced `$BASE_BRANCH`; GitHub computes no merge ref for a DIRTY PR, so the test workflow never registers and a CI watcher waits forever for checks that cannot appear. On PR_CREATED for any PR that may sit behind already-merged siblings, check `mergeStateStatus` first and resolve DIRTY before starting the watch.
    
    **Non-responsive teammate escalation**: If a teammate ignores a direct instruction (nudge to fix CI, address review feedback, follow lead guidance) or sends a false REVIEW_CLEAR (claims ready but criteria aren't met), don't keep nudging. Shut it down and respawn the same task on opus. One strike — don't give sonnet a second chance on the same task.
    
    **Idle teammate ≠ dead teammate**: Before killing an idle teammate, check if their worktree has subagent activity. Subagents run as child processes and cause idle notifications on the parent. Check for recent file modifications or running processes in the worktree before assuming the teammate is stalled:
    ```bash
    # Check for recent activity in teammate's worktree (files modified in last 5 min)
    find ~/dev/github.com/<org>/<repo>/worktree/<tag>/<task-id>--<slug> -mmin -5 -type f | head -3
    ```
    
    **Unreliable REVIEW_CLEAR**: Don't rely solely on messages. **Proactively poll ALL tracked PRs** on two triggers:
    1. When any teammate goes idle or sends a message
    2. **Periodically** — every ~30 minutes during long marathons to catch stalled teammates early
    
    ```bash
    for PR in $(jq -r '.tasks | to_entries[] | select(.value.status == "working") | .value.pr' "$TRACK_FILE"); do
      THREADS=$(gh api graphql -f query="query { repository(owner: \"<owner>\", name: \"<repo>\") {
        pullRequest(number: $PR) { reviewThreads(first: 100) { nodes { isResolved } } }
      }}" | jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length')
      MERGE=$(gh pr view $PR --json mergeStateStatus,mergedAt,statusCheckRollup \
        | jq '{mergeStateStatus, mergedAt, checks: [.statusCheckRollup[] | select(.conclusion == "FAILURE" or .conclusion == "CANCELLED")] | length}')
      echo "PR #$PR: threads=$THREADS merge=$MERGE"
    done
    ```
    If green with 0 unresolved threads, run smart-merge regardless of teammate message. If stalled (teammate idle but PR not green), message teammate to continue.
    
    **Lead overlap rule:** Never block on a single PR's CI. While waiting for CI on one PR, process other actionable items: teammate shutdowns, tracking updates, next-wave setup, cleanup of merged PRs, spawning newly unblocked tasks. The lead loop is event-driven, not sequential.
    
    **Accidental input guard:** Empty messages, single characters, or auto-suggested prompt text → brief status summary only, no expensive operations.
    
    ## Smart Merge
    
    The lead runs smart-merge via the pr-review-merge skill (Smart Merge section): dismiss stale
    bot CRs, verify the five auto-merge criteria, handle UNSTABLE/UNKNOWN, merge in hot-file order.
    On a solo-maintainer repo (0 required approvals) merge with `gh pr merge $PR --squash --delete-branch --admin`
    once the *required* checks are green — a plain merge gets bounced when a non-required check (CodeRabbit,
    an advisory AI review, a regression gate that re-runs on base advance) is mid-run at the merge instant.
    After a merge, close the unit via the adapter's **close on merge** operation, then proceed to
    the wave transition below.
    
    **Version-counter conflicts resolve here, never by re-messaging.** Each teammate's target version
    was fixed at spawn (Step 1). When a PR becomes ready out of the planned merge order — usually because
    an externally-merged PR advanced the base mid-run — either hold the lower-version PR until its
    predecessor merges, or lead-resolve the `plugin.json` conflict to the highest version (per
    [Lead conflict resolution](#step-4-lead-monitoring)). Re-messaging a new version to an in-flight
    teammate races with its own PR_CREATED/REVIEW_CLEAR — don't.
    
    **Teammate shutdown and cleanup are two separate stages — don't conflate them.** The teammate is shut
    down *early*, at REVIEW_CLEAR (see [Step 4](#step-4-lead-monitoring)) — the moment its PR is required-green
    with threads resolved, never held live through the claude-review wait or the merge. *Cleanup* (worktree
    removal, branch deletion, unit-close) is the *later* stage and is the only thing the verified-MERGED gate
    guards. By the time you merge, the teammate is already gone; the post-merge step below is a confirm-pane-dead
    check, not a second shutdown.
    
    **Gate cleanup on a VERIFIED merge.** Worktree removal, branch deletion, and unit-close MUST run
    only after confirming `gh pr view $PR --json state --jq '.state' == "MERGED"`. Never chain them
    unconditionally after the merge call — a rejected merge with chained cleanup deletes the branch/worktree
    of a PR that never merged (recoverable via the remote branch, but it wastes a recovery cycle every time).
    
    **Don't merge an AI-authored docs/content PR while its AI reviewer is still pending.** This hold is
    `pr-review-merge` Ready Criterion 6 applied, not a separate rule: the lead holds the merge for every bot the
    Marathon Configuration flags `Re-reviews on push: yes` until it has reviewed the head SHA, bounded by that bot's
    `Max wait for re-review` (15m when absent). For a bot with `Re-review check name` its check run on the head SHA has
    four states: in progress, keep waiting until the max wait expires; completed with conclusion `success`, the
    criterion is satisfied and the `Commit:`-line spot check below applies; completed with conclusion `skipped`, the
    bot does not apply to this PR, so the criterion is satisfied with no warning; completed with any other conclusion
    (failure, cancelled, neutral, timed_out), a settled verdict that the bot did not complete a green pass, so
    take the warning path at once. On expiry or a settled run whose conclusion is neither `success` nor `skipped`, merge with a warning in the merge record
    naming the bot, the head SHA, the run's conclusion, and whether the reviews endpoint shows a review of that head
    SHA anyway; nothing holds forever on an advisory bot. The reason to flag an AI reviewer `Re-reviews on push: yes`:
    AI-written docs are exactly where AI-authoring residue (leaked tool-envelope tags, duplicated sections) hides,
    and the reviewer catches it. The minutes of waiting are cheaper than a follow-up PR + patch release.
    
    **A green `claude-review` check is evidence the reviewer completed, not that it reviewed the right head.** The workflow's final-status step now turns the check red when the action exits with `is_error: true`, an empty execution log, or a missing result entry (the silent-success failure seen on four PRs in 2026-09-04), which is why criterion 6 keys on the check run for this bot. Before merging on the strength of a review, confirm the reviewer's sticky summary comment cites the PR **head sha** in its `Commit:` line (or that `claude[bot]` resolved threads on that head). If the check is green but the summary still cites an older sha, the head was not reviewed: re-run the workflow once, and if it fails the same way, stand up a cold local reviewer per PR (a fresh agent with no authoring context, read-only, re-running the PR's measurements and verifying each open thread), post a comment disclosing that substitution, and resolve threads on that evidence. A red `claude-review` run, or any settled conclusion other than `success` or `skipped`, is not this case: it is criterion 6's warning-path state, which merges with a warning at once.
    
    **Any push after REVIEW_CLEAR re-opens the verify gate.** A lead conflict-resolution, a base-advance
    re-trigger, or a late fix all produce a new head, and bots re-review that new commit — a reviewer that
    passed clean on the prior head can post a fresh finding on this one. After any post-REVIEW_CLEAR push,
    re-check the required checks *and* unresolved threads on the new head before merging; never merge on the
    strength of the earlier REVIEW_CLEAR alone.
    
    **After merge:**
    1. Report to user
    2. Confirm the teammate is already down — you stood it down at REVIEW_CLEAR; this is a confirm-pane-dead check, not a second shutdown, and is **not** gated on the merge. The verified-`MERGED` gate below guards *cleanup* (step 3 onward), not the shutdown.
    3. Mark internal task completed
    4. Check for newly unblocked tasks. If this merge made a verification unit eligible (its last child merged, per the adapter's rule), spawn that parent's read-only acceptance-check subagent now, continue to the wave transition, and close or report the parent per the adapter when the result returns.
    5. **Wave transition**: Batch-dismiss stale CRs across all eligible PRs before spawning next wave. Review signals from completed wave, adapt next prompts with learnings.
    6. Check ready tasks via the adapter's enumerate operation filtered to `pending` status. Spawn fresh teammates for ready tasks.
    7. If all done → [Completion](#completion--retrospective). "All done" includes every eligible verification unit: wait for each outstanding parent acceptance-check subagent to return and for its parent to be closed or reported per the adapter before entering Completion.
    
    **If not merge-ready:**
    - BLOCKED → report to user, message teammate
    - DIRTY → message teammate: "resolve merge conflicts"
    - Human changes requested → report to user
    
    ## Crash Recovery
    
    If the session crashes, background teammates live only as long as the session. The team config directory (`~/.claude/teams/session-<id>/`, named from the session ID - **not** the tag) is removed automatically when the session exits, and the task-list directory (`~/.claude/tasks/session-<id>/`) is intentionally kept so a resumed session recovers its tasks. There is no per-tag team directory to force-remove - the state that actually carries a marathon across a crash is the worktree and its `pr-tracking.json`, so recovery is just reconciliation:
    
    ```bash
    # 1. Check worktrees for uncommitted work
    git worktree list | grep "<tag>"
    
    # 2. Re-invoke the calling command (e.g. /tm <tag> or /issues <label>) — reconciliation against the source of truth + GitHub handles stale tracking entries automatically.
    ```
    
    Worktrees and `pr-tracking.json` survive crashes in `worktree/<tag>/`. If a background teammate pane genuinely lingers after the session exits, end it at the terminal level (`tmux ls` → `tmux kill-session`), not by deleting `~/.claude/` state.
    
    ### Ephemeral Teammates
    
    One task, one session. The work source is the coordination brain.
    
    ```
    Spawn → Setup worktree → Implement → Create PR → required checks green + threads resolved → REVIEW_CLEAR
      → Lead: shut teammate down → Lead owns claude-review wait → smart-merge + cleanup
    ```
    
    Never reuse a teammate for a different task. Shutdown + spawn fresh.
    
    ### Lead Authority
    
    The lead operates as a **tech lead running a sprint** — not a task router.
    
    **The lead delegates, never executes.** During a marathon, the lead's job is to coordinate: merge PRs, spawn teammates, monitor status. Any work that takes more than ~30 seconds (tests, coverage, code exploration, implementation, thread resolution) must be delegated to a teammate or subagent. The one carve-out is resolving a DIRTY merge conflict for an *idle* teammate (see [Lead conflict resolution](#step-4-lead-monitoring)) — that, and nothing else, the lead does directly. The lead must stay responsive to teammate messages at all times. The moment the lead starts executing, messages queue up and momentum stalls.
    
    **Trusted decisions (no human approval needed):**
    - Defer or cancel tasks that become irrelevant
    - Combine related tasks into one PR
    - Create new/follow-up tasks in the work source
    - Fix minor style nits across PRs directly
    - Escalate model tier (shutdown + respawn on opus)
    - Limit concurrency to prevent merge conflicts
    - Reprioritize based on learnings
    
    **Always escalate to human:**
    - Architectural changes not in original task descriptions
    - Public API surface modifications
    - Deferring more than 30% of tasks
    - Ambiguous reviewer feedback
    
    **Report judgment calls in status updates.**
    
    ## Completion + Retrospective
    
    1. Send `shutdown_request` to each remaining teammate **once**. Each approves with a structured `shutdown_response` (addressed to `team-lead`, echoing the `request_id`, `approve: true`), which terminates it. Treat that approval, or an already-exited teammate, as the completion signal - don't block waiting on one that has already gone.
    2. Once every teammate has acknowledged shutdown or already exited, the team is gone - background teammates reap when the session exits. If a stale one lingers, verify its pane/process is dead. Nothing persists to block a future marathon.
    3. **Contract verification gate (non-removable)** — before the PRD-delivery check, run-complete requires a fresh non-implementing agent to execute the frozen acceptance contract against the assembled product. The frozen contract is required (its sha256 was recorded at freeze; the verifier re-hashes it and a mid-run edit aborts the run rather than certifying against a moved target). Spawn the cold verifier ONLY through the custody chokepoint — `python "${CLAUDE_PLUGIN_ROOT}/scripts/contract/spawn_verifier.py" <frozen-contract-path> <assembled-product-path>` (exactly two positional inputs; the run-id is derived from the contract filename, and the chokepoint mints the provenance token and writes the side-channel). Drive the verifier with the emitted prompt, record its per-criterion observed results into the completion record, then gate completion with `python "${CLAUDE_PLUGIN_ROOT}/scripts/contract/complete_gate.py" <run-id>`, which fails closed (non-zero) unless `${CLAUDE_PLUGIN_ROOT}/scripts/contract/validate_completion.py` accepts the record — no record, a record without verifier results, or a rejected record all block completion. Prefix these with the same `: "${CLAUDE_PLUGIN_ROOT:=.}"` guard line as the entry gate so an unset variable falls back to the current checkout instead of a missing absolute path. The run is not complete until this gate exits zero. Part of the constitutional floor (`FLOOR.md`); the retro may propose changes but never self-apply them.
    4. **PRD delivery check** — Re-read the original work units' acceptance criteria (PRD, issue bodies, or task details) and cross-reference against merged PRs. Report:
       - Criteria met (with PR evidence)
       - Criteria not met or partially met (flag for user)
       - Scope that was delivered beyond the original acceptance criteria (emergent work)
    5. Read the retro log (path from Marathon Configuration `$RETRO_LOG`, or skip if not configured)
    6. Run retrospective using the structured format below
    7. Append this marathon to the retro log (Marathon History + update Template Changes validation)
    
    ### Retro Boundary
    
    The retrospective's self-rewrite scope **excludes** the floor artifacts. The retro may **propose** changes to any of them but must never self-apply one (`FLOOR.md` clause iii): a self-rewriting process with no outcome signal can optimize away its own verification, and that capability is already proven. Excluded (all paths relative to the repo root):
    
    - `FLOOR.md`
    - the `<!-- floor:cold-verify-completion -->` markers in all four marked files (`skills/marathon/SKILL.md`, `skills/pr-review-merge/SKILL.md`, `commands/tm.md`, `commands/issues.md`) and the gate invocations alongside them
    - `.github/workflows/floor.yml`
    - `scripts/contract/`
    - `scripts/canaries/`
    - `tests/canaries/`
    - `tests/contract/`
    
    A retro proposal touching any of these is recorded for the maintainer's out-of-band sign-off, never enacted by the run itself.
    
    ```
    ## Marathon Complete: <tag>
    
    All <N> tasks done. <N> PRs merged.
    
    <PR table: Wave | PR | Tasks | Title | Merged time>
    
    ### PRD Delivery
    
    | Criterion | Status | Evidence |
    |-----------|--------|----------|
    | <success criterion from PRD> | Met / Partial / Not met | PR #<N>, ... |
    
    <If any not met, explain what's remaining and whether follow-up tasks are needed.>
    
    ### Retrospective
    
    **Template changes tested this run**
    <Check marathon-retros.md Template Changes table for "Pending" items.
    For each that was exercised: did it help, hurt, or not apply? Mark validated.>
    
    **What worked well**
    - <specific patterns, tools, or approaches — with evidence>
    
    **What didn't work — with waste estimate**
    - <friction point>: ~<N> min lost. Root cause: <X>
    - <friction point>: ~<N> min lost. Root cause: <X>
    
    **Lead decisions log**
    - <decision>: <alternatives considered> -> <chosen> because <reason>
    (task combinations, dependency changes, merge ordering, model upgrades, interventions)
    
    **Where does this learning belong?** (specific command / cross-cutting rule / README / don't capture)
    - <target file or section>: <concrete change with rationale>
    
    **What clause is no longer earning its place?**
    - <existing rule or block>: <reason it can be removed or demoted>
    
    **Stats**
    - Tasks: <N> completed, <N> cancelled/deferred
    - PRs: <N> merged, <N> avg review iterations
    - Teammates: <N> spawned (<N> sonnet, <N> opus), <N> needed intervention
    - Wall clock: ~<N> min spawn to final merge
    - Estimated waste: ~<N> min (CI waits, conflict resolution, stalled teammates)
    ```
    
    ## Subagent Fallback (no teams)
    
    When `$TEAMS_AVAILABLE` is `false`, run the same loop without a persistent team (see [Execution Modes](#execution-modes)). Per wave: enumerate the next-ready units via the adapter, spawn one parallel subagent per unit (or combined group), and wait for their returned transcripts in place of `SendMessage` events. Instruct each subagent to end its transcript with the same JSON payload it would otherwise `SendMessage` (the [Teammate Event Payloads](#teammate-event-payloads) schema — typically a `PR_CREATED` then a `REVIEW_CLEAR`), so the lead parses the `event` field and drives the [reactive table](#teammate-messages-reactive) identically in both modes. The lead still owns CI watching, smart-merge, and cleanup exactly as in team mode — `REVIEW_CLEAR` from a returned transcript is API-re-verified just as a `SendMessage` one is — and the tracking file (Step 2) carries cross-wave state in place of live teammates. There is no `shutdown_request` or idle-churn to manage — subagents return when done — so the early-shutdown and idle-ping guidance simply has no equivalent here.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related