Appearance
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
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
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
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)
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
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
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
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
bash
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
Each run appends one record to ~/.claude/loop-telemetry.jsonl (same JSONL convention as the warp-drive token log):
json
{
"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
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:
bash
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
The primitive is a provisionable registry item — skills/loop-primitive — with orchestrator metadata (applies_to, recommended_for: [development, testing], category: process). Add it via:
bash
cdprov add skills/loop-primitive # or /provision, or the orchestrator interviewSee 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