blob: 197deb33b36d179c1573bf972efbc0f488df6953 [file] [view]
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->
---
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.
| 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
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
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.