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-sync is 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-sync marker 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-sync marker helpers; executed by spec-validate.
  • tools/spec-loop/PROMPT_plan.md — gap-analysis prompt that rewrites IMPLEMENTATION_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. plan compares specs to code and updates the implementation plan without committing. build implements one uncovered work item. update compares code to specs and back-fills the spec directory. consolidate shrinks the implementation plan without dropping planned work.
  • One work item, one branch, one PR. Build work creates a bare <slug> branch off SPEC_LOOP_BASE and commits exactly one work item there. Update work creates a uniquely named sync-specs-<timestamp> branch. Consolidate commits only the plan file on the control branch.
  • No remote state changes. The runner and prompts forbid git push and gh pr create; each successful build/update beat prints the human-run push and gh pr create --web commands 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_REF is 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 with git show, while implementing product changes on the work branch.
  • Update marker ownership. tools/spec-loop/.last-sync records 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.md exceeds SPEC_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.

HarnessPrompt transportCwd contractUnattended flagModel overrideOutput modeExtra denial
Claude Codestdin to claude -plaunched from repo root--dangerously-skip-permissions--model--output-format--disallowedTools denies push and gh
Codexstdin to codex exec ---cd "$ROOT"--dangerously-bypass-approvals-and-sandbox--model--json for stream JSONexternal sandbox and exec policy
Cursorpositional prompt to cursor agent --print or cursor-agent --print--workspace "$ROOT"--force --trust--model--output-formatexternal sandbox and Cursor policy
Gemini CLI--prompt "<prompt>"launched from repo root--yolo--modeldefault CLI outputexternal sandbox and Gemini policy
OpenCodepositional prompt to opencode runlaunched from repo root--auto--model--format json for stream JSONexternal sandbox and OpenCode policy
Kiropositional prompt to kiro-cli chatlaunched from repo root--no-interactiveagent config (.kiro/agents)plain text onlyexternal 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

  1. loop.sh accepts plan, build, update, and consolidate, plus a bare numeric build count, and rejects unknown modes or non-numeric iteration counts.
  2. Build/update iterations check out SPEC_LOOP_BASE, create a non-base branch, and stop if a commit lands on the base.
  3. Plan/build prompts receive both open PR context and local work-item branch context.
  4. Update prompts receive incremental-scope guidance from .last-sync when present, but the runner remains the only writer of .last-sync.
  5. Claude Code, Codex, Cursor, Gemini CLI, OpenCode, and Kiro are documented in the headless harness matrix and implemented in loop.sh.
  6. Operator docs and prompts agree on branch naming: build uses bare <slug> branches; update uses sync-specs-<timestamp> branches.
  7. 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.