name: feature-plan-loop description: Use when modifying the plan loop, debugging plan stages, changing clarification flow, scope isolation, or understanding how spectre-build --plan works end-to-end user-invocable: false
Plan Loop (--plan)
Trigger: plan loop, --plan, plan pipeline, planning loop, --scope-name, plan stages, clarifications, plan resume, scope to manifest, scope isolation, scope slug, run_plan_pipeline, create_plan_pipeline Confidence: high Created: 2026-02-17 Updated: 2026-02-19 Version: 3
What is the Plan Loop?
The --plan flag on spectre-build runs a multi-stage autonomous planning loop that transforms scope documents into a build-ready manifest. It decomposes the interactive /spectre:plan workflow into 7 independent stages — each reading and writing files — leveraging the existing pipeline executor with zero changes to the core engine.
Key insight: The plan loop does NOT use --tasks (tasks are generated, not provided). It takes --context scope docs as input and produces a .md manifest with YAML frontmatter as output.
Why Use It?
| Problem | How Plan Loop Solves It |
|---|---|
| 20-40 min manual planning orchestration before builds | Runs autonomously from scope docs to build manifest |
Manual multi-step /spectre:* command sequencing |
7 stages execute in sequence, file-mediated |
| Complexity misjudgment (over/under-planning) | Assess stage scores complexity, routes LIGHT/STANDARD/COMPREHENSIVE |
| Plans that are over-engineered | Plan review stage catches unnecessary abstractions |
| Scope gaps discovered late | Req validate cross-references scope vs plan/tasks |
| Clarification needs block the entire pipeline | Pause/resume: saves session, user edits file, resumes |
User Flows
Flow 1: Normal Planning (scope → manifest)
spectre-build --plan --context scope.md design_notes.md --max-iterations 10
Pipeline runs 6 stages autonomously:
research → assess → [create_plan] → create_tasks → plan_review → req_validate
Output: docs/tasks/{branch}/{scope_slug}/build.md manifest, then run spectre-build build.md.
Flow 2: LIGHT Complexity (skips plan generation)
research → assess(LIGHT) → create_tasks → plan_review → req_validate
Assess determines the task is simple enough to skip create_plan entirely.
Flow 3: Clarification Pause + Resume
# Pipeline pauses:
spectre-build --plan --context scope.md
# → req_validate finds gaps
# → writes scope_clarifications.md
# → saves session, exits with code 0
# → prints: "Edit: .../scope_clarifications.md"
# User edits clarifications file, then:
spectre-build resume
# → detects plan=True in session
# → runs update_docs stage only
# → incorporates answers, writes manifest
Technical Design
CLI Routing (cli.py:main())
parse_args()
├─ --plan → run_plan_pipeline() ← checked FIRST, before --validate
├─ --pipeline → run_pipeline()
├─ --validate (no --pipeline) → run_default_pipeline()
└─ no flags → run_build_validate_cycle()
--plan requires --context (errors without it). Does NOT require --tasks.
Pipeline Structure (7 stages)
Normal flow
══════════
research → assess ─── LIGHT ──────────────────→ create_tasks → plan_review → req_validate
├── STANDARD ───────→ create_plan ─→ create_tasks → plan_review → req_validate
└── COMPREHENSIVE ──→ create_plan ─→ create_tasks → plan_review → req_validate
Resume flow (separate pipeline config)
════════════════════════════════════
update_docs → PLAN_READY (end)
Stage Definitions
| Stage | Completion | Signals | Max Iter | Transitions |
|---|---|---|---|---|
| research | JSON | RESEARCH_COMPLETE |
1 | → assess |
| assess | JSON | LIGHT, STANDARD, COMPREHENSIVE |
1 | LIGHT→create_tasks, STANDARD/COMPREHENSIVE→create_plan |
| create_plan | JSON | PLAN_COMPLETE |
1 | → create_tasks |
| create_tasks | JSON | TASKS_COMPLETE |
1 | → plan_review |
| plan_review | JSON | REVIEW_COMPLETE |
1 | → req_validate |
| req_validate | JSON | PLAN_VALIDATED, CLARIFICATIONS_NEEDED |
1 | end pipeline |
| update_docs | JSON | PLAN_READY |
1 | end pipeline (resume only) |
All stages use JsonCompletion with signal_field="status". End signals: ["PLAN_VALIDATED", "PLAN_READY"].
run_plan_pipeline() Function (cli.py)
def run_plan_pipeline(
context_files: list[str],
max_iterations: int,
agent: str = "claude",
output_dir: str | None = None, # Default: docs/tasks/{branch}/{scope_slug}
scope_name: str | None = None, # Scope slug for directory isolation
resume_stage: str | None = None, # Set to "update_docs" for resume
resume_context: dict | None = None, # Preserved context from session
) -> tuple[int, int, str]:
Key behaviors:
- Output dir: Auto-creates
docs/tasks/{branch}/{scope_slug}/,specs/,clarifications/subdirs. Each scope gets an isolated directory to prevent collisions between planning cycles. - Pipeline selection:
resume_stage→create_plan_resume_pipeline(), else →create_plan_pipeline() - Context dict: Built fresh or restored from
resume_context - CLARIFICATIONS_NEEDED handling: Saves session, prints instructions, returns exit 0
- PLAN_VALIDATED/PLAN_READY: Prints manifest path and
spectre-buildcommand
Context Variables (shared across stages)
context = {
"context_files": "- `scope.md`\n- `notes.md`", # Input scope docs
"output_dir": "/abs/path/docs/tasks/main/my_scope", # Artifact root (scope-isolated)
"task_context_path": ".../task_context.md", # Written by research
"plan_path": ".../specs/plan.md", # Written by create_plan
"tasks_path": ".../specs/tasks.md", # Written by create_tasks
"clarifications_path": "", # Set by req_validate if gaps
"clarification_answers": "", # Injected by hook for update_docs
"manifest_path": "", # Set by req_validate/update_docs
"depth": "standard", # Set by assess artifacts
"tier": "STANDARD", # Set by assess artifacts
}
Planning Hooks (hooks.py)
plan_before_stage() (hooks.py:118):
create_plan: Defaultsdepthto"standard"if missingupdate_docs: Reads clarifications file, injects content asclarification_answers
plan_after_stage() (hooks.py:147):
assess: Ensuresdepthandtierfrom artifacts flow into contextreq_validate+ CLARIFICATIONS_NEEDED: Storesclarifications_pathin context
Stats (stats.py)
plan_loops: int = 0field onBuildStats(stats.py:64)create_plan_event_handler(stats)(stats.py:220) — factory returning callback that incrementsplan_loopson everyStageCompletedEvent- Dashboard shows
PLAN LOOPS: Nwhenplan_loops > 0(stats.py:205-206)
Session Persistence (cli.py:30-67)
Planning adds 5 fields to session JSON:
save_session(
tasks_file="", # Empty for --plan
plan=True, # Planning mode flag
plan_output_dir=output_dir, # Artifact directory (scope-isolated)
plan_context=context, # Full context dict for resume
plan_clarifications_path=clarif_path, # Path to clarifications file
plan_scope_name=scope_name, # Scope slug for directory isolation
plan_auto_build=auto_build, # Chain to build after plan completes
...
)
format_session_summary() shows "Mode: Planning", output dir, and clarifications path.
Resume Flow (cli.py:836-856)
spectre-build resume
→ load_session()
→ session.get("plan") == True
→ save_session() (timestamp update)
→ run_plan_pipeline(
resume_stage="update_docs",
resume_context=session.get("plan_context"),
output_dir=session.get("plan_output_dir"),
)
→ create_plan_resume_pipeline() (single update_docs stage)
→ executor.run()
→ notification
Tool Filtering
- Research stage:
PLAN_RESEARCH_DENIED_TOOLS— allowsWebSearch/WebFetch(for external API docs) - All other stages:
PLAN_DENIED_TOOLS— same as build loop restrictions
Defined in loader.py:395-410.
Pipeline Factories (loader.py)
create_plan_pipeline()(loader.py:413) — 7-stage config,start_stage="research",end_signals=["PLAN_VALIDATED", "PLAN_READY"]create_plan_resume_pipeline()(loader.py:523) — singleupdate_docsstage,start_stage="update_docs",end_signals=["PLAN_READY"]
Resume uses a separate pipeline config (not a start-stage offset) to avoid modifying the executor.
Key Files
| File | Purpose | When to Modify |
|---|---|---|
build-loop/src/build_loop/cli.py |
--plan flag, run_plan_pipeline(), session save/load/resume |
Adding plan CLI options, changing plan routing |
build-loop/src/build_loop/pipeline/loader.py |
create_plan_pipeline(), create_plan_resume_pipeline(), denied tools lists |
Adding/removing plan stages, changing transitions |
build-loop/src/build_loop/hooks.py |
plan_before_stage(), plan_after_stage() |
Changing what context flows between plan stages |
build-loop/src/build_loop/stats.py |
plan_loops field, create_plan_event_handler() |
Adding plan-specific metrics |
build-loop/src/build_loop/prompts/planning/*.md |
7 prompt templates (research, assess, create_plan, create_tasks, plan_review, req_validate, update_docs) | Changing agent instructions per stage |
Common Tasks
Change Planning Stage Behavior
- Edit the prompt in
prompts/planning/{stage}.md - If changing signals, update
complete_statusesincreate_plan_pipeline()(loader.py) - If changing transitions, update the
transitionsdict for that stage
Add a New Planning Stage
- Create prompt template in
prompts/planning/ - Add
StageConfigtocreate_plan_pipeline()in loader.py - Wire transitions from/to adjacent stages
- Add hook logic in
hooks.pyif inter-stage context injection needed
Debug Clarification Flow
Trace: req_validate emits CLARIFICATIONS_NEEDED → plan_after_stage() stores path → pipeline ends → run_plan_pipeline() detects signal at cli.py:756 → save_session() → user edits file → resume → plan_before_stage("update_docs") reads file at hooks.py:136
Gotchas
- Scope-based directory isolation: Each planning cycle creates an isolated directory
docs/tasks/{branch}/{scope_slug}/. The scope slug is derived from the first context file (strippingscope_prefix) or set explicitly via--scope-name. This prevents planning cycles from colliding when running multiple plan/build/ship cycles on the same branch. Theupdate_docs.mdprompt has a scope mismatch guard that detects when plan/tasks belong to a different scope. --planwithout--contextexits immediately: Error at cli.py. No interactive fallback.- Resume uses a separate pipeline config:
create_plan_resume_pipeline(), not a start-stage param on the main pipeline. The executor always starts atstart_stage. - Context dict is serialized to session JSON: Must be JSON-serializable (no Path objects). All paths are strings.
- CLARIFICATIONS_NEEDED is NOT in
end_signals: The pipeline ends because there's no transition for it (empty transitions dict on req_validate). The CLI detects it by checkingstate.stage_history[-1][1]. - Research stage tool filtering differs: Only stage that allows WebSearch/WebFetch. Defined by
PLAN_RESEARCH_DENIED_TOOLSvsPLAN_DENIED_TOOLS. - All stages max_iterations=1: Each planning stage runs once. No looping within stages.
- Artifacts flow via executor:
context.update(result.artifacts)happens automatically in executor after each stage. Hooks handle edge cases (defaults, file reading). - Executor copies context dict:
executor.run()createsdict(self.initial_context)— a copy. Hook mutations to context during pipeline execution don't propagate back to the caller's original dict. For values that must survive session save (likeclarifications_path), the caller must sync them fromstate.global_artifactsafterexecutor.run()returns. plan_auto_buildpersists across resume: The auto-build preference (from--buildflag or interactive "Auto-start build?" prompt) is saved to session asplan_auto_buildand checked inrun_resume()to chain torun_manifest()after plan completes.