Spec-driven development with approval loops
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.
Notes & tips
- Depend only on the public API.
pipelineexportsPipelineOrchestrator,PipelineResult,TaskResult, andread_spec.specexportsSpecState,clear_state,find_resumable_plans,format_progress_bar,get_pending_tasks,load_state,present_task_detail,present_task_result,present_tasks, andsave_state.execute_with_approvallives inattune.spec.runner. Private helpers can change without notice. - Presenter functions are pure.
present_tasks,present_task_detail,present_task_result, andformat_progress_baracceptpipelinedata types, hold no state, and have no coupling to the pipeline layer — safe to call anywhere. - Prefer
skip_task_idsoverclear_statefor re-runs. Clearing state is irreversible mid-run; skipping completed tasks preserves yourcompletedlist and keepstotal_cost/duration_msaccurate.