xplan Planning System

workflow no always-loaded rules -- loads on demand updated 2026-08-02

Deep research + planning + execution framework. Spawns parallel research/review agents, creates comprehensive plans, and executes via parallel agent waves.

Tags

  • planning
  • execution
  • research
  • parallel

README

xplan

Interactive deep research + planning + execution framework for new projects. Interviews you upfront, researches deeply, proposes tech stack and scope for your sign-off, creates a parallelized execution plan, hardens it with constructive peer review and a sequence of adversarial reviews (6 independent reviews in the full configuration), and executes via parallel agents.

What This Module Does

xplan is a human-in-the-loop planning framework with mandatory confirmation gates throughout:

  • Phase 0 - Parse input, create plan directory
  • Phase 0.4 - Existing-repo analysis (only with --repo): a Source Freshness Guard fetches origin, pins the default-branch anchor SHA, and plans against a temp worktree at that anchor so a stale clone never poisons the plan. Skipped for greenfield.
  • Phase 0.5 - Discovery interview: confirm core concept, choose research depth
  • Phase 1 - Deep research via parallel agents (configurable preset: Full / Technical Only / Market & Product / Lite / Custom)
  • Phase 1.5 - Research review with business viability assessment; confirm to proceed
  • Phase 2 - Naming ideation (optional, with domain availability checks)
  • Phase 2.5 - Tech stack sign-off: propose stack, get approval
  • Phase 2.6 - Scope sign-off: approve epic structure and wave breakdown
  • Phase 2.7 - Multi-agent setup review
  • Phase 3 - Create parallelized plan with epics and dependency waves
  • Phase 4 - Constructive peer review by security, architecture, and business logic agents (stage 1 of 2)
  • Phase 5 - Write comprehensive plan.md (+ 5.6 self-review loop)
  • Phase 5.7 - Adversarial review sequence (stage 2 of 2): 3 sequential adrev-reviewer passes on Opus 4.8 (max effort). Each pass attacks the plan after the previous pass's fixes are incorporated (it uses the agent's native apply mode), so only one reviewer edits plan.md at a time and the third pass judges the fully-hardened plan. A finished plan has had 6 independent reviews in the full configuration (3 standard + 3 adversarial).
  • Phase 6 - Web review (default surface) + final confirmation gate before execution
  • Phase 7 - Create repo, issues, and spawn parallel agents per wave
  • Phase 8 - Verification, audit, retrospective, optional template generation

Three Modes

Mode Interview Research Tech Stack Scope Reviews Walkthrough
Default (interactive) Full Q&A Full Approved by user Approved by user Standard + adversarial sequence Skipped (approved inline)
--light Skipped Reduced (inferred) Internal default Internal Optional (no adversarial sequence) Full section-by-section at end
--autonomous (or /xplana) Skipped Full Internal (best-fit) Internal (best-fit) Full standard + adversarial sequence (always) Plan-as-artifact presentation at end

Reviews run in two stages: Phase 4 standard peer review against the draft, then the Phase 5.7 adversarial sequence (3 sequential passes) against the finished plan. A completed plan has survived 6 independent reviews in the full configuration (3 standard + 3 adversarial).

Autonomous-Execution Tenets

Every plan carries four requirements that let it execute with minimal human involvement and self-certify — written during planning (Phase 3), verified by the self-review (Phase 5.6), and enforced by the adversarial reviewer (Phase 5.7 / adrev-reviewer tenets T1–T4), which expands the plan when any is thin:

  1. Human work at the edges (T1) — human involvement is minimized (anything an agent can do via CLI/API is not human work) and what remains is bucketed to the start (front-loaded prerequisites) or the end (deferred steps), never mid-run. Once execution starts, it does not pause for a person.
  2. Follow-up-completion contract (T2, plan §9.5) — any follow-on work discovered during execution is tracked, triaged, and — if in-scope — completed before execution is reported complete. Only genuinely human-blocked work may remain open. Phase 7 gates completion on it.
  3. Autonomous decision context (T3, plan §1.4) — the plan carries the software's mission, the codebase's governing conventions, and its decision principles, so an execution agent directs unplanned follow-on work itself instead of stopping to ask. /etp reads this context (its Phase 1.5) to triage and reason.
  4. Comprehensive autonomous E2E testing (T4, plan §8) — every plan ships a full autonomous E2E suite over all testable surfaces, wired into CI as a blocking merge gate, so the suite (not the user) is the ready-to-merge oracle and no manual testing is required. New projects build it in from the ground up (a Wave-1 harness epic); existing repos get optimistic gap-fill for any touched area lacking coverage. The plan provisions whatever infra certainty needs (testing agents, RunPod, cloud Mac, real devices) — no resource constraint on testing. Phase 7 and /etp gate completion on a green suite.

Live-Testing Authorization (plan §8.6)

Live testing — app launches/relaunches, dictation firing, synthetic input events, focus changes, machine-global input/audio overrides, mic/camera capture, host-driven simulator or device runs — never runs on the dev machine, whose input surface is the user's control channel to every concurrent agent stream (live-testing-guard module).

Planning asks where it runs and whether the user approves it (Phase 0.5.2 Q8, deferred to Phase 3.3.5 in --light), and records the answer in plan §8.6 with the runner named and the grant dated. --autonomous cannot answer for the user: it writes NOT AUTHORIZED and surfaces the affected steps at the 6.A walkthrough and the 6.5 gate. Execution (/etp Phase 0.6, /xplan-resume §3.5, /xplan Phase 7.2) holds every live-testing step whose grant is missing, incomplete, or names a different machine, surfaces it by name, and asks — while the rest of the run continues. A plan step instructing a live test is never its own authorization.

--light: fast path. Reduced depth, minimal interaction. Skips Phases 0.5, 1.5, 2.5, 2.6, and 2.7 (Q8 is deferred to 3.3.5, not dropped). Traditional section-by-section walkthrough at the end.

--autonomous: deep path. Maximum depth, zero interruption until the final gate. Runs the full research pipeline (all 7 agents), full standard review (security + architecture + business logic), the self-review loop, and the Phase 5.7 adversarial review sequence (3 sequential adrev-reviewer passes on Opus 4.8 at max effort, each incorporating its fixes before the next; the third judges the fully-hardened plan). Tech stack, scope, naming, and multi-agent setup are inferred and documented in decisions.md. At Phase 6 the completed plan is presented as a single structured artifact with every inferred default called out, then the (non-bypassable) Phase 6.5 final execution gate fires. Pick this when you know exactly what you want to plan and prefer reviewing a finished artifact over answering questions during creation. Correct any wrong inferences with /xplan --deepen ~/code/plans/{concept-name} rather than re-running from scratch.

--light and --autonomous are mutually exclusive.

Web Review (Phase 6)

Phase 6's default review surface is a local browser UI served by stdlib http.server on 127.0.0.1. xplan renders plan.md with marked.js (CDN) and attaches a comment button to every ## and ### heading. The user can:

  • Submit for deepening — xplan reads the comments, runs a targeted Deepen Mode pass on each commented section, re-renders the patched plan for a second review round, then proceeds to the Phase 6.5 gate.
  • Accept as-is — proceed directly to the Phase 6.5 gate.

The web UI activates when plan.md exists and the environment is not headless. Fallbacks to the terminal walkthrough (6.A / 6.1-6.4) when:

  • XPLAN_NO_WEB=1 is set
  • No $DISPLAY on Linux
  • The server cannot bind a loopback port
  • The helper script ~/.claude/lib/xplan-web-review.py is missing

The Phase 6.5 final execution gate always fires afterward, web or not. The web UI is the review mechanism; 6.5 is the go/no-go.

Comments are persisted to ~/code/plans/{concept-name}/comments.json before the server shuts down — safe to close the tab or CTRL+C the script after clicking Submit.

Companion commands:

  • /xplana - Thin alias for /xplan --autonomous
  • /xplan-status - Check progress on a running or completed plan
  • /xplan-resume - Resume an interrupted plan execution from its last checkpoint
  • /etp - Execute ready work end-to-end — a plan file (xplan-authored or hand-written) or one-or-more investigated GitHub issues. Parallel implementation agents, full two-stage adversarial review of every PR by a separate agent, reasonable-and-valid fixes, follow-up completion, run-to-completion. Stops only for absolute blockers, which it reports while continuing all non-blocked work.

/etp vs /xplan-resume

Both execute work, but they are not interchangeable. /xplan-resume resumes an /xplan-native interrupted run using xplan's epic/wave checkpoint structure - it is xplan-specific. /etp is the general-purpose execution engine: point it at any plan file, a single issue (/etp #42), or a batch (/etp #42 #43) - it resolves the target into units, decomposes, executes, adversarially reviews, and completes, including the follow-up work that surfaces mid-run. Ceremony scales to the work: a single issue skips the wave/clone machinery a plan needs. /etp is resumable - a plan run checkpoints beside the plan, an issue/batch run reconciles against live GitHub state - so a re-invocation continues rather than restarts.

Files

File Type Description
commands/xplan.md command Main planning and execution command (/xplan)
commands/xplana.md command Autonomous alias - /xplana invokes /xplan --autonomous
commands/xplan-status.md command Plan progress dashboard (/xplan-status)
commands/xplan-resume.md command Resume interrupted execution (/xplan-resume)
commands/etp.md command Execute a plan or GitHub issue(s) end-to-end with adversarial PR review and follow-up completion (/etp)
lib/xplan-status-gather.sh lib Helper script that gathers plan progress data for /xplan-status
lib/xplan-web-review.py lib Local web server that renders plan.md in browser with section-level comment support for Phase 6 review

Dependencies

  • multi-agent: Required for parallel agent execution during research, review, and implementation phases
  • adversarial-review: Provides the adrev-reviewer agent that Phase 5.7's adversarial review sequence dispatches (3 sequential passes on Opus 4.8). Installed automatically as a module dependency.
  • lem-deepresearch (companion install): xplan's Phase 1 delegates research to the /deepresearch command, which is not part of CCGM - it lives in a standalone repo with its own installer

/deepresearch - required for research phase

xplan's research phase (Phase 1) spawns an agent that runs /deepresearch to produce a comprehensive research.md. Without it, xplan cannot complete its research step.

/deepresearch uses a fully local pipeline - Ollama (qwen2.5:72b) for query generation and fact extraction, SearXNG (self-hosted Docker) for web search - then Claude Code synthesizes the results. No external API keys required. It requires Docker, Ollama (~40GB model), and a Python venv, which the installer handles.

git clone https://github.com/lucasmccomb/lem-deepresearch.git
cd lem-deepresearch
./install.sh

See the lem-deepresearch README for manual setup, prerequisites, and troubleshooting.

Manual Installation

# Copy command files
mkdir -p ~/.claude/commands
cp commands/xplan.md ~/.claude/commands/xplan.md
cp commands/xplana.md ~/.claude/commands/xplana.md
cp commands/xplan-status.md ~/.claude/commands/xplan-status.md
cp commands/xplan-resume.md ~/.claude/commands/xplan-resume.md
cp commands/etp.md ~/.claude/commands/etp.md

# Copy lib files
mkdir -p ~/.claude/lib
cp lib/xplan-status-gather.sh ~/.claude/lib/xplan-status-gather.sh
cp lib/xplan-web-review.py ~/.claude/lib/xplan-web-review.py
chmod +x ~/.claude/lib/xplan-web-review.py

Plans Directory

xplan creates plan directories under ~/code/plans/. Create this directory if it does not exist:

mkdir -p ~/code/plans

Optional: Create a templates directory for reusable plan patterns:

mkdir -p ~/code/plans/_templates

After installation, invoke with:

  • /xplan <concept> - full interactive mode
  • /xplan <concept> --repo <existing-repo-path> - plan work against an existing repo
  • /xplan <concept> --light - fast path, minimal interaction
  • /xplan <concept> --autonomous or /xplana <concept> - full-depth pipeline with zero mid-flow prompts; completed plan presented at the end
  • /etp <plan-file-or-dir | #issue …> - execute a plan or investigated GitHub issue(s) end-to-end; /etp #42 #43 batches independent issues (--dry-run to preview, --confirm for a go/no-go gate, --max-agents N to cap parallelism, --light-review to opt out of Stage 2 on trivial diffs)

Will install

Path Action Target Type
commands/xplan.md commands/xplan.md command
commands/xplana.md commands/xplana.md command
commands/xplan-status.md commands/xplan-status.md command
lib/xplan-status-gather.sh lib/xplan-status-gather.sh lib
lib/xplan-web-review.py lib/xplan-web-review.py lib
commands/xplan-resume.md commands/xplan-resume.md command
commands/etp.md commands/etp.md command

Dependencies

Required by

No other module depends on this one.

Included in presets

Install this module

Agent prompt

Recommended for agent users -- hands the whole install off to your assistant.

Fetch https://cd23a9be.ccgm-site.pages.dev/modules/xplan.md and install this module into my Claude Code setup.

Native plugin marketplace

One command via the native plugin marketplace -- additive, does not merge settings.json.

claude plugin install xplan@ccgm

The marketplace path is additive, not a replacement: it installs commands, agents, and skills as native plugin components, but it does not perform the bash installer's deep settings.json merge, and it does not write the always-loaded global CLAUDE.md context. Rules are only injected via an opt-in SessionStart hook rather than being auto-loaded. Use the bash installer when those pieces matter to you.

Manual, per file

Full control -- copy exactly the files you want from the sections below.

Files

Files

1 further file is available as raw text.

command (5)

commands/xplan.md

---
description: Interactive deep research + planning + execution framework for new projects and features
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, Agent, AskUserQuestion, WebSearch, WebFetch
argument-hint: <project concept or idea> [--repo <existing-repo-path>] [--light | --autonomous] [--deepen [<plan-dir>]]
---

# xplan - Interactive Project Planning & Execution

A human-in-the-loop planning framework that interviews you upfront, deeply researches your concept, builds a contextual model, proposes tech stack and architecture for your sign-off, creates a parallelized execution plan, reviews it with specialized agents (constructive peer review + a 3-pass sequential adversarial review — 6 independent reviews in the full configuration), and then autonomously executes using parallel…

View raw (131370 bytes)

commands/xplana.md

---
description: Autonomous xplan - full-depth research + planning + reviews with zero mid-flow prompts. Presents the completed plan as a single artifact at the end.
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, Agent, AskUserQuestion, WebSearch, WebFetch
argument-hint: <project concept or idea> [--repo <existing-repo-path>] [--deepen [<plan-dir>]]
---

# xplana - Autonomous xplan

Thin alias for `/xplan --autonomous`. Runs the full xplan pipeline (research + naming + tech stack + scope + multi-agent setup + full plan + full standard review + self-review + **3 sequential adversarial reviews**) end-to-end without any mid-flow prompts, then presents the completed plan as a structured artifact for review at a single final gate.

Pick `/xplana` when:
- You know exactly what you want to plan and don't want the 7-question discovery interview
- You prefer reviewing a finished plan over answering questions during its creation
- You want maximum-depth output (full research, full review) without interruption

Pick `/xplan` (default) when:
- You want the guided, section-by-section interactive experience
- You want to refine the concept during research
- You want to approve the tech stack and scope before the full plan is written

Pick `/xplan --light` when:
- You want a quick pass (reduced research, internal defaults, section-by-section walkthrough at the end)

## Input

```
$ARGUMENTS
```

## Behavior

Delegate immediately to the main `/xplan` command with the `--autonomous` flag set. Read `~/.claude/commands/xplan.md` and execute its full workflow, treating the following flag as set:

```
--autonomous
```

Preserve every other argument the user passed (e.g., `--repo <path>`, `--deepen [<plan-dir>]`). Do NOT strip or transform the concept text.

Autonomous mode affects these xplan phases:

| Phase | What changes in autonomous mode |
|-------|--------------------------------|
| 0.4.0 (Source Freshness Guard) | Runs automatically with NO prompt when `--repo` is set: fetch, pin the origin default-branch anchor, expose a temp anchor worktree, and verify every repo fact against it. Never fast-forwards the user's clone. Skipped entirely for greenfield (no `--repo`). |
| 0.5 (Discovery Interview) | Skipped. Defaults inferred per Phase 0.5 Inference Rules; recorded in `decisions.md`. **Except Q8 (live-testing authorization)**, which cannot be inferred: plan §8.6 records `NOT AUTHORIZED` and 6.A surfaces it. |
| 1.0 (Research Config) | Locked to Full - all 7 research agents fire. |
| 1.5 (Research Review) | Skipped mid-flow; summary stashed for final walkthrough. |
| 2 (Naming Ideation) | Runs silently. Top pick auto-selected; top-5 surfaced in final walkthrough. |
| 2.5 (Tech Stack Sign-off) | Proposal built and auto-approved. No sign-off question. |
| 2.6 (Scope Sign-off) | Proposal built and auto-approved. No sign-off question. |
| 2.7 (Multi-Agent Setup) | Inferred from scope (9+ epics = workspace, 4-8 = flat, 1-3 = single). |
| 4.0 (Review Configuration) | Locked to Full - security + architecture + business logic. |
| 5.6 (Plan Quality Self-Review) | Unchanged - still loops until clean. |
| 5.7 (Adversarial Review Sequence) | **Locked ON.** Runs 3 sequential `adrev-reviewer` passes (Opus 4.8, max effort) against the finished plan — each pass attacks after the previous pass's fixes are incorporated; the third is the final review. No mid-flow prompt - any P0/P1 the final pass leaves unresolved are recorded and surfaced at the 6.A walkthrough + 6.5 gate instead of asking. |
| 6 (Walkthrough) | Runs the new **Phase 6.A Autonomous Plan Walkthrough** - structured plan-as-artifact presentation with explicit assumption callouts. |
| 6.5 (Final Execution Gate) | **Always fires**, same as any xplan run. Autonomous mode does NOT bypass this gate. |

## Built-In Execution Tenets (matter most here)

Autonomous mode is where these four plan tenets matter most: there is no human in the loop during creation *or* execution, so the plan must stand entirely on its own. They apply in every xplan mode, but `/xplana` leans on them hardest. The Phase 5.7 adversarial reviewer enforces all four (T1–T4), expanding the plan when any is thin:

1. **Human interaction is minimized and bucketed to the edges** (T1). No human step an agent could do via CLI/API; unavoidable human work is front-loaded before Wave 1 or deferred to the end, never wedged mid-run. Once execution starts, it runs to done without pausing for a person.
2. **Follow-up-completion contract** (T2, plan §9.5). Any follow-on work discovered during execution is tracked, triaged, and — if in-scope — completed before execution is reported complete. Only genuinely human-blocked work may remain open, surfaced with its exact ask.
3. **Autonomous decision context** (T3, plan §1.4). The plan carries the software's mission, the codebase's governing context, and its decision principles, so an execution agent deduces the direction for unplanned follow-on work *itself* — critical in autonomous mode, where there is no user to ask mid-run. If that context is thin, the adversarial review expands the plan to add it.
4. **Comprehensive autonomous E2E testing** (T4, plan §8). Every plan ships a full autonomous E2E suite over all testable surfaces, wired into CI as a blocking merge gate — the certainty oracle so the user never tests manually. New projects build it in from the ground up; existing repos get **optimistic gap-fill** for touched areas that lack coverage (added by default on the assumption the user wants more coverage — surfaced in the final walkthrough, not gated behind a mid-flow question, which fits autonomous mode). The plan provisions whatever infra certainty needs (testing agents, RunPod, cloud Mac, real devices); there is no resource constraint on testing. The adversarial review expands §8 when coverage is thin.

## What This Command Does NOT Do

- It does NOT skip research, naming, the standard reviews, the self-review loop, or the Phase 5.7 adversarial review sequence. Autonomous is the *deep* mode, not the fast one — the finished plan has survived 6 reviews (3 standard + 3 sequential adversarial) before you see it.
- It does NOT skip the four execution tenets above. The adversarial review enforces minimal edge-bucketed human work (T1), a follow-up-completion contract (T2), enough decision context to direct unplanned work without a human (T3), and a comprehensive autonomous E2E suite over all testable surfaces (T4) — expanding the plan when any is missing.
- It does NOT skip the final execution gate. 6.5 is non-bypassable.
- It does NOT grant live-testing permission. Autonomous planning can infer a tech stack; it cannot approve running app launches, dictation, synthetic input, focus changes, machine-global input/audio overrides, or mic capture on the user's behalf. Plan §8.6 records `NOT AUTHORIZED`, the 6.A walkthrough shows the affected steps, and `/etp` or `/xplan-resume` holds each one and asks before running it. See `~/.claude/rules/live-testing-guard.md`.
- It does NOT automatically proceed to execution. The default recommendation at 6.5 in autonomous mode leans toward "save plan, don't execute yet" so the user can review before committing to multi-agent work.
- It does NOT leave a worktree behind on that gate-stop path. When `--repo` is set, Phase 0.4.0 creates a temp anchor worktree; because autonomous mode usually stops at the 6.5 gate *without* executing, run the Phase 8.7 worktree teardown on exit anyway (it is explicitly early-exit-safe). If any execution worktrees were created, `/worktree-sweep` reclaims the leaks. Nothing worktree-shaped outlives the run — see `git-worktrees.md`.

For the fast path (reduced depth, minimal interaction), use `/xplan --light` instead.

## Correcting Inferred Assumptions

If the final walkthrough surfaces an assumption the user wants to correct, the recommended path is:

```
/xplan --deepen ~/code/plans/{concept-name}
```

`--deepen` mode loads the existing plan and runs targeted passes on under-specified sections without re-running the full pipeline. This is faster than `--autonomous` again with adjusted inputs.

## Companion Commands

- `/xplan` - Full interactive version
- `/xplan-status` - Check progress on a running or completed plan
- `/xplan-resume` - Resume an interrupted plan execution

commands/xplan-status.md

---
description: Check progress on a running or completed xplan
allowed-tools: Agent
argument-hint: "[plan-name]"
---

# xplan-status - Plan Progress Dashboard

Use the Agent tool to execute this workflow on a cheaper model:

- **model**: sonnet
- **description**: xplan status check

Pass the agent all workflow instructions below. Include the received arguments: `$ARGUMENTS`

After the agent completes, relay its dashboard output to the user exactly as received.

---

## Workflow Instructions

Check the status of a running or completed `/xplan` execution.

Arguments: $ARGUMENTS

### Step 1: Find the Plan

If a plan name was provided in the arguments, look for it directly:
```bash
ls ~/code/plans/{plan-name}/
```

If no plan name was provided, list all plans and show the most recently modified:
```bash
ls -lt ~/code/plans/ | head -20
```

If multiple plans exist and none was specified, ask which plan to check.

### Step 2: Read Progress File

Read `~/code/plans/{plan-name}/progress.md` for execution state.

Extract the project name and GitHub username from progress.md or plan.md.

### Step 3: Gather Live State (if IN PROGRESS)

If the plan is still in progress, run the gather script:

```bash
bash ~/.claude/lib/xplan-status-gather.sh "{project-name}" "{github-user}"
```

This outputs structured `=== SECTION ===` blocks: ISSUES, PRS, CLONES, TRACKING.

### Step 4: Present Dashboard

```
Plan: {plan-name}
Status: {IN PROGRESS / COMPLETE / INTERRUPTED}
Progress: {N}/{total} epics complete

Wave Status:
  Wave 1: COMPLETE (3/3 epics merged)
  Wave 2: IN PROGRESS (1/4 epics complete, 2 active, 1 pending)
  Wave 3: PENDING

Active Agents:
  {clone}: {branch} [status from CLONES section]

Open PRs:
  {from PRS section}

Human-Epics:
  {any blocking human tasks from progress.md}

Last Checkpoint: {from progress.md}
```

### Step 5: Suggest Actions

Based on the state:
- If INTERRUPTED: "Run `/xplan-resume {plan-name}` to continue execution"
- If blocked by human-epic: Show the walkthrough instructions for the blocking task
- If COMPLETE: "Run `cat ~/code/plans/{plan-name}/retro.md` to see the retrospective"
- If all agent work done but human-epics remain: List exactly what the user needs to do

commands/xplan-resume.md

---
description: Resume an interrupted xplan execution
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, Agent, AskUserQuestion, WebSearch, WebFetch
argument-hint: <plan-name>
---

# xplan-resume - Resume Interrupted Plan Execution

Resume a previously interrupted `/xplan` execution from its last checkpoint.

## Sub-Agent Model Optimization

When spawning execution agents to resume epic work, set model to **sonnet** in the Agent tool call. The orchestrator remains on the current model for coordination and decision-making.

---

## Input

```
$ARGUMENTS
```

## Instructions

### 1. Find the Plan

If a plan name was provided in `$ARGUMENTS`, look for it:
```bash
ls ~/code/plans/{plan-name}/
```

If no plan name was provided, find plans with IN PROGRESS or INTERRUPTED status:
```bash
for dir in ~/code/plans/*/; do
  if [ -f "$dir/progress.md" ]; then
    name=$(basename "$dir")
    status=$(grep "^## Status:" "$dir/progress.md" | head -1)
    echo "$name: $status"
  fi
done
```

If multiple resumable plans exist, use AskUserQuestion to ask which one.

### 2. Read Plan State

Read these files to reconstruct full context:

1. **progress.md** - Current execution state, completed epics, last checkpoint
2. **plan.md** - Full plan with all epic definitions
3. **decisions.md** - Decisions made during execution
4. **research.md** - Original research (for context on goals and constraints)

### 3. Analyze Last Checkpoint

From the most recent checkpoint in progress.md, determine:
- **Last completed wave**: Which wave finished successfully
- **Current wave**: Which wave was in progress when interrupted
- **Completed epics**: Which epics have been merged to main
- **In-flight epics**: Which epics were being worked on (may have partial work)
- **Pending epics**: Which epics have not been started
- **Resume context**: Key decisions, patterns, and gotchas noted in checkpoint

### 3.5 Re-check Live-Testing Authorization

A resumed run re-enters the plan mid-flight, so re-run the authorization check rather than assuming the interrupted session cleared it. Scan the remaining epics for **live-testing steps** — anything that launches or relaunches an app, fires dictation, posts synthetic input events, changes focus, sets a machine-global input/audio override, opens the mic or camera, or drives a simulator/attached device from the host — and read plan **§8.6 Live-Testing Authorization**.

A step is authorized only by a `GRANTED by {user} on {date}` line naming the runner that step targets. Missing §8.6, `NOT AUTHORIZED`, a grant with no named runner, or a grant naming a different machine all mean **UNAUTHORIZED**: hold the step, list it in the Section 7 resume plan, and ask the user before running it. Live testing never runs on the dev machine, and a plan step that mandates a live test is not its own authorization. See `~/.claude/rules/live-testing-guard.md`.

### 4. Verify Live State

Check actual state against checkpoint (things may have changed):

```bash
# Get project name
# Check what is actually merged to main
gh pr list --state merged --repo {your-username}/{project-name} --limit 100

# Check open PRs (in-flight work)
gh pr list --state open --repo {your-username}/{project-name}

# Check issue state
gh issue list --state open --repo {your-username}/{project-name} --limit 100
gh issue list --state closed --repo {your-username}/{project-name} --limit 100

# Check clone states (auto-detect workspace vs flat model)
# Workspace model: look for {project}-workspaces/{project}-w*/{project}-w*-c*/
WORKSPACES_DIR=~/code/{project-name}-workspaces
REPOS_DIR=~/code/{project-name}-repos

if [ -d "$WORKSPACES_DIR" ]; then
  for dir in "${WORKSPACES_DIR}"/{project-name}-w*/{project-name}-w*-c*/; do
    [ -d "$dir" ] || continue
    echo "=== $(basename $dir) ==="
    git -C "$dir" fetch origin
    git -C "$dir" branch --show-current
    git -C "$dir" status --short
    git -C "$dir" log --oneline -3
  done
elif [ -d "$REPOS_DIR" ]; then
  for dir in "${REPOS_DIR}"/{project-name}-[0-9]*/; do
    [ -d "$dir" ] || continue
    echo "=== $(basename $dir) ==="
    git -C "$dir" fetch origin
    git -C "$dir" branch --show-current
    git -C "$dir" status --short
    git -C "$dir" log --oneline -3
  done
fi
```

### 5. Handle In-Flight Work

For epics that were in progress when interrupted:

1. **Check if PR exists** - If yes, check CI status:
   - CI green: Merge the PR and mark epic complete
   - CI red: Read the failure, fix it, push, wait for green, merge
   - No CI yet: Wait for it

2. **Check for uncommitted work** - If a clone has uncommitted changes:
   - Read the changes to understand what was in progress
   - Determine if the work is complete enough to commit and PR
   - If partial, assess whether to continue or start fresh

3. **Check for branches without PRs** - If work was committed but no PR created:
   - Review the commits
   - Create the PR if work is complete
   - Continue the work if incomplete

### 6. Sync Isolation State

If the run used **worktrees** (the single-machine default), reclaim any that leaked when the run was interrupted — a merged epic's worktree that was never removed, or a built-in `isolation:"worktree"` worktree the harness could not auto-reclaim — before resuming:

```bash
/worktree-sweep    # removes only clean worktrees; preserves any with unsaved in-flight work (see Section 5)
```

Run the sweep *after* Section 5 has recovered in-flight work, so a worktree holding uncommitted progress is preserved (the sweep never removes a worktree with unsaved changes) rather than swept.

If the run used **clones**, ensure all clones are on latest main:
```bash
for i in 0 1 2 3; do
  dir=~/code/{project-name}-repos/{project-name}-$i
  if [ -d "$dir" ]; then
    git -C "$dir" fetch origin
    git -C "$dir" checkout main
    # Safe: resets to remote ref (auto-approved by hook)
    git -C "$dir" reset --hard origin/main
  fi
done
```

### 7. Present Resume Plan

Show the user:
```
Resuming: {plan-name}
Last checkpoint: Wave N - {timestamp}

Completed (verified):
  - Epic 1: "Repo Setup" (PR #1, merged)
  - Epic 2: "Shared Types" (PR #3, merged)
  - Epic 3: "Auth Flow" (PR #5, merged)

Recovered (in-flight at interruption):
  - Epic 4: "User Dashboard" - PR #7 exists, CI green, merging now
  - Epic 5: "API Endpoints" - partial work on clone-1, continuing

Remaining:
  Wave 3: Epic 6, Epic 7, Epic 8 (all pending)
  Wave 4: Epic 9, Epic 10 (all pending)

Human-epics still needed:
  - Human-Epic 2: "Stripe Setup" (blocks Wave 4)

Live-testing steps held (no recorded grant in §8.6):
  - Epic 7: "dictation fixture preflight" - needs a named runner + your approval

Resuming from: Wave N (continuing in-flight) / Wave N+1 (all prior complete)
```

### 8. Resume Execution

Continue execution using the same protocol as Phase 7 of `/xplan`:
- Pick up from the current wave
- Spawn agents for remaining epics
- Continue writing checkpoints after each wave
- Update progress.md throughout
- Do not stop until all completable work is done

### 9. On Completion

Follow Phase 8 of `/xplan`:
- Full audit
- Final report
- Generate or update retro.md (note the interruption and resume in the retro)
- Offer to save as template
- Update agent logs
- Mark progress.md as COMPLETE

commands/etp.md

---
description: Execute a ready plan OR GitHub issue(s) end-to-end with parallel agents, adversarial PR review, and follow-up completion. Runs to completion, stopping only for absolute blockers.
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, Agent, AskUserQuestion, WebSearch, WebFetch
argument-hint: <plan-file-or-dir | #issue [#issue ...] | issue-url> [--dry-run] [--confirm] [--max-agents N] [--light-review]
---

# etp - Execute the Plan or Issue(s)

Take work that is ready to build - a plan an agent has been developing, any plan file you point at, or one-or-more GitHub issues that have been investigated and are ready to complete - and drive it to done. `etp` does not research, name, or write the work; it **executes** it. It resolves the target into units, runs them with parallel agents, adversarially reviews every PR with a *separate* agent, fixes what is reasonable and valid, completes the follow-up work that surfaces along the way (reviewing those PRs too), and does not stop until everything that can be done is done.

A plan and an issue are two **sources** of work that both resolve into the same thing - **units** - and everything downstream (review → fix → merge → follow-ups → audit) is shared. **Ceremony scales to the work**: a single issue skips the multi-clone / wave / bring-up machinery a multi-epic plan needs and collapses to implement → adversarial review → fix → merge → follow-ups → done.

## Prime Directive

> Execute the plan that is linked in the prompt itself, according to the file path, use as many agents as makes sense, do adversarial critical reviews of all PRs and make any fixes to those PRs that are reasonable and valid, complete any follow-up issues that arise during the execution of the plan and do adversarial reviews on those PRs as well. Don't stop until everything is complete. Do your best to reason through issues that you find along the way. If there's an absolute blocker, notify me of that, but continue the plan if possible.

Everything below is the operational expansion of that directive. When a phase and the directive seem to conflict, the directive wins. The directive was written for a plan; it applies identically when the work source is one or more GitHub issues - read "plan" as "the work you pointed me at."

## When to use `/etp`

- A `/xplan` (or `/xplana`) plan exists and you want it executed now.
- An agent has been working a plan and you want another agent to pick it up and finish it.
- You have a hand-written plan, design doc with executable steps, or checklist at a known path.
- **One or more GitHub issues** have been investigated and are ready to build: `/etp #42`, or a batch `/etp #42 #43 #45`.

## When NOT to use it

- There is no ready work yet - neither a plan nor an investigated issue. Create a plan (`/xplan`) or flesh out the issue first.
- The issue is vague or un-investigated (no acceptance criteria, no approach, no root-cause). `etp` surfaces this rather than guessing - see Phase 0.5. Investigate it first (`/debug` for a bug, `/xplan` for a feature).
- The "plan" is pure prose with no executable units (a vision doc). Same handling: surfaced, not guessed.
- You want to resume an `/xplan`-native interrupted run with its full epic/wave checkpoint structure - `/xplan-resume` is the specialized tool. `etp` is the general work executor (and is itself resumable - see Resumability).

## Sub-Agent Model Optimization

| Role | Agent type | Model |
|------|-----------|-------|
| Implementation (one per unit) | `implementer` | sonnet |
| Adversarial review - Stage 1 | `spec-compliance-reviewer` | sonnet |
| Adversarial review - Stage 2 | `code-quality-reviewer` | sonnet |
| Follow-up fixes | `implementer` | sonnet |
| Cheap mechanical checks (status, ls) | default | haiku |
| Orchestrator (this session) | - | current model |

The orchestrator stays on the current model for synthesis, triage, and routing. It never grades a PR itself - grading is delegated to a separate agent (see Integrity, below).

---

## Input

```
$ARGUMENTS
```

Parse from `$ARGUMENTS`:
- **Target** (positional): a plan path (file or directory), **one or more issue references** (`#N`, a bare number, or a GitHub issue URL), or empty.
- **`--dry-run`**: resolve and analyze the target, print the execution model, then STOP. No branches, PRs, or merges.
- **`--confirm`**: pause for one explicit go/no-go gate after the pre-flight analysis (Phase 3). Off by default - the directive says don't stop, so the default is to proceed once the target is resolved unambiguously.
- **`--max-agents N`**: cap concurrent implementation agents (default: the width of the widest wave, clamped to the available isolation slots).
- **`--light-review`**: downgrade the adversarial review to a single separate-agent spec-compliance pass (skip the Stage-2 code-quality pass). For trivial diffs only. **The default is the full two-stage review regardless of diff size** - this flag is an explicit opt-out, never the default.

---

## Phase 0: Resolve & Load the Work Source

### 0.1 Classify the target

Determine the target type deterministically - with shell/pattern checks, not guesses:
- Every positional token matches an **issue reference** (`^#?\d+$`, or a `github.com/.../issues/N` URL) → **issue target** (single if one, **batch** if several).
- The positional token is a **path** that exists (file or directory) → **plan target**.
- **Empty** → autodetect an in-progress **plan** only (see 0.2.3). Do not autodetect an issue - guessing which issue to build is the wrong kind of guess.
- Anything that resolves to neither (a path that does not exist, a malformed ref) → absolute blocker: notify the user with what you looked for; do not invent a target.

Do not mix types in one run (a plan path AND issue refs). If both appear, ask which the user meant.

### 0.2 Resolve a plan target

1. **Explicit file** - the argument is a path to a file that exists → that is the plan.
2. **Directory** - look, in order, for `plan.md`, `PLAN.md`, then the single most-recently-modified `*.md`. If a `progress.md` / `etp-progress.md` sits beside it, load that too (resumability).
3. **Empty argument (autodetect)** - find the plan an agent has been working on:
   - `ls -t ~/code/plans/*/plan.md` and check each sibling `progress.md` for `Status: IN PROGRESS` or `INTERRUPTED`.
   - Check the current repo root and `docs/` for a `plan.md` / `PLAN.md`.
   - Exactly one in-progress candidate → use it. Several → list them and ask (AskUserQuestion). None → absolute blocker, notify and stop.

Announce explicitly: `Resolved plan: <absolute path>`. Resolving the *wrong* target and executing it is the most expensive failure mode this command has.

### 0.3 Resolve an issue target

For each issue reference, load the full issue - the body **and the comments** (the comments are where investigation, root-cause, and the agreed approach usually live):

```bash
gh issue view <N> --json number,title,body,comments,labels,state,assignees
```

- Announce each: `Resolved issue: #<N> "<title>" (<state>)`.
- If an issue is **CLOSED**, warn and ask whether to proceed (it may already be done - see Phase 2).
- The issue's title + body + acceptance criteria + investigation comments **become the unit's spec** - this is exactly what the adversarial reviewer will check the PR against (Phase 4.2). Capture it.
- For a **batch**, resolve every issue before building the model so dependencies between them are visible.

### 0.4 Load full context

Read everything that grounds the work - never operate on a skimmed target:
- A plan: the plan file plus `progress.md`/`etp-progress.md`, `decisions.md`, `research.md`, `reviews/*.md` (whatever exists).
- An issue: the issue body + all comments, plus any files, PRs, or prior issues they link to.
- In both cases: the target repo's `CLAUDE.md` and `README.md` once the repo is identified (Phase 1.3).

### 0.5 Validate the work is executable

Work must be concrete enough to build without guessing.
- A **plan** must contain actionable units. Pure prose with no steps/epics/tasks → do not fabricate an execution graph.
- An **issue** must carry enough to act: acceptance criteria or a clear definition of done, plus an approach or root-cause for anything non-trivial. A one-line "fix the thing" with no investigation does not qualify.

In either case, if the work is too thin, **do not guess**. Present the decomposition you *would* execute and ask the user to confirm or point you at the investigated/fleshed-out version. This is a soft blocker handled per the Confusion Protocol.

### 0.6 Live-testing authorization check (before anything runs)

Scan the work for **live-testing steps**: anything that launches or relaunches an app, fires dictation, posts synthetic input events, changes focus, sets a machine-global input/audio override, opens the mic or camera, or drives a simulator/attached device from the host. These never run on the dev machine — its focus, keyboard, and dictation are the user's control channel to every concurrent agent stream (`~/.claude/rules/live-testing-guard.md`).

For each live-testing step found, look for the recorded grant — an xplan plan carries it in **§8.6 Live-Testing Authorization**; another plan or an issue carries it wherever the user wrote it, or not at all:

| What you find | What it means |
|---|---|
| `GRANTED by {user} on {date}`, naming the runner this step targets | Authorized. Run it there. |
| No §8.6 / no grant anywhere in the work | **UNAUTHORIZED** |
| `NOT AUTHORIZED`, or a grant with no named runner | **UNAUTHORIZED** |
| A grant naming a different machine than this step targets | **UNAUTHORIZED** |
| The plan step itself instructing the test to be run | **UNAUTHORIZED** — that is the thing being authorized, not the authorization |

Mark every UNAUTHORIZED step as **held** and record it for Phase 3's pre-flight print. A held step is a notify-and-continue blocker: it blocks itself, never the run. Before executing any held step, surface it to the user — name the specific step, say no grant was recorded, and ask where it should run and whether they approve — then wait. Absence of a recorded grant is never consent, and neither is the user approving the run as a whole.

---

## Phase 1: Build the Execution Model

### 1.1 Decompose into units

Resolve the target into discrete **units** - the smallest independently-shippable pieces:
- **Plan**, xplan-style → one unit per agent-epic.
- **Plan**, task list / checklist → one unit per task / unchecked item.
- **Plan**, prose with embedded steps → one unit per step you can scope concretely.
- **Single issue** → **one unit** (the issue itself).
- **Issue batch** → **one unit per issue**.

Each unit needs: a clear scope, the files/areas it touches, its acceptance criteria, and its dependencies. For an issue unit these come from the issue (body + criteria + investigation comments). For a plan unit, use the plan's own spec if present, else derive it and record the derivation.

### 1.2 Derive dependency order → waves

Group independent units into **waves** (parallel within a wave, sequential across waves):
- **Single issue** → one wave of one. No wave machinery.
- **Issue batch** → group issues that touch different files/areas into a parallel wave; serialize any that depend on each other (an issue that says "depends on #A", or two issues editing the same files). When independence is unclear, **serialize** - parallelism is an optimization, correctness is not.
- **Plan** → use the plan's own waves/dependency graph; infer it from file overlap if the plan is a flat list.

### 1.3 Identify the target repo, isolation, and ceremony level

- Find the target repo (from the plan, the issue's repo, or the cwd).
- **Isolation — worktrees are the default.** Give each parallel implementation agent its own **git worktree** (`isolation: "worktree"`) so they never share a working tree. Worktrees are ephemeral (one per unit, torn down on merge — see 4.4), share the parent `.git`, and reclaim disk automatically. **Do not provision extra permanent clones for parallelism.** Use clones only when the repo *already* has a multi-clone setup you should reuse, or a specific need forces it: per-branch dev-server ports (worktrees share `.env`), hook-driven per-branch `tracking.csv`, multiple long-lived independent agents, or cross-machine dispatch. When an existing workspace/flat-clone setup is present, assign one unit per clone; otherwise (the common case) use worktrees. See `git-worktrees.md`.
- **"As many agents as makes sense"** = `min(units in this wave, available isolation slots, --max-agents)`. Never spawn more agents than the wave has independent units. With worktrees the "isolation slots" are effectively the concurrency cap (below), not a fixed clone count.
- **Ceremony scales to the work.** Match the apparatus to the size:

  | Target | Waves | Isolation | Bring-up runbook | Checkpoint record |
  |--------|-------|-----------|------------------|-------------------|
  | Single issue | none (1 unit) | one worktree | only if the issue calls for it | the issue + its PR |
  | Issue batch | group independents | one worktree per parallel unit | per-issue if any call for it | live GitHub state (re-run reconciles) |
  | Plan | from the plan | worktrees (or an existing clone/workspace setup) | per the plan (Phase 4.5) | progress file beside the plan |

  Do not impose plan-scale ceremony (multi-clone provisioning, wave checkpoints, bring-up runbooks) on a single issue. Do not skip it for a real multi-epic plan. Every worktree created here is torn down after its unit merges (4.4) and any leaks are swept at the end (Phase 8) — teardown is mandatory, not best-effort.

### 1.4 Surface prerequisites and human-only steps (bucket them to the edges)

Scan for anything the work needs that an agent cannot do: credentials, API keys, OAuth/dashboard setup, DNS, paid-service signups. First apply the minimization test — anything an agent *can* do via CLI/API is not human work; do it, don't ask. Whatever genuinely remains is **bucketed to the edges**, never left to stall the run mid-stream:

- **Front-loaded** — surface every up-front human-only step *now*, before any unit runs, so the human can clear them once and the run then proceeds untouched.
- **Deferred** — human steps that only make sense at the end (final DNS cutover, store submission, a human sign-off) are queued for after all agent work completes.

They become Phase 5 blockers (notify-and-continue), not silent failures mid-run. The user should not be a step *inside* the run: never pause the whole run waiting on human work that could have been front-loaded.

### 1.5 Establish the decision context (so follow-on calls need no human)

Execution will surface unplanned follow-on work (Phase 5). To triage and direct it *without stopping to ask the user*, ground yourself in the decision context first:

- **Plan target**: read the plan's **Mission & Guiding Decision Principles** (xplan plans: §1.4) — the software's mission, the codebase's governing conventions, and the plan's decision heuristics. This is what lets you decide follow-on direction the way the plan's author would.
- **Issue target, or a plan missing that section**: derive the equivalent from the codebase — its `CLAUDE.md`, `README.md`, and the conventions visible in the code — plus the issue's own stated intent. If you cannot form a confident decision context this way, that gap itself is a Phase 5 human-blocked item (a product decision with no right answer), not a reason to guess.

Hold this context for Phases 5 and 6: it is the reference you triage and reason against.

---

## Phase 2: Reconcile with Live State (resumability)

Work that an agent has touched is rarely a blank slate. Before executing, reconcile against reality so you never redo finished work:

```bash
gh pr list --state merged --limit 100      # what already landed
gh pr list --state open                    # in-flight work
gh issue list --state all --limit 200      # tracked + closed work
git branch -a                              # existing feature branches
```

Classify each unit:
- **DONE** - its PR is merged, its issue is already closed by a merged PR, or its acceptance criteria already hold on `main`. Skip it.
- **IN-FLIGHT** - an open PR or a branch exists. Do not restart it; enter it at the review step (Phase 4.2). A branch with no PR → assess the commits and open the PR if the work is complete.
- **PENDING** - not started. Full treatment.

For an issue **batch**, this is the resume mechanism: re-running `/etp #42 #43 #45` skips the ones already merged and continues the rest - no progress file needed, live GitHub state is the checkpoint. Build the remaining-work list from PENDING + IN-FLIGHT only.

---

## Phase 3: Pre-Flight Analysis

Print the execution model so the run is legible before it starts:

```
Work source: <plan path | issue #N | issues #N #M #… (batch)>
Repo: <repo>  ·  Isolation: workspace | flat-clones | worktrees | single
Units: N total  ·  M already done  ·  K remaining
Waves: <wave 1: units…> → <wave 2: units…> → …   (or "single unit" / "N independent issues")
Review: full two-stage (default)  |  light (spec-compliance only)
Max parallel agents: <n>
Prerequisites / human-only blockers detected: <list or none>
Live-testing steps: <none | N authorized on {runner} | N HELD - unauthorized (list them)>
```

Then choose the path:
- **`--dry-run`** → stop here. The model above is the deliverable.
- **`--confirm`** → ask one AskUserQuestion go/no-go gate, then proceed on approval.
- **Default** → proceed immediately. The directive is to execute, not to ask.

**Exception - Confusion Protocol.** Stop and ask even in default mode when a genuine high-stakes ambiguity exists. Narrow triggers: the target could not be resolved; the work is too thin to build without guessing (0.5); it calls for destructive or irreversible actions it does not clearly authorize (dropping data, deleting resources, force-pushing shared branches, production deploys not named in the work); a live-testing step is held as UNAUTHORIZED (0.6) and its turn has come; or two incompatible interpretations of a unit's scope exist and the choice shapes everything downstream. Name the ambiguity in one sentence, give 2-3 options with tradeoffs, and wait. This is the directive's "absolute blocker → notify me" path. Everything outside these triggers, you reason through yourself.

---

## Phase 4: Execute

For each wave, in order (a single issue is a wave of one). This is the core loop, identical for plans and issues.

### 4.1 Implement (parallel within a wave)

Spawn one `implementer` agent per unit (model sonnet), each in its own **worktree** (`isolation: "worktree"`) — or its assigned clone when the repo uses the multi-clone setup (1.3). Each agent:

> **Concurrency — avoid the 429 throttle.** `implementer` agents run on `sonnet` (light), so a wave of up to ~8 is safe. But the default `--max-agents` (widest wave, clamped to isolation slots) can exceed that in a workspace with many clones — **cap the live wave at 8 light agents, or 4 if you raise `implementer` to a heavier model / higher effort**. If a wave has more units than the cap, split it into sub-waves. Bursting too many heavy agents trips a server-side rate limit (`Server is temporarily limiting requests · Rate limited`) that fails the whole wave; if you hit it, wait 30–60s and re-spawn only the unfinished units in smaller sub-waves. See `~/.claude/rules/concurrency-and-rate-limits.md`.
- Branches from `origin/main` (`git checkout -b {issue#}-{desc} origin/main` for an issue unit, `{slug}-{desc}` for a plan unit).
- Implements the unit with tests, following the existing project patterns.
- **Verifies the work actually functions** - unit tests passing is the floor, not the finish line. Run the real path where feasible.
- Pushes and opens a PR. For an issue unit the PR body **closes the issue** (`Closes #N`); for a plan unit it references the plan unit and closes its tracking issue if one exists.
- Returns the four-state status (DONE / DONE_WITH_CONCERNS / BLOCKED / NEEDS_CONTEXT) with verification evidence.

Do not trust the self-report as proof. The diff and the review are the proof.

### 4.2 Adversarial review (SEPARATE agents - this is the integrity property)

For every PR - newly created or inherited in-flight - run the adversarial review. **The reviewer is a different agent from the implementer and is given the unit's spec plus the diff - never the implementer's rationale or self-report.** For an issue unit, the **spec is the issue** (title + body + acceptance criteria + investigation comments). For a plan unit, the spec is the plan's unit. Coupled self-grading inflates grades; an implementer asked to grade its own work will pass it. Independence is what makes the sign-off mean anything.

**Default - full two-stage review** (every PR, regardless of diff size):
- **Stage 1 - `spec-compliance-reviewer`**: Did the PR deliver exactly the spec (the issue's definition of done / the plan unit's deliverables)? Everything present? Any scope creep (files, helpers, adjacent "while I'm here" changes not asked for)? It treats the implementer's DONE as a claim and re-verifies from the diff. Stage 1 gates Stage 2.
- **Stage 2 - `code-quality-reviewer`** (only if Stage 1 passes): correctness bugs, security holes, silent failures, unhandled edge cases, project-pattern violations, over-engineering. Runs fresh checks (tests, build) rather than trusting prior output.

**`--light-review`** collapses this to Stage 1 only - a single separate-agent spec-compliance pass, skipping Stage 2. Use it only for trivial diffs (a typo, a constant bump). It still keeps the integrity property (a separate reviewer, the issue as spec); it only drops depth. It is never the default.

Each stage returns a verdict and a specific, itemized findings list. Quality-reviewing a spec-failing PR wastes effort on code that will change - so the order is fixed, never parallel.

### 4.3 Apply reasonable and valid fixes

Triage the findings yourself (orchestrator judgment - latent work, not delegable):
- **Reasonable and valid** (real bug, real scope creep, real spec gap) → fix it. Dispatch an `implementer` against the PR branch (or fix inline for a one-liner), push, and **re-review the changed PR** (back to 4.2).
- **Invalid, speculative, or out-of-scope** (gold-plating, hypothetical edge cases, "you could also…") → reject with a one-line reason recorded in the run record. Completeness means finishing the unit, not expanding it. Do not implement review suggestions with no caller or that the work did not ask for.

Loop review → fix → re-review until the PR passes. Bound it: after **3 fix rounds** on the same PR without convergence, freeze that PR, record the unresolved findings as a blocker, and move on - one stuck PR does not halt the wave.

### 4.35 Drive the PR to CI-green (bounded post-PR loop)

The finish line is not "PR opened" - it is "CI green and mergeable." Adversarial review (4.2-4.3) judges the diff; this step makes the *pipeline* agree. Run it for every PR after it passes adversarial review and before it can merge (4.4). It applies to inherited in-flight PRs too.

**Read CI fresh, never assume.** Poll the actual checks - do not infer state from "the implementer said tests passed":

```bash
gh pr checks <PR> --watch    # block until checks finish (or poll without --watch and re-read)
gh pr view <PR> --json mergeable,mergeStateStatus,statusCheckRollup
```

Classify the result:
- **Green and mergeable** → done with this step; proceed to 4.4.
- **Red checks** → a real failure to diagnose and fix (below).
- **Conflicting / behind base** (`mergeable: CONFLICTING`, or a `mergeStateStatus` indicating the branch is behind) → rebase the branch on the latest `origin/main` and resolve conflicts before re-checking.
- **Pending/queued** → keep waiting; do not act on an unfinished run.

**The bounded fix loop.** Each round, in order:

1. **Read the failure, fully.** Pull the failing job's logs - `gh run view <run-id> --log-failed` (find the run via `gh pr checks` or `gh run list --branch <branch>`). Read the actual error, the failing job, and the step. Do not guess from the check name.
2. **Find the root cause, then fix at the source.** This is systematic-debugging, not symptom-patching: one hypothesis, the minimal change. For a code/test failure, dispatch a targeted `implementer` (model sonnet) against the PR branch scoped to *exactly* that failure - never a broad rewrite. For a merge conflict / behind-base, rebase on `origin/main` and resolve. For a suspected flaky check, re-run it once (`gh run rerun <run-id> --failed`) before treating it as real - a check that fails twice is a real bug, not flake.
3. **Push and re-check.** Push the fix, then re-read CI fresh (back to the top of this step). A fix that introduces a new failure counts as the same loop continuing, not a fresh start.

**The bound (explicit, no infinite loop).** Allow **at most 3 CI-fix rounds** on the same PR. This mirrors the three-strike rule (Phase 6) and the 4.3 review bound. After 3 rounds without reaching green:
- **Freeze** the PR (do not merge it).
- **Record** the unresolved CI failure as a blocker on the run record (issue/batch: a one-line note + the failing-job link; plan: the progress file's blocker list).
- **Escalate to the user** with a clear, specific summary: which PR, which check failed, the root-cause read so far, the fixes already attempted, and the exact failing-log link. This is the directive's "absolute blocker → notify me" path for CI.
- **Continue all other PRs and waves.** One CI-stuck PR is set aside, never a halt for the run.

A PR that cannot be driven green within the bound is treated exactly like a PR frozen at 4.3: blocked, recorded, escalated, and stepped around - not merged, not abandoned silently.

### 4.4 Merge and tear down the unit's worktree

Merge a PR only when: it passed review (both stages, or Stage 1 under `--light-review`), CI is **green and mergeable** (verified fresh via 4.35, not assumed), and it does not conflict. Merge in dependency order within the wave. Squash merge (the repo default). Never merge a PR that failed adversarial review or has red/unresolved CI to "keep moving" - that defeats the entire loop.

**Then immediately remove that unit's worktree** (when the unit ran in a worktree, the default). This is mandatory, not best-effort — a built-in worktree does **not** auto-remove, so a merged unit whose worktree lingers is exactly the leak that filled 237 GB in the incident:

```bash
git worktree remove <unit-worktree-path>   # non-force; the branch ref and its commits survive removal
git worktree prune
```

Removing the worktree does not delete the branch or the merged work — only the checkout (and its build tree). If the non-force remove refuses (unexpected for a just-merged clean unit), do not force it blindly; leave it for the Phase 8 sweep to classify. A unit that ran in a reused clone (multi-clone setup) is not a worktree — reset that clone to `origin/main` instead of removing anything.

### 4.5 Bring-up & integration verification (when applicable)

If the work specifies bring-up (migrations, dependency installs, type regen, env/secret sets, dev-server/worker restarts, deploys), execute it and verify every layer is actually live - DB migrated, backend responding, frontend loading, workers running, deploy current - then run the **autonomous E2E suite against the running system**, not just CI. For a plan, that is its Section 8 suite; for an issue, the E2E tests covering the changed surface. The E2E suite (not a bare smoke test) is the certainty gate: green ⇒ clean / mergeable, red ⇒ broken. A single issue usually has no bring-up; confirm that is true rather than assuming. A plan wave does not advance against a degraded system or a red suite.

**If the touched surface has no E2E coverage**, adding it is an in-scope follow-up (Phase 5), not an optional extra — the standing assumption is that more autonomous E2E coverage is always wanted, so the changed behavior gets a real end-to-end test before the work is called complete. This is the executor's side of the plan's E2E mandate (adrev tenet T4).

### 4.6 Checkpoint

Record progress so the run is resumable, matched to the ceremony level (1.3):
- **Single issue** → the issue + its PR is the record. No file.
- **Issue batch** → live GitHub state is the record; a re-run reconciles (Phase 2). Optionally jot a one-line status per issue if the batch is large.
- **Plan** → update the progress file (`progress.md`, else `<plan-basename>.etp-progress.md` beside the plan) with units done, PRs, merged SHAs, next wave, live-state result, and open blockers.

---

## Phase 5: Follow-Up Work That Arises

Execution surfaces work the target did not enumerate: a bug found while integrating, a missing prerequisite, a gap between two units, a flaky test that is really a real bug. Handle every one - do not let it evaporate. This is the directive's "complete any follow-up issues that arise," and for an xplan-authored plan it is that plan's **Follow-Up Work Completion Contract (§9.5)**: execution is **not complete** while an in-scope follow-up remains open.

For each arising item:
1. **Track it** - open a GitHub issue (label it `follow-up`), so nothing is lost.
2. **Triage scope against the decision context (Phase 1.5)** - decide this *yourself* from the plan's §1.4 / the codebase's mission and conventions; do NOT stop to ask the user how to direct in-scope follow-on work. That is what the decision context is for.
   - **In-scope-now** (the work cannot be called complete, or reasonable+valid, without it) → treat it as a first-class unit: branch, implement (`implementer`), then the **same adversarial review** as any other PR (Phase 4.2-4.4), then merge. Follow-up PRs get adversarially reviewed too - this is explicit in the directive.
   - **Out-of-scope / speculative** (a nice-to-have, an unrelated improvement, a v2 idea) → log it as a deferred issue and leave it. Surface the deferred list in the final report. Scope discipline: finish the work and what it necessitates, not every improvement you can see.
3. **Absolute blocker** (needs a credential you do not have, a human-only dashboard action, a product decision with no right answer) → notify the user immediately with the exact ask, file a `blocked` issue, and **continue all non-blocked work**. A blocker stops one unit, never the run. Human-blocked follow-ups are the *only* work allowed to remain open when the run is reported complete.

---

## Phase 6: Reason Through Problems

When a unit fails - red CI, merge conflict, failing test, an ambiguous step - do not stop and do not stack random fixes. Apply systematic debugging:
1. Read the actual error fully.
2. Find the root cause (trace it; do not patch the symptom).
3. Form one hypothesis, make the minimal change, verify it.
4. If three focused attempts fail (three-strike rule), stop guessing: question the assumption, re-read the relevant source/docs, or escalate that single unit as a blocker - then continue the rest.

Distinguish "something I can reason through" (the overwhelming majority - ambiguous wording, an obvious-once-traced bug, a missing import) from "an absolute blocker that genuinely needs the human" (missing credentials, a product decision with no right answer, an irreversible action the work does not authorize). Reason through the first kind — ground the call in the decision context (Phase 1.5): the plan's mission and decision principles, or the codebase's conventions, tell you which direction the author would take. Notify on the second. Never conflate "this is hard" with "this is blocked."

---

## Phase 7: Run to Completion

Loop Phases 4-6 until every condition holds:
- All units DONE or explicitly escalated as blocked.
- All in-scope follow-ups DONE.
- All PRs merged (or frozen-and-recorded as blocked); all target issues closed by their merged PRs.
- Every merged PR reached CI-green via the bounded loop (4.35); any PR that could not be driven green within the bound is among the frozen-and-recorded blockers, not silently merged.
- CI green, no uncommitted changes in any clone, no unexpected open PRs.
- No merged unit's worktree left behind — each removed at 4.4, and `/worktree-sweep` (Phase 8) reclaimed any leak.
- All layers confirmed live (where the work has runtime impact); the **autonomous E2E suite is green** against the running system (the plan's §8 suite, or the E2E tests covering a changed issue surface) — the certainty oracle, not a bare smoke test. Any surface a plan's §8.5 names as not-certified is the only acceptable manual residue.

"Don't stop until everything is complete" means: do not stop while completable work remains. Blocked units are set aside with a clear notification; they do not end the run. The run ends when the only thing left is genuinely human-blocked, and the user has been told exactly what each blocker needs.

---

## Phase 8: Final Audit & Report

### 8.1 Fresh audit (evidence, not memory)

```bash
gh pr list --state open
gh issue list --state open
# per clone: git status; then run the project's test + build
```

**Sweep leaked worktrees (mandatory teardown backstop).** Even with per-unit removal at 4.4, a worktree can leak — a unit that errored before its merge-and-remove, a `isolation:"worktree"` worktree the harness could not auto-reclaim because it was built in, an early exit. Run the safe sweep so no worktree outlives the run:

```bash
/worktree-sweep        # removes only clean worktrees; preserves any with unsaved work; prunes stale metadata
```

Report what it reclaimed and anything it preserved (a preserved worktree means unsaved work an implementer left behind — surface it, do not force it away). **Run this even when the run exits early** (a blocker halts a unit, a gate stops the run): teardown must not depend on reaching a clean completion — that is the whole lesson of the incident.

### 8.2 Report to the user

- **Completed**: units finished, PRs merged, issues closed - with evidence (test output, **autonomous E2E suite result (green)**, live URLs).
- **Blocked**: each blocker, why, and the exact human action that unblocks it.
- **Deferred**: out-of-scope follow-ups logged but intentionally not done.
- **Live state**: the verification that the system actually runs end-to-end (where applicable).

### 8.3 Finalize the run record

A plan run: mark the progress file `COMPLETE`, or `BLOCKED - WAITING ON HUMAN` with the blocker list. An issue/batch run: live GitHub state is the record - a re-run of `/etp <same args>` reconciles and resumes from exactly here.

---

## Guardrails

**Integrity - the separate judge.** The agent that reviews a PR is never the agent that wrote it, and the reviewer never sees the implementer's rationale or self-report - only the unit's spec (the issue, or the plan unit) and the diff. The orchestrator does not grade PRs in its own context either. This separation is the whole reason a self-signed-off execution can be trusted; collapsing it turns review into rubber-stamping.

**Two-stage order is fixed.** Spec-compliance gates code-quality. Never run them in parallel, never quality-review a spec-failing PR. `--light-review` drops Stage 2; it never reorders or parallelizes the stages.

**Verify, don't trust.** A subagent's DONE is a claim. Read the diff, run the tests, check the artifact before treating a unit as complete. Fresh evidence before every completion claim.

**Scope discipline.** Execute the work plus the follow-ups it necessitates. Reject "while I'm here" work, speculative features, and review suggestions with no caller. Finishing the job is not expanding the job.

**Notify-and-continue.** Absolute blockers are reported the moment they are found and never halt non-blocked work. The run degrades gracefully around blockers; it does not stop dead.

**Human work at the edges.** The user is not a step inside the run. Anything an agent can do via CLI/API is done, not asked. Genuine human-only work is surfaced up front (front-loaded, Phase 1.4) or queued for the end (deferred), never left to stall the run mid-stream. In-scope follow-on work is reasoned through against the decision context (Phase 1.5), not bounced to the user — only genuinely human-blocked items remain open at completion, each surfaced with its exact ask.

**The E2E suite is the completion oracle — no manual testing bounced to the user.** "Done" means the autonomous end-to-end suite is green against the running system, not "I believe it works" or "the user can check." Provision whatever the suite needs — testing agents for flows that can't be asserted programmatically, third-party compute (RunPod, cloud Mac, real devices) as a front-loaded prerequisite; there is no resource constraint on testing. A changed surface without an E2E test gets one before completion (adrev tenet T4 / plan §8). The only manual residue permitted is a surface a plan's §8.5 explicitly names as not-certified.

**Safety on irreversible / outward actions.** Even in autonomous mode, anything destructive or externally-visible that the work did not clearly authorize - production deploys, resource deletion, force-pushing shared branches, sending external communications - requires notifying the user first. The work authorizes its own scope; it does not authorize off-scope irreversible acts.

**Live testing runs on the runner, under a recorded grant.** App launches/relaunches, dictation firing, synthetic input events, focus changes, machine-global input/audio overrides, mic/camera capture, and host-driven simulator or device runs never touch the dev machine — that machine's input surface is the user's control channel to every concurrent agent stream. Every such step needs a grant recorded at planning time (plan §8.6) naming the runner; a step without one is held as UNAUTHORIZED (0.6), surfaced by name, and asked about before it runs. A plan instructing the test is not the authorization for it. See `~/.claude/rules/live-testing-guard.md`.

**Worktree teardown is mandatory.** Parallel units run in ephemeral worktrees by default (1.3). Each is removed the moment its PR merges (4.4), and Phase 8 sweeps any leak (`/worktree-sweep`) — including on early exit. A built-in worktree never auto-removes; leaving merged units' worktrees behind is the exact failure that consumed 237 GB in the incident. Removing a clean worktree never loses committed work (the branch ref survives). Do not force-remove worktrees with unsaved work — the sweep preserves those.

**No AI attribution** in commits or PR bodies (per the git-workflow rule). Use the repo's PR template if one exists.

**Resumability.** A plan run checkpoints to the progress file beside the plan; an issue/batch run uses live GitHub state. Either way, re-invoking `/etp` on the same target continues rather than restarts.

---

## Relationship to xplan

- `/xplan`, `/xplana` - create a plan (research → plan → review). `etp` consumes work; it never creates a plan.
- `/xplan-resume` - resumes an `/xplan`-native interrupted execution using xplan's epic/wave checkpoint structure. Prefer it when the target is a live xplan run.
- `/etp` - executes **any** ready work: a plan file (xplan-authored or not) **or** one-or-more investigated GitHub issues, through the hardened adversarial-review and follow-up loop above. It is the general-purpose execution engine.
lib (2)

lib/xplan-status-gather.sh

#!/usr/bin/env bash
# xplan-status-gather.sh - Parallel data gathering for /xplan-status
# Usage: xplan-status-gather.sh <project-name> <github-user>
# Runs issue, PR, and clone state checks concurrently.

PROJECT="$1"
GH_USER="$2"

if [ -z "$PROJECT" ] || [ -z "$GH_USER" ]; then
  echo "ERROR: Usage: xplan-status-gather.sh <project-name> <github-user>"
  exit 1
fi

TMPDIR=$(mktemp -d)
trap "rm -rf $TMPDIR" EXIT

# --- Parallel jobs ---

# 1. Open issues
(
  gh issue list --state open --repo "${GH_USER}/${PROJECT}" --limit 50 2>/dev/null || echo "none"
) > "$TMPDIR/issues" &

# 2. Open PRs
(
  gh pr list --state open --repo "${GH_USER}/${PROJECT}" 2>/dev/null || echo "none"
) > "$TMPDIR/prs" &

# 3. Clone states (auto-detect workspace vs flat model)
(
  WORKSPACES_DIR="$HOME/code/${PROJECT}-workspaces"
  REPOS_DIR="$HOME/code/${PROJECT}-repos"

  if [ -d "$WORKSPACES_DIR" ]; then
    for dir in "$WORKSPACES_DIR"/${PROJECT}-w*/${PROJECT}-w*-c*/; do
      [ -d "$dir" ] || continue
      CLONE=$(basename "$dir")
      BRANCH=$(git -C "$dir" branch --show-current 2>/dev/null || echo "unknown")
      STATUS=$(git -C "$dir" status --short 2>/dev/null)
      echo "${CLONE}|${BRANCH}|${STATUS}"
    done
  elif [ -d "$REPOS_DIR" ]; then
    for dir in "$REPOS_DIR"/${PROJECT}-[0-9]*/; do
      [ -d "$dir" ] || continue
      CLONE=$(basename "$dir")
      BRANCH=$(git -C "$dir" branch --show-current 2>/dev/null || echo "unknown")
      STATUS=$(git -C "$dir" status --short 2>/dev/null)
      echo "${CLONE}|${BRANCH}|${STATUS}"
    done
  else
    echo "no_clones_found"
  fi
) > "$TMPDIR/clones" &

# 4. Tracking dashboard
(
  python3 ~/.claude/lib/agent_tracking.py list --repo "$PROJECT" 2>/dev/null || echo "unavailable"
) > "$TMPDIR/tracking" &

wait

# --- Output ---
cat <<GATHER_EOF
=== PROJECT ===
name:${PROJECT}
user:${GH_USER}

=== ISSUES ===
$(cat "$TMPDIR/issues")

=== PRS ===
$(cat "$TMPDIR/prs")

=== CLONES ===
$(cat "$TMPDIR/clones")

=== TRACKING ===
$(cat "$TMPDIR/tracking")
GATHER_EOF

lib/xplan-web-review.py

#!/usr/bin/env python3
"""xplan-web-review.py - Local web UI for reviewing an xplan plan directory.

Renders plan.md (and sibling artifacts research.md / decisions.md / naming.md /
reviews/*.md) in the browser with section-level comment support. On Submit,
writes comments to {plan_dir}/comments.json and shuts down. The xplan
orchestrator then reads comments.json and runs a targeted Deepen Mode pass.

Usage:
  xplan-web-review.py <plan_dir> [--port PORT] [--no-open]

Exit codes:
  0  - User submitted comments or accepted the plan as-is
  1  - Server failed to launch (caller should fall back to text walkthrough)
  2  - Invalid plan directory

Fallback signals (caller should use text walkthrough):
  - Env var XPLAN_NO_WEB=1
  - Python exit code 1
  - $DISPLAY unset on Linux (approximate headless check)

Trust model:
  Server binds to 127.0.0.1 only. The rendered markdown (plan.md and siblings)
  is content this same user wrote or that xplan wrote into their own plan dir.
  It is NOT user-submitted content from an untrusted third party. The review
  UI uses marked.parse on this trusted content; inline HTML in markdown will
  render as HTML, same as any local markdown viewer. If a plan contains raw
  HTML, that is by the plan author's intent (diagrams, tables, etc.). We do
  escape text-derived values (section titles, toast messages) into DOM text
  nodes / attributes rather than interpolating them into innerHTML templates.

No runtime dependencies beyond stdlib. HTML uses marked.js via CDN.
"""

from __future__ import annotations

import argparse
import json
import os
import socket
import sys
import threading
import time
import webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import unquote, urlparse


def is_headless() -> bool:
    """Approximate headless check. Returns True if the web UI should not launch."""
    if os.environ.get("XPLAN_NO_WEB") == "1":
        return True
    if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
        return True
    return False


def find_free_port(preferred: int | None = None) -> int:
    if preferred:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            try:
                s.bind(("127.0.0.1", preferred))
                return preferred
            except OSError:
                pass
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(("127.0.0.1", 0))
        return s.getsockname()[1]


def list_plan_files(plan_dir: Path) -> list[dict]:
    """Return a list of {name, path, kind} for every markdown file in the plan dir."""
    files: list[dict] = []
    top_level = ["plan.md", "research.md", "decisions.md", "naming.md", "progress.md"]
    for name in top_level:
        p = plan_dir / name
        if p.is_file():
            files.append({"name": name, "path": name, "kind": "main"})
    reviews_dir = plan_dir / "reviews"
    if reviews_dir.is_dir():
        for p in sorted(reviews_dir.glob("*.md")):
            files.append({"name": f"reviews/{p.name}", "path": f"reviews/{p.name}", "kind": "review"})
    return files


INDEX_HTML = r"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>xplan review</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
  :root {
    --bg: #0e1116;
    --panel: #151a21;
    --border: #2a3038;
    --text: #e6edf3;
    --muted: #8b949e;
    --accent: #58a6ff;
    --accent-2: #3fb950;
    --danger: #f85149;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    font: 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
    background: var(--bg);
    color: var(--text);
    display: grid;
    grid-template-columns: 260px 1fr;
    grid-template-rows: auto 1fr auto;
    grid-template-areas:
      "header header"
      "sidebar main"
      "footer footer";
    min-height: 100vh;
  }
  header {
    grid-area: header;
    padding: 12px 20px;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: 16px;
  }
  header h1 { font-size: 16px; margin: 0; font-weight: 600; }
  header .meta { color: var(--muted); font-size: 13px; }
  nav {
    grid-area: sidebar;
    border-right: 1px solid var(--border);
    padding: 16px 10px;
    overflow-y: auto;
  }
  nav .file-list { display: flex; flex-direction: column; gap: 2px; margin-bottom: 20px; }
  nav .file-list button {
    text-align: left;
    background: transparent;
    color: var(--text);
    border: 1px solid transparent;
    border-radius: 6px;
    padding: 6px 10px;
    cursor: pointer;
    font: inherit;
  }
  nav .file-list button:hover { background: var(--panel); }
  nav .file-list button.active { background: var(--panel); border-color: var(--border); }
  nav .toc { font-size: 13px; color: var(--muted); }
  nav .toc a {
    display: block;
    color: var(--muted);
    text-decoration: none;
    padding: 3px 10px;
    border-radius: 4px;
  }
  nav .toc a:hover { color: var(--text); background: var(--panel); }
  nav .toc a.h3 { padding-left: 20px; font-size: 12px; }
  main {
    grid-area: main;
    padding: 24px 40px 80px 40px;
    overflow-y: auto;
    max-width: 960px;
  }
  main h1, main h2, main h3 { scroll-margin-top: 16px; }
  main h2, main h3 { position: relative; }
  main h2 .comment-btn, main h3 .comment-btn {
    position: absolute;
    left: -32px;
    top: 50%;
    transform: translateY(-50%);
    width: 24px;
    height: 24px;
    padding: 0;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--panel);
    color: var(--muted);
    cursor: pointer;
    font: inherit;
    font-size: 13px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  main h2 .comment-btn:hover, main h3 .comment-btn:hover {
    color: var(--accent);
    border-color: var(--accent);
  }
  main h2 .comment-btn.has-comment, main h3 .comment-btn.has-comment {
    color: var(--accent-2);
    border-color: var(--accent-2);
  }
  .comment-box {
    margin: 8px 0 16px;
    padding: 10px 12px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: 6px;
    display: none;
  }
  .comment-box.open { display: block; }
  .comment-box textarea {
    width: 100%;
    min-height: 70px;
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 8px;
    font: inherit;
    resize: vertical;
  }
  .comment-box .row { display: flex; gap: 8px; margin-top: 8px; }
  .comment-box button {
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 4px 10px;
    cursor: pointer;
    font: inherit;
    font-size: 13px;
  }
  .comment-box button.save { border-color: var(--accent-2); color: var(--accent-2); }
  .comment-box button.discard { border-color: var(--danger); color: var(--danger); }
  pre, code {
    font: 13px ui-monospace, "SF Mono", Consolas, monospace;
  }
  main pre {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 12px;
    overflow-x: auto;
  }
  main code { background: var(--panel); padding: 1px 5px; border-radius: 3px; }
  main pre code { background: transparent; padding: 0; }
  main a { color: var(--accent); }
  main table {
    border-collapse: collapse;
    width: 100%;
    margin: 12px 0;
  }
  main th, main td {
    border: 1px solid var(--border);
    padding: 6px 10px;
    text-align: left;
  }
  main th { background: var(--panel); }
  main blockquote {
    border-left: 3px solid var(--border);
    padding-left: 12px;
    color: var(--muted);
    margin-left: 0;
  }
  footer {
    grid-area: footer;
    border-top: 1px solid var(--border);
    padding: 12px 20px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: var(--panel);
  }
  footer .status { color: var(--muted); font-size: 13px; }
  footer .actions { display: flex; gap: 10px; }
  footer button {
    font: inherit;
    font-size: 14px;
    padding: 8px 18px;
    border-radius: 6px;
    cursor: pointer;
    border: 1px solid var(--border);
    background: var(--bg);
    color: var(--text);
  }
  footer button.primary {
    background: var(--accent);
    color: #001428;
    border-color: var(--accent);
    font-weight: 600;
  }
  footer button.secondary { border-color: var(--accent-2); color: var(--accent-2); }
  .toast {
    position: fixed;
    bottom: 80px;
    left: 50%;
    transform: translateX(-50%);
    background: var(--panel);
    border: 1px solid var(--accent-2);
    color: var(--text);
    padding: 10px 20px;
    border-radius: 8px;
    display: none;
  }
  .toast.show { display: block; }
</style>
</head>
<body>
<header>
  <h1 id="concept-title">xplan review</h1>
  <span class="meta" id="comment-count">0 comments</span>
</header>
<nav>
  <div class="file-list" id="file-list"></div>
  <div class="toc" id="toc"></div>
</nav>
<main id="content">Loading…</main>
<footer>
  <span class="status" id="status">Ready</span>
  <div class="actions">
    <button class="secondary" id="accept-btn">Accept as-is</button>
    <button class="primary" id="submit-btn">Submit for deepening</button>
  </div>
</footer>
<div class="toast" id="toast"></div>
<script>
  const FILES = __FILES_JSON__;
  const CONCEPT = __CONCEPT_JSON__;
  document.getElementById("concept-title").textContent = "xplan review: " + CONCEPT;

  const state = {
    currentFile: FILES[0]?.path || "plan.md",
    comments: {},
  };

  const fileListEl = document.getElementById("file-list");
  const tocEl = document.getElementById("toc");
  const contentEl = document.getElementById("content");
  const countEl = document.getElementById("comment-count");
  const statusEl = document.getElementById("status");
  const toastEl = document.getElementById("toast");

  FILES.forEach(f => {
    const btn = document.createElement("button");
    btn.textContent = f.name;
    btn.dataset.path = f.path;
    btn.onclick = () => loadFile(f.path);
    fileListEl.appendChild(btn);
  });

  function updateFileListActive() {
    fileListEl.querySelectorAll("button").forEach(b => {
      b.classList.toggle("active", b.dataset.path === state.currentFile);
    });
  }

  function toast(msg, ms = 2000) {
    toastEl.textContent = msg;
    toastEl.classList.add("show");
    setTimeout(() => toastEl.classList.remove("show"), ms);
  }

  function updateCount() {
    const total = Object.values(state.comments)
      .flat()
      .filter(c => c && c.text && c.text.trim()).length;
    countEl.textContent = total === 1 ? "1 comment" : `${total} comments`;
  }

  function anchorFor(file, title) {
    return `${file}::${title}`;
  }

  function attachCommentButtons(file) {
    const headings = contentEl.querySelectorAll("h2, h3");
    const tocItems = [];
    headings.forEach(h => {
      const title = h.textContent.trim();
      const level = h.tagName.toLowerCase();
      const anchor = anchorFor(file, title);
      const id = "section-" + anchor.replace(/[^a-zA-Z0-9-]/g, "-");
      h.id = id;
      tocItems.push({ id, title, level });

      const btn = document.createElement("button");
      btn.className = "comment-btn";
      btn.textContent = "+";
      btn.title = `Comment on: ${title}`;
      btn.onclick = (e) => {
        e.preventDefault();
        const box = h.nextElementSibling?.classList?.contains("comment-box")
          ? h.nextElementSibling
          : createCommentBox(h, file, title, anchor);
        box.classList.toggle("open");
        if (box.classList.contains("open")) {
          box.querySelector("textarea").focus();
        }
      };

      const existing = (state.comments[anchor] || [])[0];
      if (existing && existing.text?.trim()) {
        btn.classList.add("has-comment");
        const box = createCommentBox(h, file, title, anchor);
        box.querySelector("textarea").value = existing.text;
      }

      h.prepend(btn);
    });

    tocEl.innerHTML = "";
    tocItems.forEach(item => {
      const a = document.createElement("a");
      a.href = `#${item.id}`;
      a.textContent = item.title;
      if (item.level === "h3") a.className = "h3";
      tocEl.appendChild(a);
    });
  }

  function createCommentBox(heading, file, title, anchor) {
    const box = document.createElement("div");
    box.className = "comment-box";

    const ta = document.createElement("textarea");
    ta.placeholder = `What would you like to change about: ${title}`;

    const row = document.createElement("div");
    row.className = "row";

    const saveBtn = document.createElement("button");
    saveBtn.className = "save";
    saveBtn.textContent = "Save";

    const discardBtn = document.createElement("button");
    discardBtn.className = "discard";
    discardBtn.textContent = "Discard";

    row.appendChild(saveBtn);
    row.appendChild(discardBtn);
    box.appendChild(ta);
    box.appendChild(row);
    heading.insertAdjacentElement("afterend", box);

    saveBtn.onclick = () => {
      const text = ta.value.trim();
      if (!text) {
        delete state.comments[anchor];
      } else {
        state.comments[anchor] = [{
          anchor,
          file,
          section_title: title,
          text,
          ts: new Date().toISOString(),
          status: "pending",
        }];
      }
      const btn = heading.querySelector(".comment-btn");
      btn.classList.toggle("has-comment", !!text);
      box.classList.remove("open");
      updateCount();
      toast(text ? "Comment saved" : "Comment cleared");
    };
    discardBtn.onclick = () => {
      ta.value = "";
      delete state.comments[anchor];
      heading.querySelector(".comment-btn").classList.remove("has-comment");
      box.classList.remove("open");
      updateCount();
    };
    return box;
  }

  async function loadFile(path) {
    state.currentFile = path;
    updateFileListActive();
    statusEl.textContent = `Loading ${path}...`;
    try {
      const resp = await fetch(`/raw/${path}`);
      if (!resp.ok) throw new Error(`${resp.status}`);
      const md = await resp.text();
      // Trust model note: plan.md and siblings are local trusted content this user
      // (or xplan running as this user) wrote into ~/code/plans/{concept}/. Rendering
      // inline HTML in markdown is expected behavior for a local markdown viewer.
      contentEl.innerHTML = marked.parse(md);
      attachCommentButtons(path);
      statusEl.textContent = `Viewing ${path}`;
    } catch (e) {
      contentEl.textContent = `Failed to load ${path}: ${e.message}`;
      statusEl.textContent = "Load error";
    }
  }

  async function submitReview(endpoint, payload) {
    statusEl.textContent = "Submitting...";
    try {
      const resp = await fetch(endpoint, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      if (!resp.ok) throw new Error(`${resp.status}`);
      statusEl.textContent = "Submitted. You can close this tab.";
      document.getElementById("submit-btn").disabled = true;
      document.getElementById("accept-btn").disabled = true;
      toast("Submitted. Return to your terminal.", 5000);
    } catch (e) {
      statusEl.textContent = `Submit failed: ${e.message}`;
      toast(`Submit failed: ${e.message}`, 5000);
    }
  }

  document.getElementById("submit-btn").onclick = () => {
    const flat = Object.values(state.comments).flat().filter(c => c && c.text?.trim());
    if (flat.length === 0) {
      toast("No comments. Use 'Accept as-is' instead.", 3000);
      return;
    }
    submitReview("/submit", { action: "deepen", comments: flat });
  };

  document.getElementById("accept-btn").onclick = () => {
    submitReview("/accept", { action: "accept", comments: [] });
  };

  loadFile(state.currentFile);
</script>
</body>
</html>
"""


class PlanHandler(BaseHTTPRequestHandler):
    plan_dir: Path = Path()
    concept: str = ""
    result_holder: dict = {}

    def log_message(self, format, *args):
        pass

    def _send(self, status: int, body: bytes, content_type: str = "text/plain; charset=utf-8"):
        self.send_response(status)
        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body)

    def _safe_path(self, rel: str) -> Path | None:
        """Resolve rel within plan_dir; return None if escape is attempted."""
        candidate = (self.plan_dir / rel).resolve()
        try:
            candidate.relative_to(self.plan_dir.resolve())
        except ValueError:
            return None
        return candidate

    def do_GET(self):
        parsed = urlparse(self.path)
        path = unquote(parsed.path)

        if path == "/":
            files = list_plan_files(self.plan_dir)
            html = (
                INDEX_HTML
                .replace("__FILES_JSON__", json.dumps(files))
                .replace("__CONCEPT_JSON__", json.dumps(self.concept))
            )
            self._send(200, html.encode("utf-8"), "text/html; charset=utf-8")
            return

        if path.startswith("/raw/"):
            rel = path[len("/raw/"):]
            target = self._safe_path(rel)
            if not target or not target.is_file() or target.suffix != ".md":
                self._send(404, b"not found")
                return
            self._send(200, target.read_bytes(), "text/markdown; charset=utf-8")
            return

        if path == "/health":
            self._send(200, b"ok")
            return

        self._send(404, b"not found")

    def do_POST(self):
        length = int(self.headers.get("Content-Length", "0"))
        body = self.rfile.read(length) if length else b""
        try:
            payload = json.loads(body.decode("utf-8"))
        except Exception:
            self._send(400, b"invalid json")
            return

        parsed = urlparse(self.path)
        path = parsed.path

        if path == "/submit":
            comments = payload.get("comments", [])
            self._write_comments(comments, action="deepen")
            self._send(200, b'{"ok":true}', "application/json")
            self.result_holder["action"] = "deepen"
            self.result_holder["comment_count"] = len(comments)
            self.result_holder["done"] = True
            return

        if path == "/accept":
            self._write_comments([], action="accept")
            self._send(200, b'{"ok":true}', "application/json")
            self.result_holder["action"] = "accept"
            self.result_holder["comment_count"] = 0
            self.result_holder["done"] = True
            return

        self._send(404, b"not found")

    def _write_comments(self, comments: list, action: str):
        out = {
            "action": action,
            "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
            "concept": self.concept,
            "comments": comments,
        }
        (self.plan_dir / "comments.json").write_text(json.dumps(out, indent=2))


def serve(plan_dir: Path, port: int, open_browser: bool) -> dict:
    PlanHandler.plan_dir = plan_dir
    PlanHandler.concept = plan_dir.name
    PlanHandler.result_holder = {"done": False}

    httpd = ThreadingHTTPServer(("127.0.0.1", port), PlanHandler)
    url = f"http://127.0.0.1:{port}/"

    thread = threading.Thread(target=httpd.serve_forever, daemon=True)
    thread.start()
    print(f"xplan-web-review: serving {plan_dir} at {url}", file=sys.stderr)

    if open_browser:
        try:
            webbrowser.open(url)
        except Exception as e:
            print(f"xplan-web-review: could not open browser ({e}); open {url} manually", file=sys.stderr)

    try:
        while not PlanHandler.result_holder.get("done"):
            time.sleep(0.2)
    except KeyboardInterrupt:
        PlanHandler.result_holder["action"] = "interrupted"
    finally:
        time.sleep(0.3)
        httpd.shutdown()
        httpd.server_close()

    return PlanHandler.result_holder


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    parser.add_argument("plan_dir", type=str, help="Path to the plan directory (contains plan.md)")
    parser.add_argument("--port", type=int, default=None, help="Port to bind (default: auto-detect free port)")
    parser.add_argument("--no-open", action="store_true", help="Do not auto-open the browser")
    args = parser.parse_args()

    if is_headless():
        print("xplan-web-review: headless environment (XPLAN_NO_WEB or no $DISPLAY); skipping", file=sys.stderr)
        return 1

    plan_dir = Path(args.plan_dir).expanduser().resolve()
    if not plan_dir.is_dir():
        print(f"xplan-web-review: not a directory: {plan_dir}", file=sys.stderr)
        return 2
    if not (plan_dir / "plan.md").is_file():
        print(f"xplan-web-review: no plan.md in {plan_dir}", file=sys.stderr)
        return 2

    try:
        port = find_free_port(args.port)
    except OSError as e:
        print(f"xplan-web-review: could not bind a port ({e})", file=sys.stderr)
        return 1

    try:
        result = serve(plan_dir, port, open_browser=not args.no_open)
    except Exception as e:
        print(f"xplan-web-review: server error ({e})", file=sys.stderr)
        return 1

    action = result.get("action", "interrupted")
    count = result.get("comment_count", 0)
    print(json.dumps({"action": action, "comment_count": count}))
    return 0


if __name__ == "__main__":
    sys.exit(main())