skip to content

The milestone pattern

One milestone: plan a unit into sprints, author and ground its gate, then run each sprint through the included `sprint` pipeline — execute → gate → one bounded fix → re-gate → review → commit — with a bounded recovery ladder. Invoked as a sub-pattern by build (per milestone) — not selected directly.

milestone — Gantry's shared inner work unit

Brief the environment, plan a unit into sprints, author and ground the unit's gate, then run the sprint loop with its cross-sprint recovery ladder. Never selected directly: it is the body build invokes once per milestone, and that design / redesign invoke twice per run.

The per-sprint pipeline is not written here — it is sprint.toml, composed in by the include step in build_one_sprint. Inclusion is an authorship boundary, not a runtime one: the two files compose into one pattern at discovery, before validation. Overriding either half for one run is dropping a file into .gantry/<plan>/patterns/.

This file and sprint.toml are now the whole statement of the compiled milestone executor Gantry ran before patterns became first class; that code is gone.

milestone
[header]
name = "milestone"
version = "1.5"
description = """
One milestone: plan a unit into sprints, author and ground its gate, then run each sprint through the
included `sprint` pipeline — execute → gate → one bounded fix → re-gate → review → commit — with a
bounded recovery ladder. Invoked as a sub-pattern by build (per milestone) — not selected directly.
"""
entry = "main"

The unit's own level is the sprint; the invoker supplies the outer labels and the engine composes them into the task-id path.

milestone
[[header.levels]]
label = "sprint"

The parameter interface

The whole input contract. Everything else — orchestration directory, ledger, preamble — is re-pointed by the engine when the sub-pattern step runs. The two prompt parameters are read by the included pipeline rather than by anything here; they are declared so an invoker passes them to one name, and the inclusion hands them through as {{token}}s that resolve when these parameters bind.

milestone
[[header.parameters]]
name = "baseline_relaxed"
kind = "string"
default = "false"
description = "\"true\" to skip the baseline-green grounding while keeping per-sprint gates; \"false\" for build."

[[header.parameters]]
name = "plan_source"
kind = "file"
default = "plan.md"
description = "The plan prose the briefer and planner read for this unit."

design passes design/briefs/ or design/pages/ to reuse this same unit with pass-specific prompts and review language.

milestone
[[header.parameters]]
name = "prompt_prefix"
kind = "string"
default = ""
description = "Optional prefix for prompt ids this shared unit resolves, e.g. design/briefs/."

[[header.parameters]]
name = "review_prompt"
kind = "string"
default = "review"
description = "Prompt id for the review stage; pass-specific gated units may provide their own."

The bounded recovery budget: consecutive per-sprint attempts that entered a non-terminal recovery route. It advances on a red gate or a green-gate review retry and resets on a completed sprint, so isolated recovery between successful sprints never accumulates. It does not survive resume — the terminal stop is deliberately cleared so a resumed run retries with a fresh budget.

milestone
[[counters]]
name = "recovery_attempts"
reset = "consecutive"
increment_on = "recovery-attempt"

main

setup → plan → gate → the per-sprint loop.

The environment briefer runs ahead of the planner, and so ahead of every later agent. Best-effort: a failed briefing is noted and the run proceeds on Gantry's bare facts.

milestone
[[blocks.main]]
type = "agent"
role = "plan"
prompt = "write-environment"
stage = "environment-build"
inputs = ["{{plan_source}}"]
failure = "note-and-continue"

The sprint planner emits the unit's sprint files under the halting file contract. halt: everything downstream builds on the split, with no independent check to catch garbage. An empty sprint directory is not a halt — it simply ends the loop below.

milestone
[[blocks.main]]
type = "agent"
role = "plan"
prompt = "{{prompt_prefix}}write-sprints"
inputs = ["{{plan_source}}"]
failure = "halt"

Commit the plan.

milestone
[[blocks.main]]
type = "git"
op = "commit"

Author the gate in its own agent run. halt: the whole run grounds on it.

milestone
[[blocks.main]]
type = "agent"
role = "plan"
prompt = "{{prompt_prefix}}write-gate"
stage = "gate-build"
inputs = ["{{plan_source}}"]
read_only = true
produces = ["bin/gate"]
failure = "halt"

Ground the fresh gate on the clean baseline — unless this unit relaxes the precondition. The branch is data: build ("false") grounds; a pattern setting "true" goes straight to the loop, whose gates still run after every sprint.

milestone
[[blocks.main]]
type = "conditional"
condition = { kind = "flag", value = "{{baseline_relaxed}}", equals = "false" }
then = "ground_baseline"

Commit the gate.

milestone
[[blocks.main]]
type = "git"
op = "commit"

The sequential per-sprint loop over this unit's own re-pointed sprint directory, re-enumerated after every sprint so observed changes to remaining sprint files are consumed now, not deferred to resume. A DONE sprint vanishing is a corrupt-plan halt, inserted sprints are announced, and the resume position is recomputed as first-not-DONE each pass.

milestone
[[blocks.main]]
type = "loop"
body = "build_one_sprint"
rules = "recovery"
work_list = { source = { kind = "directory", path = "." } }

ground_baseline

Run the gate on the clean, pre-sprint tree. A red baseline means the gate is broken or the tree is not green, so stop before any sprint runs, attributed to the gate-builder.

milestone
[[blocks.ground_baseline]]
type = "gate"
if = "red"
then = "failure"

build_one_sprint

The loop body is this one-step wrapper rather than the sprint pipeline's own entry block, because an include step is replaced by an unconditional branch into the included entry rather than having the included steps spliced in where it was written. That is the one structural difference between the composed pattern and the single pre-split file it replaces, and it is invisible at runtime.

The two prompt parameters are handed through as this pattern's own {{token}}s: composition leaves such a token in place, so it resolves when this pattern's parameters bind.

milestone
[[blocks.build_one_sprint]]
type = "include"
pattern = "sprint"
parameters = { prompt_prefix = "{{prompt_prefix}}", review_prompt = "{{review_prompt}}" }

The mutual contract with sprint.toml

The two files compose into one pattern, so branch targets cross between them in both directions:

  • The sprint pipeline branches into this file's escalation entries. A red gate surviving its bounded fix leaves repair for troubleshoot_or_handover; a post-review one leaves repair_review / revert_review_edits for troubleshoot_or_handover_review. A replacement sprint.toml must target those names or bring its own escalation blocks — a red exit naming neither is a validation refusal on the composed whole, not a silent dead end.
  • This file branches into blocks the sprint pipeline defines: review_blocked_stop, the retry_note channel prepare_retry carries, and the [inner_unit_state] contract and commit_task block completion rests on.

recovery

Evaluated after each sprint iteration, in order. A rule whose outcome is a named block runs that block and evaluation continues with the remaining rules, so ordering plus consumed verdict state decide the route. Every sprint done ends the unit successfully.

milestone
[[rule_blocks.recovery]]
condition = { kind = "work-list-empty" }
outcome = "success"

A blocked review verdict stops the unit through the declared channel.

milestone
[[rule_blocks.recovery]]
condition = { kind = "channel-value", channel = "review_scope", value = "blocked" }
outcome = "review_blocked_stop"

The budget is spent: a handover-position troubleshoot writes the stop document and the unit stops as a resumable partial. This rule is the backstop for the green-gate review retry route, which never enters the red-gate troubleshoot branch below.

milestone
[[rule_blocks.recovery]]
condition = { kind = "counter-threshold", counter = "recovery_attempts", threshold = 3 }
outcome = "handover_stop"

A green-gate review retry prepares the next attempt against the same brief. Review retries share the recovery_attempts budget with red gates: no block can reset or increment a counter, so a dedicated troubleshoot_attempts is not expressible and the shared budget bounds the diagnostic too, through the in-body threshold on troubleshoot_or_handover.

Nothing here matches a red re-gate — it already ran the troubleshoot ladder from its own branch, so a remaining recovery attempt simply loops back onto this still-not-DONE sprint. Nor does a green sprint: it committed and went DONE in the included pipeline, and the loop advances. The legacy executor's no-op reconciliation gate, which fabricated a review verdict of its own, is not carried forward — an unchanged tree that leaves the gate red is just a red gate here.

milestone
[[rule_blocks.recovery]]
condition = { kind = "channel-value", channel = "review_scope", value = "retry" }
outcome = "prepare_retry"

Store the reviewer's note for the next attempt, then reset the project tree, preserving .gantry/.

milestone
[[blocks.prepare_retry]]
type = "inner-unit"
op = "prepare-retry"
note_channel = "retry_note"

[[blocks.prepare_retry]]
type = "git"
op = "reset"
scope = { kind = "project-only" }

The troubleshoot ladder

A red gate that survives its bounded fix is diagnosed before anything is discarded: the troubleshoot agent runs against the intact failed tree and either repairs and verifies (the gate proves it), or briefs a rollback-and-retry through its note, or writes a handover and stops. Rollback is the briefed retry behind the diagnosis, never the blind one in front of it — the legacy executor's rollback-then-investigate order, reversed.

Under the budget's in-body threshold the troubleshoot runs in repair position; at the threshold the same agent runs in handover position instead, parameterized by its position clause, because the first rung may be ambitious where the last rung before a human must prefer a clean handover to a heroic half-fix. The _review twin differs only in where a verified repair rejoins: branch targets are structural references and cannot be parameterized, so both entry points are spelled out.

milestone
[[blocks.troubleshoot_or_handover]]
type = "conditional"
condition = { kind = "counter-threshold", counter = "recovery_attempts", threshold = 2 }
then = "handover_stop"
else = "troubleshoot_and_route"

[[blocks.troubleshoot_or_handover_review]]
type = "conditional"
condition = { kind = "counter-threshold", counter = "recovery_attempts", threshold = 2 }
then = "handover_stop"
else = "troubleshoot_and_route_review"

The repair-position troubleshoot, then the verifying re-gate. A green re-gate proves the repair and rejoins the ordinary pipeline — the agent's word is never the grounds for advancing, the gate is. A troubleshoot the harness lost degrades to the conservative disposition rather than halting the ladder.

milestone
[[blocks.troubleshoot_and_route]]
type = "agent"
role = "build"
prompt = "troubleshoot"
stage = "troubleshoot"
failure = "degrade"
degrade_default = "handover"
protected = ["PROGRESS.md", ".preamble.sys.md", "bin", ":(glob)[0-9][0-9]-*.md"]
confine = true

[blocks.troubleshoot_and_route.template_values]
POSITION_CLAUSE = """
You are the first diagnostic rung and the recovery budget still has room: be ambitious. Prefer a
verified repair to a handover wherever a real check can prove the repair; choose the briefed retry
when the attempt itself took a wrong turn that a fresh run, told about it, would avoid."""

[[blocks.troubleshoot_and_route]]
type = "gate"
if = "green"
then = "after_green_gate"
else = "troubleshoot_red"

The same rung for a post-review red, rejoining at after_green_review instead.

milestone
[[blocks.troubleshoot_and_route_review]]
type = "agent"
role = "build"
prompt = "troubleshoot"
stage = "troubleshoot"
failure = "degrade"
degrade_default = "handover"
protected = ["PROGRESS.md", ".preamble.sys.md", "bin", ":(glob)[0-9][0-9]-*.md"]
confine = true

[blocks.troubleshoot_and_route_review.template_values]
POSITION_CLAUSE = """
You are the first diagnostic rung and the recovery budget still has room: be ambitious. Prefer a
verified repair to a handover wherever a real check can prove the repair; choose the briefed retry
when the attempt itself took a wrong turn that a fresh run, told about it, would avoid."""

[[blocks.troubleshoot_and_route_review]]
type = "gate"
if = "green"
then = "after_green_review"
else = "troubleshoot_red"

Still red: the diagnosis produced no verified repair, so the agent's declared disposition decides. An absent disposition — the agent ended without declaring one — takes the conservative branch.

milestone
[[blocks.troubleshoot_red]]
type = "conditional"
condition = { kind = "channel-value", channel = "troubleshoot_disposition", value = "retry" }
then = "troubleshoot_retry"
else = "troubleshoot_handover_stop"

The briefed rollback-and-retry: store troubleshoot's note for the next execute attempt (the same prepare-retry plumbing the review retry uses), then reset the project tree, preserving .gantry/. Falling off the end leaves the iteration red; the recovery rules loop back onto this still-not-DONE sprint, and the next execute starts clean with the note.

milestone
[[blocks.troubleshoot_retry]]
type = "inner-unit"
op = "prepare-retry"
note_channel = "troubleshoot_note"

[[blocks.troubleshoot_retry]]
type = "git"
op = "reset"
scope = { kind = "project-only" }

[[blocks.troubleshoot_handover_stop]]
type = "terminate"
disposition = "resumable-stop"
message = "troubleshoot ended without a verified repair; its handover (if written) is at state/handover.md"
report = "state/handover.md"

handover_stop

The forced terminal: the budget is spent, so the troubleshoot runs in handover position — no further repair, the document is the whole deliverable — and the unit stops as a resumable partial. Best-effort: a failed handover agent never blocks the stop it precedes. A later resume retries the sprint with a fresh budget, starting from the handover.

milestone
[[blocks.handover_stop]]
type = "agent"
role = "review"
prompt = "troubleshoot"
stage = "troubleshoot"
failure = "note-and-continue"

[blocks.handover_stop.template_values]
POSITION_CLAUSE = """
The recovery budget for this task is spent and the run stops when you finish: do not attempt another
repair. Your whole deliverable is the handover document — end in the Handover state below; the other
two end states are closed to you."""

[[blocks.handover_stop]]
type = "terminate"
disposition = "resumable-stop"
message = "recovery budget spent; the troubleshoot handover is at state/handover.md"
report = "state/handover.md"