skip to content

The authoring guide

The narrative companion to the format reference: how to think while writing a pattern, and which shape the format wants for each kind of work.

Narrative guide to spec.md.

1. What you are writing

A pattern is finite TOML data that describes how Gantry composes work. It has no load-time computation, expression language, or author-defined control operators.

The language has nine primary words:

  • pattern defines a callable sequence.
  • agent, gate, and command are the only producing steps.
  • for, if, stop, and call are the only composition steps.
  • output is the value word: a file or directory path produced by exactly one step.

Write patterns by asking three questions for each producing step:

  1. What output or outcome does this step produce?
  2. Which if routes each value, absence, or failed outcome?
  3. What pattern should run next, including nothing when nothing should run next?

A dead agent is a failed step outcome, not a missing value. A file that was not written is absence, routed by else, not a malformed value. A malformed value is the producer's failed outcome.

Gantry, not the agent, owns Git and step boundaries. A pattern may name structural boundaries for restore or diff, and never runs Git operations as pattern steps.

2. Reading your first pattern

A runnable file has a [header] naming pattern_language_compatibility = "3", its entry pattern, and a human description. Pattern definitions live under [patterns.<name>] and contain ordered steps.

The smallest useful work loop has this form:

toml
[header]
pattern_language_compatibility = "3"
entry = "main"
description = "Run one task directory through an executor and gate."

[patterns.main]

[[patterns.main.parameters]]
name = "tasks"
kind = "path"
description = "Directory of task files."

[[patterns.main.steps]]
name = "run_tasks"
type = "for"
list = { directory = "{{tasks}}" }
body = "one_task"

[patterns.one_task]

[[patterns.one_task.steps]]
name = "execute"
type = "agent"
prompt = "{{task.prompt}}"

[[patterns.one_task.steps]]
name = "verify"
type = "gate"
command = "{{task.gate}}"

[[patterns.one_task.steps]]
if = { verify = "red" }
then = "stop_red"
else = "nothing"

[patterns.stop_red]

[[patterns.stop_red.steps]]
type = "stop"
note = "The gate went red; stopping for inspection."

The for step is sequential bounded iteration over a directory of task files. The directory may be a bound input or a directory output written by an earlier step. The body sees the current task through {{task}}, {{task.name}}, {{task.prompt}}, and {{task.gate}}.

The gate step runs Gantry's authoritative project check package. A command can compute a verdict, but it does not become a gate. A task may carry a gate command through {{task.gate}}; that changes the command the package runs, not the package's authority.

3. Producing values

Declare every file or directory a step produces on that step:

toml
[[patterns.review.steps]]
name = "review"
type = "agent"
prompt = "review"
outputs = [
  { name = "review_verdict", path = "state/review-verdict.txt", values = ["complete", "retry", "blocked"] },
  { name = "retry_note", path = "state/retry-note.md" },
]

An output with values is a branchable value. The producing step writes exactly one of those values to the declared file. An output without values is a data or prose record. A path ending in / is a directory output, which never declares values.

A branch on a declared output is exhaustive over the authored vocabulary:

toml
[[patterns.review.steps]]
if = "review_verdict"
then = { complete = "verify_review_edits", retry = "retry_sprint", blocked = "stop_review_blocked" }
else = "stop_missing_review"

else on this output-wide form is the absence arm. Use it deliberately when the producer may complete without writing the file. If absence should be terminal, route it to a stop pattern with an explicit note. If the branch should do nothing, route to nothing, the reserved engine-provided identity pattern. Do not define nothing yourself.

4. Routing failure

A producing step with a name also has a branchable outcome: done or failed. Gates additionally allow the color aliases green and red.

Route a failed outcome with an if, which must be the step immediately after the producing step it tests. With no else, a done outcome continues at the next step:

toml
[[patterns.one_task.steps]]
name = "execute"
type = "agent"
prompt = "{{task.prompt}}"

[[patterns.one_task.steps]]
if = { execute = "failed" }
then = "repair_dead_session"

[[patterns.one_task.steps]]
name = "verify"
type = "gate"
command = "{{task.gate}}"

An unrouted failed outcome reaches the engine-forced stop and may enter the header on_stop hook. If a failed step should be recoverable, say so with an if.

A reviewer that writes no verdict does not silently become retry; its missing verdict routes through else, and its failed session routes through the step outcome. Test the outcome first, because a value branch with both then and else always routes and no step after it runs:

toml
[[patterns.after_review.steps]]
if = { review = "failed" }
then = "retry_sprint"
else = "route_review_verdict"

[[patterns.route_review_verdict.steps]]
if = "review_verdict"
then = { complete = "verify_review_edits", retry = "retry_sprint", blocked = "stop_review_blocked" }
else = "retry_sprint"

5. Commands are deterministic data producers

Use command to derive deterministic values. A command writes declared outputs, and later if steps route those outputs or the command's outcome.

toml
[[patterns.check_roster.steps]]
name = "check_roster"
type = "command"
run = "bin/check-roster \"$roster\" > \"$roster_verdict\""
outputs = [
  { name = "roster_verdict", path = "state/roster-verdict.txt", values = ["valid", "invalid"] },
]

[[patterns.check_roster.steps]]
if = "roster_verdict"
then = { valid = "run_roster", invalid = "rewrite_roster" }
else = "rewrite_roster"

Command text receives whole values through environment variables. Parameters, readable outputs, and the current step's declared output paths are exported with their names; dots become underscores, so {{task.name}} becomes $task_name. $orchestration is the current invocation's state directory. Do not put {{...}} substitutions in run; whole-value references remain legal in typed slots such as command = "{{task.gate}}" or list = { directory = "{{roster}}" }.

Derive the fact, write an authored value, and route on it. Numbers are never stored as pattern state.

6. Iteration and recursion

Use for when the work is bounded by a directory of task files:

toml
[[patterns.run_roster.steps]]
name = "run_tasks"
type = "for"
list = { directory = "{{roster}}" }
body = "one_task"
fail = "continue"

for is sequential, not a parallel map, and has no concurrency option. The engine reads the directory when the for step starts, so a task added during an iteration is not in that invocation's list. When a body invocation falls off its end, the task is complete.

fail = "continue" handles only failed body invocations the body did not route. A red gate that the body routes to repair is not a for.fail event. An authored or engine stop inside the body is still a run stop.

Use recursion through call for unbounded loops:

toml
[patterns.until_green]

[[patterns.until_green.steps]]
name = "check"
type = "command"
run = "bin/check-goal > \"$goal_verdict\""
outputs = [
  { name = "goal_verdict", path = "state/goal-verdict.txt", values = ["green", "red"] },
]

[[patterns.until_green.steps]]
if = "goal_verdict"
then = { green = "nothing", red = "close_gap" }
else = "write_handover"

[patterns.close_gap]

[[patterns.close_gap.steps]]
name = "build_cycle"
type = "call"
pattern = "build"

[[patterns.close_gap.steps]]
name = "again"
type = "call"
pattern = "until_green"

There is no repeat, until, loop, cycle count, retry budget, depth guard, or max_cycles pattern word. Recursion ends when ordinary data routes somewhere that does not call back. The bundled until pattern declares max_cycles with the default unbounded; the bundled sprint pattern declares attempt_ceiling with the default unbounded, and the build entry passes 2.

Every edge that participates in a recursive component must be in tail position. The loader enforces this constraint.

7. Composition and includes

Every callable definition is a pattern. call invokes another pattern in the composed namespace:

toml
[[patterns.build.steps]]
name = "build_milestone"
type = "call"
pattern = "milestone"

[patterns.build.steps.parameters]
plan_source = "{{plan_source}}"

Files compose through the header include list, not through an include step:

toml
[header]
pattern_language_compatibility = "3"
entry = "build"
include = ["milestone.toml"]
description = "Build each milestone in order."

Definitions from the entry file and every included file form one flat pattern namespace. The entry file's header alone selects the run's entry pattern and stop hooks.

Treat a called pattern's interface as its parameters and declared outputs. Its name carries no engine behaviour. There is no level key or display label: a for adds a task-path segment, and the interface renders a position as numbers.

8. Plan-change handling

Plan-change observation is an agent-step property:

toml
[[patterns.review.steps]]
name = "review"
type = "agent"
prompt = "review"
on_plan_change = "review_plan_change"

on_plan_change = "<handler>" names a handler pattern. Gantry derives the watched plan-file set from the declared plan source and files derived from it, observes the diff after the agent step, calls the handler, and re-observes until the plan-file diff is clean. Use this policy for executor, reviewer, and recovery steps.

on_plan_change = "nothing" observes declared plan-file changes and accepts the observed diff without a handler. Use this policy for planner steps whose product is plan files.

An omitted on_plan_change key leaves the step unwatched for plan-file edits. The format has no author-owned plan-change scope vocabulary.

9. Prompt and reference discipline

Prompt ids are whole typed values. A prompt field may name a literal prompt id, use prompt = "{{task.prompt}}", or use a whole {{prompt-id-parameter}} reference. A prompt id never selects a role, stage, branch, or recovery policy.

Prompt prose placeholders live in the agent's text, not in the pattern namespace. A step's text table supplies prose values to those placeholders:

toml
[[patterns.repair.steps]]
name = "repair"
type = "agent"
prompt = "troubleshoot"

[patterns.repair.steps.text]
POSITION_CLAUSE = "Repair the red gate and write the troubleshoot verdict."

Whole-value references are structural only when they occupy the entire TOML value, except in prose text and declared output paths. They cannot supply identifiers. Do not build a prompt id, pattern name, output name, branch target, parameter name, or step name by string interpolation.

Task-carried prompts and gates are typed task references. {{task.prompt}} is usable where an agent prompt slot is expected; {{task.gate}} is usable where a gate command slot is expected. Neither grants behaviour because of its name or file path.

10. Stop hooks and compatibility

on_stop in the entry header installs hooks for engine stops only. A single pattern value is the catch-all form:

toml
on_stop = "write_handover"

A table can choose hooks by stop class:

toml
[header.on_stop]
step = "repair_unrouted_step"
gate = "repair_gate_stop"
any = "write_handover"

The closed stop classes are step, absence, plan, gate, record, and merge. any is the hook-table catch-all key, and a class-specific hook takes precedence. A record hook may be declared, but record stops neither dispatch nor fall through to any. merge is not a hook-table key and merge stops do not fall through to any.

A matching hook runs before the stop is final. Gantry then re-derives the stop condition. A cleared condition continues the run; recurrence at the same position without a new witnessed boundary records a final stop. Authored stop steps, operator pauses, and operator aborts do not enter on_stop.

Keep two compatibility surfaces separate:

  • pattern_language_compatibility = "3" in a pattern file is the grammar contract for the reader. A file declaring the retired version key instead is read only far enough to be refused with an incompatible-version error.
  • The run pin also records a pattern-language compatibility marker for the frozen closure it resumes. That marker protects reuse of an existing run under a different interpreter contract; it is not a second grammar.

11. Running, inspecting, resuming

  • Run: gantry --pattern <name|file.toml> [inputs…]. Name resolution walks the project tier, the global tier, and the bundled tier, taking the first that has a file.
  • Freeze: at start, Gantry freezes the resolved pattern closure, the selected merge pattern closure, and bound inputs. Edit the source pattern freely while a run executes; the run keeps its frozen copy.
  • Inspect: the run directory under .gantry/ is a book repository holding the run pin, merge-pattern.toml, the journals, the PROGRESS.md ledger, the frozen pattern closure under patterns/, state files, and step and session records.
  • Resume: gantry resume [<address>] re-enters a recorded run by name, plan path, or plan directory. It resumes from the frozen closure after checking the run pin's compatibility marker; older incompatible pins stay inspectable but are not parsed as patterns. --ignore-position-checkpoint begins at the entry pattern instead of the recorded position while retaining the other re-entry protections. A plan address identifying no run needs gantry <plan> to start a fresh run. A run recorded before merge-pattern.toml existed is backfilled once with the bundled merge closure.

12. Conventions

  • Name patterns for the author's intent, not for engine behaviour.
  • Fork the nearest bundled pattern; do not edit bundled files in place.
  • Comment policy choices and invariants, not TOML syntax.
  • Keep prompts and patterns harness-neutral.
  • Branch with if on the produced value or step outcome that decides the route.
  • Keep commands narrow: deterministic process text, environment variables in, declared outputs out.
  • Use nothing for an identity arm and stop for a terminal authored note.

docs/patterns/manual.md in the Gantry repository, rendered as it stands