Spec-loop runner
July 8, 2026 · View on GitHub
title: Spec-loop runner status: experimental kind: feature mode: infra source: > tools/spec-loop/README.md, tools/spec-loop/loop.sh, tools/spec-loop/lib.sh, tools/spec-loop/tests/test_runner_fixtures.sh, tools/spec-loop/PROMPT_plan.md, tools/spec-loop/PROMPT_build.md, tools/spec-loop/PROMPT_update.md, tools/spec-loop/PROMPT_consolidate.md, and docs/spec-driven-development.md. acceptance:
- The loop supports plan, build, update, and consolidate beats with one fresh agent context per iteration.
- Build and update beats never commit to the integration base; they create reviewable local branches and stop before push or PR creation.
- Open PRs and local work-item branches are passed into plan/build prompts as in-flight work so the loop does not duplicate already-built items.
- The headless-agent harness contract is explicit for Claude Code, Codex, Cursor, Gemini CLI, OpenCode, and Kiro.
- The update marker
tools/spec-loop/.last-syncis owned by the runner, not by the prompt.
Spec-loop runner
What it does
The spec-loop runner is the framework's local, review-first development loop. It runs a headless agent CLI against fixed prompts, one iteration at a time, to compare specs with the working tree, produce planned changes, back-fill specs after normal contributions, or consolidate a long implementation plan.
The loop is not an autonomous merge system. Its boundary is local branch creation: it may edit, validate, and commit locally, but it never pushes and never opens a pull request.
Where it lives
tools/spec-loop/loop.sh— Bash runner for the four beats.tools/spec-loop/lib.sh— deterministic prompt assembly, harness command rendering, agent launch, and.last-syncmarker helpers used by the runner and fixture tests.tools/spec-loop/tests/test_runner_fixtures.sh— deterministic fixture tests for prompt assembly, harness command construction, and.last-syncmarker helpers; executed byspec-validate.tools/spec-loop/PROMPT_plan.md— gap-analysis prompt that rewritesIMPLEMENTATION_PLAN.md.tools/spec-loop/PROMPT_build.md— implementation prompt for exactly one work item.tools/spec-loop/PROMPT_update.md— spec back-fill prompt for functionality that landed outside the loop.tools/spec-loop/PROMPT_consolidate.md— plan-size reduction prompt.tools/spec-loop/AGENTS.md— loop-specific operational context: repository map, validation commands, branch rules, hard limits, and commit rules.tools/spec-loop/README.md— operator quickstart.docs/spec-driven-development.md— full explanation of the loop's posture and lifecycle.
Behaviour & contract
- Four beats, one mechanism.
plancompares specs to code and updates the implementation plan without committing.buildimplements one uncovered work item.updatecompares code to specs and back-fills the spec directory.consolidateshrinks the implementation plan without dropping planned work. - One work item, one branch, one PR. Build work creates a bare
<slug>branch offSPEC_LOOP_BASEand commits exactly one work item there. Update work creates a uniquely namedsync-specs-<timestamp>branch. Consolidate commits only the plan file on the control branch. - No remote state changes. The runner and prompts forbid
git pushandgh pr create; each successful build/update beat prints the human-run push andgh pr create --webcommands instead. - In-flight duplicate guard. Before each relevant iteration, the runner appends open PR context and local work-item branch context to the prompt. Plan/build treat both as in-flight work. This matters because a loop-built branch may be local-only and invisible to GitHub.
- Control branch vs integration base.
TOOLING_REFis captured before checkout. When the integration base does not carry the spec-loop files, the runner tells the agent to read prompts/specs/plan from the control branch withgit show, while implementing product changes on the work branch. - Update marker ownership.
tools/spec-loop/.last-syncrecords the base SHA last synced by the update beat. The runner reads it to append incremental-scope guidance, then amends or creates a marker commit after the agent finishes. Prompts must not instruct the agent to edit the marker. - Plan-size hysteresis. Build mode switches to one consolidate pass
when
IMPLEMENTATION_PLAN.mdexceedsSPEC_LOOP_PLAN_MAX, then builds even if planned work alone keeps the file over the threshold. The latch resets once the plan drops below the threshold.
Headless harness contract
Every supported harness must provide the same loop-level behaviour: run one non-interactive agent iteration in the repository root, receive the assembled prompt, allow local edits/validation/commits under the external sandbox, and stop without pushing or opening a PR.
| Harness | Prompt transport | Cwd contract | Unattended flag | Model override | Output mode | Extra denial |
|---|---|---|---|---|---|---|
| Claude Code | stdin to claude -p | launched from repo root | --dangerously-skip-permissions | --model | --output-format | --disallowedTools denies push and gh |
| Codex | stdin to codex exec - | --cd "$ROOT" | --dangerously-bypass-approvals-and-sandbox | --model | --json for stream JSON | external sandbox and exec policy |
| Cursor | positional prompt to cursor agent --print or cursor-agent --print | --workspace "$ROOT" | --force --trust | --model | --output-format | external sandbox and Cursor policy |
| Gemini CLI | --prompt "<prompt>" | launched from repo root | --yolo | --model | default CLI output | external sandbox and Gemini policy |
| OpenCode | positional prompt to opencode run | launched from repo root | --auto | --model | --format json for stream JSON | external sandbox and OpenCode policy |
| Kiro | positional prompt to kiro-cli chat | launched from repo root | --no-interactive | agent config (.kiro/agents) | plain text only | external sandbox and the agent-guard --kiro hook |
SPEC_LOOP_AGENT chooses the CLI. SPEC_LOOP_HARNESS chooses the
invocation convention and defaults from the agent basename. Adding a new
harness means extending this matrix, documenting the safety boundary, and
updating loop.sh in the same change.
Out of scope
- Auto-pushing, auto-opening PRs, auto-merging, or changing remote state.
- Replacing the project sandbox or adding new filesystem/network permissions.
- Teaching individual skills how to do their domain work; those contracts live in the corresponding functional specs.
- Editing
docs/rfcs/; RFCs remain the separate governance layer.
Acceptance criteria
loop.shacceptsplan,build,update, andconsolidate, plus a bare numeric build count, and rejects unknown modes or non-numeric iteration counts.- Build/update iterations check out
SPEC_LOOP_BASE, create a non-base branch, and stop if a commit lands on the base. - Plan/build prompts receive both open PR context and local work-item branch context.
- Update prompts receive incremental-scope guidance from
.last-syncwhen present, but the runner remains the only writer of.last-sync. - Claude Code, Codex, Cursor, Gemini CLI, OpenCode, and Kiro are documented in
the headless harness matrix and implemented in
loop.sh. - Operator docs and prompts agree on branch naming: build uses bare
<slug>branches; update usessync-specs-<timestamp>branches. - Security docs describe unattended agent flags as harness-specific agent-level bypasses that must run under the external sandbox.
Validation
bash -n tools/spec-loop/loop.sh
shellcheck tools/spec-loop/loop.sh
shellcheck tools/spec-loop/lib.sh tools/spec-loop/tests/test_runner_fixtures.sh
uv run --project tools/spec-validator --group dev spec-validate
Known gaps
- The non-Claude harnesses rely on external policy/config plus the OS sandbox for push/PR denial; only Claude has a per-invocation hard-deny flag in the current runner.
- The update beat's incremental-scope mapping is path-based; it does not yet map changed files to likely spec topics with a deterministic helper.