Skip to content

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).

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.

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 guardrails

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.

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.

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.

  • evaluator_pass (default): stop when the evaluator exits 0.
  • output_matches: stop when the evaluator exits 0 and its combined output matches pattern. Use when exit code alone is insufficient.

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.

Terminal window
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.

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.

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:

Terminal window
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.json

The primitive is a provisionable registry item — skills/loop-primitive — with orchestrator metadata (applies_to, recommended_for: [development, testing], category: process). Add it via:

Terminal window
cdprov add skills/loop-primitive # or /provision, or the orchestrator interview