Spec Engine¶
Overview¶
The spec engine turns a plan file — an XML task list stored in
.claude/plans/ — into executed, gate-checked code. It owns two
distinct concerns: running the pipeline (pipeline.*) and managing
interactive, approval-gated execution with persistent state
(spec.*).
It is not responsible for authoring plan files, running the Socratic brainstorm / decompose / review phases, or displaying output in the Claude Code UI — those belong to the skill layer above it.
The engine matters whenever you need to understand why a run stopped,
how to resume it, or how quality-gate outcomes map to the severity
and gate_score fields on a TaskResult. If you are writing code
that hooks into execution — an on_task_complete callback or a custom
presenter — these are the types and functions you work with directly.
Concepts¶
A spec plan is an XML file under .claude/plans/. When you trigger
execution, the engine works through four concerns in sequence:
- Reading —
read_spec(plan_path)parses the plan file and returns a list ofDecomposedTaskobjects. - Orchestrating —
PipelineOrchestratoriterates those tasks, calls quality gates after each viarun_gates_for_task, and collects results into aPipelineResult. - Gating — each task produces a
TaskResultwith fields likequality_gate_passed,tests_passed,gate_score, and theseverityproperty. The orchestrator stops the run whenquality_gate_passedisFalse; otherwise it consults theon_task_completecallback's returned decision (continue, redo, auto, or stop).tests_passed,gate_score, andseverityare recorded for you to inspect — they don't drive the loop themselves. - State tracking —
SpecStaterecords which task IDs arecompletedand which iscurrent.save_statewrites this back into an HTML comment inside the plan file itself, so the file is the single source of truth.get_pending_tasksfilters the full task list down to whatever hasn't finished, enabling resumption mid-run.
Core data structures¶
| Type | What it represents |
|---|---|
TaskResult |
The outcome of one task: whether it executed, whether quality_gate_passed and tests_passed are satisfied, the gate_score (float), and any error string. The severity property classifies the gate result for display. |
PipelineResult |
The rolled-up outcome across all tasks: spec_path, every TaskResult in tasks, total_cost, duration_ms, and success (true only when all tasks executed and passed gates). |
SpecState |
Durable progress record: plan_path, the list of completed task IDs, the current task ID, and an auto_run flag that controls whether the engine prompts for approval between tasks. |
How the two packages fit together¶
The engine spans two packages, each with a distinct role:
pipelineowns execution.PipelineOrchestratorreads an XML plan file, runs tasks one at a time, and evaluates quality gates after each.read_spec()parses a plan file intoDecomposedTaskobjects.TaskResultandPipelineResultcarry the outcome data.specowns state and presentation.SpecStatetracks progress;load_state/save_state/clear_statemanage the embedded state comment; the presenter functions render engine output for display; andexecute_with_approval(inspec.runner) wraps the orchestrator with a per-task approval loop.
State lifecycle¶
load_state(plan_path) # returns SpecState | None
│
▼
get_pending_tasks(tasks, state) # filters out completed IDs
│
▼
[execute tasks, update state.completed after each]
│
├─ save_state(state) # persists progress into the plan file
│
└─ clear_state(plan_path) # removes state when the run finishes
find_resumable_plans(plans_dir) scans .claude/plans/ (the default)
for any plan file that still carries a SpecState comment, giving you
a list of interrupted runs you can pick back up.
Design & extension¶
Design decisions¶
- State embedded in the plan file, not a sidecar.
save_stateandload_stateread and writeSpecStateas an HTML comment block inside the.mdplan file rather than a separate JSON file. This keeps the plan and its progress co-located — the plan file is the single artifact needed to resume. The trade-off is that plan files are mutable after authoring, so the state block is versioned (schema_version) to survive format changes. skip_gates,skip_tests,skip_simplifyas constructor flags, not subclasses. The three concerns are tightly coupled inside a single task execution; separating them into composable strategies would add indirection without simplifying the gate logic. The flags propagate transparently throughexecute_with_approval.SpecState.completedis a list of task IDs, not a count.get_pending_tasksfilters by ID, so tasks can be skipped non-linearly viaskip_task_idsinrun_allwithout corrupting resume behavior. A simple counter could not support this.
Extension points¶
- Add a new quality-gate check: extend
PipelineOrchestrator.run_gates_for_task(). It returns aTaskResult; add fields to theTaskResultdataclass inpipeline/models.pyif the new gate produces data callers must inspect. - Hook into task completion: pass an async
on_task_completecallback torun_all(). It is awaited with(task, result)after each task and may return a decision string ("redo","auto","stop") — the intended integration point for custom reporting, logging, or approval UIs, without modifying the orchestrator. - Resume or skip tasks selectively: pass
skip_task_ids: set[str]torun_all(). Task IDs come from theDecomposedTaskobjects returned byread_spec(). - Add a new presenter format: add a function alongside the
existing presenters in
spec/presenter.py. Presenters are pure functions overDecomposedTaskandTaskResult. - Find or restore interrupted runs: use
find_resumable_plans(plans_dir)to listSpecStateobjects for incomplete plans, then passplan_pathtoexecute_with_approval()to resume.