Declarative Loop Primitive
A project-ownable, reusable construct for expressing arbitrary “do X until condition Y” loops — fix-until-tests-pass, refactor-until-lint-clean, and anything else shaped like iterate an agent against a check until it’s satisfied.
Introduced in #424 (part of #422).
Why it exists
Section titled “Why it exists”Every loop in BoB used to be hardcoded to one job: warp-drive and autoloop
both bake in the dev cycle, and the harness /loop skill is runtime-level. There
was no declarative, version-controlled way to say “run this agent against this
check until it passes, with these guardrails.” The loop primitive fills exactly
that gap — and only that gap.
Per the BoB Prime Directive, it is built on reuse, not reinvention:
| Concern | Reused from |
|---|---|
| Manifest validation | scripts/checks/schema-validator.js |
| Cost telemetry | scripts/warp-drive/token-report.js (shared cost model + token log) |
| Guardrails / blockable halts | warp-drive’s budget/blockable-event vocabulary (budget_exceeded as a mandatory human checkpoint) |
| Loop-state observability, iteration feedback, composition | warp-drive state machine, context injection, integration-branch streams + cdfork |
The last row is deliberately out of scope — those parts of #422 are already delivered by warp-drive and are not rebuilt here.
The three pieces
Section titled “The three pieces”manifest (what to do) ──▶ runner (scripts/loop/run.js) ──▶ telemetry goal render prompt iterations agent invoke agent stop_reason evaluator run evaluator cost (reused) stop_condition feed output forward guardrails enforce guardrails1. The manifest
Section titled “1. The manifest”A loop manifest is a single JSON object validated against
schemas/loop-manifest.schema.json.
| Field | Required | Description |
|---|---|---|
name |
no | Stable kebab-case id (defaults to the filename). Surfaced in telemetry. |
goal |
yes | One- or two-sentence objective. Injected via {goal}. |
agent.command |
yes | Shell command that invokes the agent, e.g. claude -p {prompt}. |
agent.prompt |
yes | Prompt template rendered each iteration. |
evaluator.command |
yes | Check run after each iteration. Exit 0 = pass. |
stop_condition.type |
yes | evaluator_pass or output_matches. |
stop_condition.pattern |
for output_matches |
Regex tested against evaluator output. |
guardrails.max_iterations |
yes | Hard iteration ceiling. An unbounded loop is never allowed. |
guardrails.max_cost_usd |
no | Dollar budget across iterations (estimated via the reused cost model). |
guardrails.max_seconds |
no | Wall-clock budget across iterations. |
guardrails.hitl_checkpoint |
no | true = pause every iteration; integer N = pause every N; absent = never. |
Prompt placeholders (feed-forward)
Section titled “Prompt placeholders (feed-forward)”The prompt template is re-rendered every iteration with the prior iteration’s results fed forward:
| Placeholder | Value |
|---|---|
{goal} |
The manifest goal. |
{prior_output} |
The previous iteration’s agent stdout (empty on iteration 1). |
{evaluator_output} |
The previous iteration’s evaluator stdout+stderr (empty on iteration 1). |
{iteration} |
The 1-based iteration index. |
Unknown placeholders are left intact, so a typo is visible rather than silently blanked.
Agent invocation
Section titled “Agent invocation”If agent.command contains {prompt}, the rendered prompt is shell-quoted and
substituted there. Otherwise the rendered prompt is piped to the command on
stdin. The command’s stdout is captured as the iteration output and fed
forward.
Stop conditions
Section titled “Stop conditions”evaluator_pass(default): stop when the evaluator exits 0.output_matches: stop when the evaluator exits 0 and its combined output matchespattern. Use when exit code alone is insufficient.
Guardrails — blockable halts
Section titled “Guardrails — blockable halts”When the stop condition is never met, the loop halts on the first guardrail to
trip. max_iterations, max_cost_usd (budget_exceeded), max_seconds
(time_exceeded) and a non-interactive hitl_checkpoint all halt with
blockable: true — a mandatory human checkpoint, matching warp-drive’s
budget_exceeded semantics. A blockable halt means: review before re-running.
2. The runner
Section titled “2. The runner”node ~/.claude/scripts/loop/run.js <manifest.json> [options]| Flag | Effect |
|---|---|
--cwd <dir> |
Working directory for the agent + evaluator (default: cwd). |
--dry-run |
Render prompts/commands and run the evaluator, but do not invoke the agent. |
--non-interactive |
Treat any HITL checkpoint as a blockable halt instead of prompting. |
--json |
Emit the final telemetry record as JSON. |
--quiet |
Suppress per-iteration progress. |
Exit codes: 0 = goal met · 1 = halted by guardrail · 2 = usage/validation error.
Telemetry
Section titled “Telemetry”Each run appends one record to ~/.claude/loop-telemetry.jsonl (same JSONL
convention as the warp-drive token log):
{ "loop": "fix-until-tests-pass", "iterations": 3, "stop_reason": "goal_met", "blockable": false, "success": true, "estimated_cost_usd": 0.42, "elapsed_seconds": 88.1, "ended_at": "2026-06-22T23:00:00.000Z"}Cost is estimated by reusing token-report.js’s shared cost model over the
token log — agents that don’t write to the token log simply contribute $0.
3. The templates
Section titled “3. The templates”Two ready-to-use templates ship under templates/loops/:
| Template | Goal | Evaluator |
|---|---|---|
fix-until-tests-pass.json |
Make the test suite pass without weakening tests | npm test |
refactor-until-lint-clean.json |
Make the linter clean without disabling rules | npm run lint |
Copy a template into a project, adjust the evaluator.command to the project’s
real test/lint command, tune the guardrails, and run it:
cp ~/.claude/templates/loops/fix-until-tests-pass.json /tmp/fix.json# edit evaluator.command if the project uses something other than `npm test`node ~/.claude/scripts/loop/run.js /tmp/fix.jsonProvisioning
Section titled “Provisioning”The primitive is a provisionable registry item — skills/loop-primitive — with
orchestrator metadata (applies_to, recommended_for: [development, testing],
category: process). Add it via:
cdprov add skills/loop-primitive # or /provision, or the orchestrator interviewSee also
Section titled “See also”schemas/loop-manifest.schema.json— the contract- warp-drive.md — the hardcoded dev-cycle loop this primitive generalizes
- token-monitoring.md — the telemetry path reused for cost