The until pattern
Work toward a goal expressed as an executable acceptance check: run the check, and while it is red, plan the gap it reports, build that plan, and start over. Ends when the check goes green, or when the gap planner reports there is no actionable gap left.
[header] pattern_language_compatibility = "3" entry = "until" include = ["build.toml"] merge = "merge" on_stop = "write_handover" description = """ Work toward a goal expressed as an executable acceptance check: run the check, and while it is red, plan the gap it reports, build that plan, and start over. Ends when the check goes green, or when the gap planner reports there is no actionable gap left. """ [[header.cli_flags]] flag = "until" value_parameter = "until_source" [patterns.until] [[patterns.until.parameters]] name = "until_source" kind = "path" declared_plan_source = true description = "The goal prose an absent acceptance check is authored from." [[patterns.until.parameters]] name = "check" kind = "path" default = "bin/goal-check" description = "The acceptance check record path: exit zero is achieved and non-zero is a gap." [[patterns.until.parameters]] name = "max_cycles" kind = "text" default = "unbounded" description = "Optional operator ceiling for completed build cycles; `unbounded` means no ceiling." [[patterns.until.parameters]] name = "merge_attempt_ceiling" kind = "text" default = "2" description = "The finish-time merge repair ceiling passed to the bundled merge pattern." [[patterns.until.parameters]] name = "troubleshoot_prompt" kind = "prompt-id" default = "troubleshoot" description = "The troubleshoot pass's prompt, used by build cycles and finish-time merge repair."
The entry pattern exists only to seed completed_cycles at zero.
[[patterns.until.steps]] name = "start_until_loop" type = "call" pattern = "until_loop" [patterns.until.steps.parameters] until_source = "{{until_source}}" check = "{{check}}" max_cycles = "{{max_cycles}}" completed_cycles = "0" [patterns.until_loop] [[patterns.until_loop.parameters]] name = "until_source" kind = "path" description = "The goal prose an absent acceptance check is authored from." [[patterns.until_loop.parameters]] name = "check" kind = "path" description = "The acceptance check record path: exit zero is achieved and non-zero is a gap." [[patterns.until_loop.parameters]] name = "max_cycles" kind = "text" description = "Optional operator ceiling for completed build cycles; `unbounded` means no ceiling." [[patterns.until_loop.parameters]] name = "completed_cycles" kind = "text" default = "0" description = "Completed build cycles carried by the authored recursive loop."
The probe is test -x, so a check that exists but is not executable is authored anyway;
author_check is not a tail route — it returns here and run_check runs what it wrote.
[[patterns.until_loop.steps]] name = "probe_check" title = "Check For Acceptance Check" type = "command" run = 'mkdir -p "$orchestration/state"; if test -x "$orchestration/$check"; then printf yes > "$orchestration/state/check-present.txt"; fi' outputs = [ { name = "check_present", path = "state/check-present.txt", values = ["yes"] }, ] [[patterns.until_loop.steps]] if = { check_present = "yes" } else = "author_check"
Exit zero ends the run by falling off the end of until_loop; non-zero is a gap, and the check's
own output is what the gap planner reads.
[[patterns.until_loop.steps]] name = "run_check" title = "Does The Acceptance Check Pass" type = "command" run = 'mkdir -p "$orchestration/state"; "$orchestration/$check" > "$orchestration/state/gap-report.txt" 2>&1' outputs = [ { name = "gap_report", path = "state/gap-report.txt" }, ] [[patterns.until_loop.steps]] if = { run_check = "failed" } then = "decide_cycle"
A branch target runs in its router's frame, so this pattern reads until_loop's parameters
without declaring any of them.
[[patterns.decide_cycle.steps]] name = "decide_cycle" title = "Check Cycle Ceiling" type = "command" run = 'mkdir -p "$orchestration/state"; completed="${completed_cycles:-0}"; case "$completed" in ""|*[!0-9]*) completed=0;; esac; case "$max_cycles" in ""|"unbounded") decision=continue;; *[!0-9]*) decision=invalid;; *) if test "$completed" -ge "$max_cycles"; then decision=ceiling; else decision=continue; fi;; esac; if test "$decision" = continue; then next=$((completed + 1)); else next=$completed; fi; printf "%s\n" "$completed" > "$orchestration/state/completed-cycles.txt"; printf "%s\n" "$next" > "$orchestration/state/next-completed-cycles.txt"; printf "%s\n" "$decision" > "$orchestration/state/cycle-decision.txt"' outputs = [ { name = "cycle_decision", path = "state/cycle-decision.txt", values = ["continue", "ceiling", "invalid"] }, { name = "next_completed_cycles", path = "state/next-completed-cycles.txt" }, ] [[patterns.decide_cycle.steps]] if = "cycle_decision" then = { continue = "close_the_gap", ceiling = "stop_cycle_ceiling", invalid = "stop_invalid_cycle_ceiling" } else = "stop_cycle_decision_absent" [[patterns.author_check.steps]] name = "author_check" title = "Author Acceptance Check" type = "agent" stage = "gate-build" prompt = "write-goal-check" inputs = ["{{until_source}}"] read_only = true outputs = [ { name = "goal_check_script", path = "{{check}}" }, ]
The planner may not edit the check it reports against, and build gets the same protection below.
[[patterns.close_the_gap.steps]] name = "plan_the_gap" title = "Plan Remaining Acceptance Check Gap" type = "agent" stage = "replan" prompt = "write-gap-plan" inputs = ["{{until_source}}", "{{gap_report}}"] protected = ["{{check}}"] on_plan_change = "nothing" outputs = [ { name = "gap_plan", path = "state/gap-plan.md" }, { name = "gap_plan_scope", path = "state/gap-plan-scope.txt", values = ["work", "none"] }, ] [[patterns.close_the_gap.steps]] if = "gap_plan_scope" then = { work = "run_cycle", none = "stop_no_gap_left" } else = "stop_gap_plan_scope_absent"
The gap plan is built as a build run; next_cycle is in tail position, so recursion does not
grow the invocation stack.
[[patterns.run_cycle.steps]] name = "build_the_plan" type = "call" pattern = "build" [patterns.run_cycle.steps.parameters] plan_source = "{{gap_plan}}" protected_check = "{{check}}" [[patterns.run_cycle.steps]] name = "next_cycle" type = "call" pattern = "until_loop" [patterns.run_cycle.steps.parameters] until_source = "{{until_source}}" check = "{{check}}" max_cycles = "{{max_cycles}}" completed_cycles = "{{next_completed_cycles}}"
The endings
A green check falls off the end of until_loop and declares no step; everything else is a routed
stop carrying one note.
[[patterns.stop_cycle_ceiling.steps]] type = "stop" note = "The acceptance check is still red after {{max_cycles}} completed build cycles, so the authored max_cycles ceiling stopped the run. The check's own output is at state/gap-report.txt." [[patterns.stop_invalid_cycle_ceiling.steps]] type = "stop" note = "The acceptance check is still red and max_cycles was `{{max_cycles}}`, which is neither `unbounded` nor a non-negative integer, so the authored cycle ceiling stopped the run before building another gap plan." [[patterns.stop_cycle_decision_absent.steps]] type = "stop" note = "The cycle-decision command did not write state/cycle-decision.txt, so the until loop cannot decide whether to build another cycle." [[patterns.stop_no_gap_left.steps]] type = "stop" note = "The acceptance check is still red and the gap planner found no actionable work left. The check's own output is at state/gap-report.txt and the planner's reasoning is at state/gap-plan.md." [[patterns.stop_gap_plan_scope_absent.steps]] type = "stop" note = "The gap planner did not write state/gap-plan-scope.txt, so the until loop cannot tell whether there is work to run."
until — work until an acceptance check goes green
One cycle is a chain of routes that never return:
until_loopruns the check,decide_cycletests the ceiling,close_the_gapplans the gap, andrun_cyclebuilds that plan and callsuntil_loopagain. Theincludesuppliesbuildand thewrite_handoverthe header names.