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

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:

ConcernReused from
Manifest validationscripts/checks/schema-validator.js
Cost telemetryscripts/warp-drive/token-report.js (shared cost model + token log)
Guardrails / blockable haltswarp-drive's budget/blockable-event vocabulary (budget_exceeded as a mandatory human checkpoint)
Loop-state observability, iteration feedback, compositionwarp-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 guardrails

1. The manifest

A loop manifest is a single JSON object validated against schemas/loop-manifest.schema.json.

FieldRequiredDescription
namenoStable kebab-case id (defaults to the filename). Surfaced in telemetry.
goalyesOne- or two-sentence objective. Injected via {goal}.
agent.commandyesShell command that invokes the agent, e.g. claude -p {prompt}.
agent.promptyesPrompt template rendered each iteration.
evaluator.commandyesCheck run after each iteration. Exit 0 = pass.
stop_condition.typeyesevaluator_pass or output_matches.
stop_condition.patternfor output_matchesRegex tested against evaluator output.
guardrails.max_iterationsyesHard iteration ceiling. An unbounded loop is never allowed.
guardrails.max_cost_usdnoDollar budget across iterations (estimated via the reused cost model).
guardrails.max_secondsnoWall-clock budget across iterations.
guardrails.hitl_checkpointnotrue = 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:

PlaceholderValue
{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 matches pattern. 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]
FlagEffect
--cwd <dir>Working directory for the agent + evaluator (default: cwd).
--dry-runRender prompts/commands and run the evaluator, but do not invoke the agent.
--non-interactiveTreat any HITL checkpoint as a blockable halt instead of prompting.
--jsonEmit the final telemetry record as JSON.
--quietSuppress 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/:

TemplateGoalEvaluator
fix-until-tests-pass.jsonMake the test suite pass without weakening testsnpm test
refactor-until-lint-clean.jsonMake the linter clean without disabling rulesnpm 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.json

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:

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

See also