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 seeds 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."
test -x treats a non-executable path as absent because run_check invokes the check.
author_check is the else branch, so run_check executes the check it writes.
[[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"; else printf no > "$orchestration/state/check-present.txt"; fi' outputs = [ { name = "check_present", path = "state/check-present.txt", values = ["yes", "no"] }, ] [[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}}" }, ]
plan_the_gap may not edit the check it reports against; build_the_plan receives that check as
protected_check.
[[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"
build_the_plan calls build, then next_cycle calls until_loop with the output counter.
[[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 completes until_loop; every other ending is a routed stop with 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
until_loopruns the check,decide_cycletests the ceiling,close_the_gapplans the gap, andrun_cyclebuilds that plan before callinguntil_loopagain. Theincludesuppliesbuildand thewrite_handoverthe header names.