@pmelab/gtd 7.4.1 → 7.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -106,6 +106,9 @@ handful of commands drive it:
106
106
  pending changes, and which action leads where. The panel and diagram highlight
107
107
  refresh live (~every 3s) while the page is open, so advancing the process
108
108
  elsewhere shows up without a manual refresh.
109
+ - **`gtd edit [path]`** — open `<path>` (or, with no argument, the current
110
+ resting state's steering file, or the repo dir) in `${VISUAL:-$EDITOR}`,
111
+ blocking until it exits.
109
112
 
110
113
  `gtd version` (or `gtd --version`/`-v`) prints the installed version and exits;
111
114
  `gtd help` (or `gtd --help`/`-h`) prints the command list. Both short-circuit
@@ -165,14 +168,20 @@ workflow is just `.gtdrc` config — edit it or write your own (see
165
168
  through two `vars` tiers — `plannerModel` (heavier planning and review) and
166
169
  `coderModel` (the coding turns) — so you can repoint the models globally in one
167
170
  place (a `vars:` edit or a `GTD_VAR_plannerModel` override) instead of per
168
- state.
171
+ state. Steering-file path vars (`feedbackFile`, `reviewFile`, …) work the same
172
+ way, and now propagate to the `on` patterns that route on them too — a repointed
173
+ path var actually reroutes the machine, not just the templates that read/write
174
+ the file.
169
175
 
170
176
  Bare `gtd` (or `gtd loop`) is a ready-to-run driver for the whole protocol —
171
177
  point it at a repo and it runs the loop until it's your turn. It is the only
172
- command you run: at a gate you edit files (answer a plan question, tick a review
173
- box, fix code) and re-launch it, and it captures your edit as its opening move
174
- before driving on — so you never run `gtd step human` by hand. See
175
- [Driving the loop](docs/loop.md).
178
+ command you run: at a human gate it now opens `${VISUAL:-$EDITOR}` on the gate's
179
+ file for you automatically (answer a plan question, tick a review box, fix
180
+ code), waits for you to save and exit, then captures your edit itself and keeps
181
+ driving — or halts if you left nothing changed — so you never run
182
+ `gtd step human` by hand. Pass `--no-edit` (or set `GTD_NO_EDIT`) to fall back
183
+ to halting and printing the gate instead, if you'd rather edit and re-launch it
184
+ yourself. See [Driving the loop](docs/loop.md).
176
185
 
177
186
  Before wiring gtd into a repo, note the
178
187
  [repository requirements](docs/cli.md#repository-requirements) — most
package/bin/gtd CHANGED
@@ -3,9 +3,17 @@ set -euo pipefail
3
3
 
4
4
  # This file is the single published `bin` entry: `bin/gtd`. Before anything
5
5
  # else, it dispatches on $1:
6
+ # - `--no-edit` (bare, or immediately after `loop`) -> stripped here and
7
+ # disables the loop's automatic editor launching for this run (see
8
+ # `edit_disabled`/`GTD_NO_EDIT` below); dispatch then continues on the
9
+ # remaining arguments as if it had never been given.
10
+ # - `edit [path]` -> handled entirely here in bash, never forwarded to the
11
+ # bundle: opens `${VISUAL:-$EDITOR}` on `path`, or (no `path` given) on the
12
+ # resolved rest's `.file` (or `.` when absent).
6
13
  # - no arguments, or exactly `loop` with no further arguments -> fall through
7
14
  # into the loop body below (the loop protocol comment further down).
8
- # - `loop` with additional arguments -> usage error, exit 2.
15
+ # - `loop` with additional arguments (other than a lone `--no-edit`) ->
16
+ # usage error, exit 2.
9
17
  # - anything else -> hand off to the bundle via `run_gtd`, forwarding all
10
18
  # arguments unchanged (e.g. `bin/gtd status`, `bin/gtd step human`).
11
19
  # `run_gtd` is the ONE place that knows how to invoke "the bundle": by default
@@ -25,6 +33,55 @@ run_gtd() {
25
33
  fi
26
34
  }
27
35
 
36
+ # Shared editor-launch helper — used by both the `edit` subcommand below and
37
+ # the loop's human-gate flow further down. Resolves the editor git-style
38
+ # ($VISUAL, then $EDITOR — no fallback to `vi`), then launches it blocking in
39
+ # the foreground with the target path passed as a positional parameter
40
+ # (never string-spliced into the command), so a multi-word command
41
+ # (`code --wait`) and a path with special characters both work. A launch
42
+ # failure (bad/missing editor binary) is a plain non-zero return, propagating
43
+ # under `set -e`; the editor's own exit code is otherwise ignored entirely —
44
+ # the outcome is judged from tree state afterward, not from this function.
45
+ launch_editor() {
46
+ local target="$1" editor="${VISUAL:-${EDITOR:-}}"
47
+ if [[ -z "$editor" ]]; then
48
+ echo "gtd: no editor configured — set \$EDITOR (or \$VISUAL)" >&2
49
+ return 1
50
+ fi
51
+ bash -c "$editor \"\$1\"" gtd-edit "$target"
52
+ }
53
+
54
+ # --no-edit / GTD_NO_EDIT govern only the loop's automatic editor launching at
55
+ # human gates (see handle_human_gate further down) — `gtd edit` itself never
56
+ # reads either. `--no-edit` is recognised in exactly the two sanctioned
57
+ # positions (bare, or right after `loop`) and stripped before anything below
58
+ # looks at "$@", so the bundle's own unknown-option guard never sees it.
59
+ edit_disabled=0
60
+ [[ -n "${GTD_NO_EDIT:-}" ]] && edit_disabled=1
61
+ if [[ "${1:-}" == "--no-edit" && $# -eq 1 ]]; then
62
+ edit_disabled=1
63
+ shift
64
+ elif [[ "${1:-}" == "loop" && "${2:-}" == "--no-edit" && $# -eq 2 ]]; then
65
+ edit_disabled=1
66
+ set -- loop
67
+ fi
68
+
69
+ if [[ "${1:-}" == "edit" ]]; then
70
+ shift
71
+ if [[ $# -gt 0 ]]; then
72
+ launch_editor "$1"
73
+ else
74
+ if ! peek_json="$(run_gtd next --json 2>&1)"; then
75
+ echo "gtd edit: could not determine the next step:" >&2
76
+ echo "$peek_json" >&2
77
+ exit 1
78
+ fi
79
+ file="$(jq -r '.file // empty' <<<"$peek_json")"
80
+ launch_editor "${file:-.}"
81
+ fi
82
+ exit 0
83
+ fi
84
+
28
85
  if [[ $# -gt 0 && "$1" != "loop" ]]; then
29
86
  run_gtd "$@"
30
87
  exit $?
@@ -33,12 +90,16 @@ if [[ "${1:-}" == "loop" && $# -gt 1 ]]; then
33
90
  echo "gtd: 'loop' takes no arguments" >&2
34
91
  exit 2
35
92
  fi
36
- # otherwise ($# -eq 0, or exactly "loop" with no further args): fall through
37
- # into the existing loop body below, unchanged in behavior.
93
+ # otherwise ($# -eq 0, or exactly "loop" with no further args, --no-edit
94
+ # already stripped above): fall through into the existing loop body below,
95
+ # unchanged in behavior.
38
96
 
39
97
  # Drives the v3 pattern-machine loop (see docs/loop.md): each iteration asks
40
98
  # `gtd next --json` who's up and what they should do, dispatching on `kind`:
41
- # - "message" (a human rest) -> halt, tell the user what gtd is waiting on.
99
+ # - "message" (a human rest) -> see handle_human_gate above: with editing on
100
+ # (the default) open the editor at the gate and step human, driving on a
101
+ # captured commit; with editing off (--no-edit/GTD_NO_EDIT), halt and tell
102
+ # the user what gtd is waiting on, unchanged from before this feature.
42
103
  # - "script" (a check rest) -> execute `content` (the emitted wrapper
43
104
  # script) verbatim via bash — exit code deliberately ignored, the outcome
44
105
  # lives in the tree — then `gtd step <actor>` to capture it; zero new
@@ -101,6 +162,57 @@ herdr_release() { # hand the pane back to Herdr's normal detection
101
162
  herdr_run pane release-agent "$HERDR_PANE_ID" --source herdr:gtd --agent gtd
102
163
  }
103
164
 
165
+ # Shared human-gate handler for a "message" (human) rest — used both by the
166
+ # opening move (when editing is on) and by every "message" rest the main loop
167
+ # encounters. $1 is the rest's `next --json` blob.
168
+ #
169
+ # Editing OFF ($edit_disabled): today's behavior, unchanged — print the gate
170
+ # and exit 0 without touching an editor.
171
+ #
172
+ # Editing ON (the default): open the editor on the rest's `.file` (or `.` when
173
+ # absent), wait for it to exit, then run `gtd step human`:
174
+ # - a NEW commit was authored -> return normally so the caller keeps driving.
175
+ # - ZERO commits were authored -> halt, exit 0 (closing the editor without a
176
+ # capturable change means "done for now" — and keeps a gate with no `C`
177
+ # pattern, e.g. idle, from being reopened forever).
178
+ # - the step REFUSES (non-zero exit: malformed steering file, unfinished
179
+ # review) -> surface it verbatim and halt, exit non-zero — the same
180
+ # wording an opening-move refusal already used before this function
181
+ # existed.
182
+ handle_human_gate() {
183
+ local json="$1" file label state step_out head_before head_after
184
+ label="$(jq -r '.label // .state' <<<"$json")"
185
+ state="$(jq -r .state <<<"$json")"
186
+
187
+ if [[ "$edit_disabled" == 1 ]]; then
188
+ herdr_report blocked "$label"
189
+ herdr_notify "gtd needs you — $(basename "$PWD")" "$label"
190
+ echo "--- Your turn ($state) ---"
191
+ run_gtd next
192
+ exit 0
193
+ fi
194
+
195
+ file="$(jq -r '.file // empty' <<<"$json")"
196
+ herdr_report working "$label"
197
+ launch_editor "${file:-.}"
198
+
199
+ head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
200
+ if ! step_out="$(run_gtd step human 2>&1)"; then
201
+ echo "gtd-loop: could not capture your changes at the human gate:" >&2
202
+ echo "$step_out" >&2
203
+ echo "Fix the reported issue and re-run gtd-loop." >&2
204
+ exit 1
205
+ fi
206
+ head_after="$(git rev-parse HEAD 2>/dev/null || echo none)"
207
+ if [[ "$head_before" == "$head_after" ]]; then
208
+ herdr_report idle "$label"
209
+ herdr_release
210
+ echo "--- Done for now ($state: nothing captured) ---"
211
+ exit 0
212
+ fi
213
+ # else: a new commit was captured — return normally, caller keeps driving.
214
+ }
215
+
104
216
  trap 'code=$?; if [ "$code" -ne 0 ]; then
105
217
  herdr_report blocked "${GTD_LAST_LABEL:-stopped}: exited $code"
106
218
  herdr_notify "gtd stopped — $(basename "$PWD")" "${GTD_LAST_LABEL:-loop} failed (exit $code)"
@@ -154,6 +266,12 @@ run_agent_turn() {
154
266
  # a harmless no-op; a genuine refusal there (a malformed steering file, or an
155
267
  # edit no `on` pattern accepts) is surfaced and stops the loop rather than being
156
268
  # driven past.
269
+ #
270
+ # Editing ON: route through the exact same gate logic the main loop uses below
271
+ # (open the editor there first, THEN step) instead of blindly stepping human
272
+ # and moving on. Editing OFF: unchanged from before this function existed —
273
+ # step silently, refusal surfaced and halts, a clean/committed gate just falls
274
+ # into the loop below.
157
275
  if ! peek_json="$(run_gtd next --json 2>&1)"; then
158
276
  echo "gtd-loop: could not determine the next step:" >&2
159
277
  echo "$peek_json" >&2
@@ -161,11 +279,15 @@ if ! peek_json="$(run_gtd next --json 2>&1)"; then
161
279
  exit 1
162
280
  fi
163
281
  if [[ "$(jq -r .kind <<<"$peek_json")" == "message" ]]; then
164
- if ! step_out="$(run_gtd step human 2>&1)"; then
165
- echo "gtd-loop: could not capture your changes at the human gate:" >&2
166
- echo "$step_out" >&2
167
- echo "Fix the reported issue and re-run gtd-loop." >&2
168
- exit 1
282
+ if [[ "$edit_disabled" == 1 ]]; then
283
+ if ! step_out="$(run_gtd step human 2>&1)"; then
284
+ echo "gtd-loop: could not capture your changes at the human gate:" >&2
285
+ echo "$step_out" >&2
286
+ echo "Fix the reported issue and re-run gtd-loop." >&2
287
+ exit 1
288
+ fi
289
+ else
290
+ handle_human_gate "$peek_json"
169
291
  fi
170
292
  fi
171
293
 
@@ -186,18 +308,15 @@ while true; do
186
308
  label="$(jq -r '.label // .state' <<<"$next_json")"
187
309
  GTD_LAST_LABEL="$label"
188
310
 
189
- if [[ "$kind" != "message" ]]; then
190
- herdr_report working "$label"
191
- fi
192
-
193
311
  if [[ "$kind" == "message" ]]; then
194
- herdr_report blocked "$label"
195
- herdr_notify "gtd needs you — $(basename "$PWD")" "$label"
196
- echo "--- Your turn ($state) ---"
197
- run_gtd next
198
- exit 0
312
+ # handle_human_gate exits 0/non-zero itself on a halt/refusal; it only
313
+ # returns here when a new commit was captured, so we keep driving.
314
+ handle_human_gate "$next_json"
315
+ continue
199
316
  fi
200
317
 
318
+ herdr_report working "$label"
319
+
201
320
  if [[ "$kind" == "script" ]]; then
202
321
  head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
203
322
  # Execute the emitted wrapper verbatim; its exit code is deliberately
@@ -189288,7 +189288,11 @@ const STATUSES = /* @__PURE__ */ new Set([
189288
189288
  * glob, is tolerated (trimmed); the glob itself is taken verbatim after that
189289
189289
  * (so a glob containing further spaces, e.g. a path with a literal space in
189290
189290
  * it, is preserved intact — only the FIRST space is the status/glob
189291
- * separator).
189291
+ * separator). Operates on the ALREADY-Eta-RENDERED pattern string — a
189292
+ * workflow author may write `"A <%= it.vars.feedbackFile %>"` in `on:`, but
189293
+ * by the time it reaches this parser (or `matchesPattern` below) the edge
189294
+ * (`src/Edge.ts`'s `renderOnEdges`) has already substituted `it.vars`; this
189295
+ * module never renders anything itself.
189292
189296
  */
189293
189297
  const parsePattern = (raw) => {
189294
189298
  const trimmed = raw.trim();
@@ -190061,9 +190065,9 @@ const collectGroups = (raw) => {
190061
190065
  * <name>:
190062
190066
  * actor: <string> # forbidden on a commit state, required otherwise
190063
190067
  * script: <string> # exactly one of script/prompt/message/commit
190064
- * on: # a mapping, DECLARATION ORDER PRESERVED
190068
+ * on: # a mapping, DECLARATION ORDER PRESERVED — each <pattern> KEY is itself an Eta template, rendered against `it.vars` at the edge (src/Edge.ts's renderOnEdges) before the pure engine ever sees it
190065
190069
  * "<pattern>": <targetState> # short form
190066
- * "<pattern>": { to: <targetState>, describe: <sentence> } # with a human-readable route description
190070
+ * "<pattern>": { to: <targetState>, describe: <sentence> } # with a human-readable route description (describe is NEVER Eta-rendered, unlike the pattern key)
190067
190071
  * initial: true # exactly one state across the whole workflow
190068
190072
  * retry:
190069
190073
  * max: <number>
@@ -190590,7 +190594,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true) =
190590
190594
  };
190591
190595
  //#endregion
190592
190596
  //#region src/workflows/unified.yaml
190593
- var unified_default = "# The unified gtd workflow template — gtd's BUILT-IN DEFAULT workflow. When no\n# `workflow:` key is configured anywhere in the cwd→home config chain,\n# src/Config.ts runs this template directly, so a state command works with no\n# configuration at all. `gtd init` seeds only the settings a project tunes\n# (vars.testCommand + a modes: formatting suggestion), never the workflow\n# itself. See STATES.md §10 for the full walkthrough.\n#\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/templates.ts does the compiling). Every content string below\n# is INLINE (no `./`-relative file references): the bundled template ships\n# inside a single-file build (`dist/gtd.bundle.mjs`), so its content must\n# already be resolved at BUILD time. `renderInitConfig` (src/workflows/\n# templates.ts) can MATERIALIZE this inline source into a `.gtdrc.json`\n# `workflow:` value — the way to eject/customize the machine, and the hermetic\n# test fixture — but `gtd init` itself no longer writes the workflow.\n#\n# ── Authored with SUB-MACHINES (Option C) ──────────────────────────────────\n#\n# This template is written with `submachines:`/`use:` (see src/Submachines.ts\n# and STATES.md). Expansion is a compile-time raw → raw pre-pass with NO runtime\n# footprint: the pure engine only ever sees the flat concrete states, and each\n# invocation's `as:` rename map reproduces the exact state names the flat\n# template used — so the compiled `WorkflowDefinition` is BYTE-IDENTICAL to the\n# frozen `unified.flat.yaml` (pinned by src/workflows/golden.test.ts). The\n# sub-machines split into DEDUP (a gate/loop authored once, invoked several\n# times) and ENCAPSULATION (a single-instance cluster grouped for reading):\n#\n# * makeGreen (dedup ×2) — the check/fix/escalate health triple shared by\n# the simple build tail and the advanced package\n# loop (identical script/prompt/message).\n# * assertGreen (dedup ×3) — the green-baseline gate (check + human blocked)\n# guarding the simple, advanced and review entries.\n# * qaLoop (dedup ×2) — the checkbox Q&A author/answer loop shared by the\n# product (adv-grilling) and technical (architecting)\n# phases of the advanced flow.\n# * planLoop (encap) — the simple flow's plan-iteration loop.\n# * humanReview (encap) — the shared review tail (reviewing → await-review →\n# review-deciding → feedback collect/build).\n# * specGate (encap) — the advanced flow's per-package agentic review.\n# * packageLoop (encap) — the advanced flow's per-package build queue.\n#\n# ── Four entry points behind a green-baseline gate, one shared tail ─────────\n#\n# EVERY entry first runs the test suite and only proceeds when it is GREEN — you\n# never start new work (or a review) on top of a red baseline. A red run halts\n# at a human `*-start-blocked` gate (fix the failures and re-run, exactly like\n# the `escalate` states), or is what `gtd fix` exists to repair.\n#\n# The initial `idle` state FORKS on which steering file the human creates,\n# routing into that flow's OWN start gate:\n#\n# * .gtd/REQUIREMENTS.md -> `adv-start-check` -> the ADVANCED flow (product +\n# technical Q&A, package decomposition, per-package parallel build +\n# agentic spec-review).\n# * anything else (e.g. .gtd/TODO.md) -> `start-check` -> the SIMPLE flow (one\n# grilling Q&A loop, a monolithic build, no decomposition, no agentic\n# review).\n#\n# A third entry, `gtd review <commitish>`, jumps to `review-start-check`\n# (reviewEntry) and, once green, to `reviewing` to review a foreign branch\n# through the same shared tail. A fourth, `gtd fix`, jumps to `fix-check`\n# (fixEntry): a red suite drops straight into the shared `fixing` loop and out\n# through the review + squash tail; a green suite is a no-op back to `idle`.\n#\n# All three converge at `reviewing` -> `await-review`. Ticking a REVIEW.md box\n# means only \"I reviewed this hunk\"; a COMMENT (not an unticked box) is what asks\n# for changes. Every human step at `await-review` goes to `review-deciding`,\n# which inspects the step and routes:\n# * no comment (every box ticked, no REVIEW.md note, no code edit) -> the diff\n# is a bare REVIEW.md deletion -> squashing -> done (the ONLY path to the\n# finale — the whole cycle collapses into one commit).\n# * any comment — a note in REVIEW.md OR a hand-edit to code — ->\n# review-deciding writes REVIEW_FEEDBACK.md -> feedback-building implements\n# it (completing any code the human hand-edited, never reverting their\n# lines) -> checking -> reviewing regenerates an INCREMENTAL REVIEW.md\n# (review-deciding carries `reviewBase: true`, so each feedback round's\n# review + checkout window covers only `last-review..HEAD`, i.e. the agent's\n# follow-through, not the human's own edit) -> await-review, looping until\n# sign-off.\n# Two dead ends never commit: the sign-off gate (src/program.ts) REFUSES a step\n# that leaves a box unticked with no comment (finish reviewing first) and a\n# deleted REVIEW.md (restore it and tick the boxes).\n#\n# The shared health segment (`checking`/`fixing`/`escalate`) serves the simple\n# build and the feedback loop — its green edge targets\n# `reviewing`. The advanced per-package loop keeps its OWN health triple\n# (`adv-checking`/`adv-fixing`/`adv-escalate`) because its green edge targets\n# the per-package `spec-review` gate instead.\n#\n# KNOWN LIMITATION: `on` pattern keys are NOT Eta templates — they keep the\n# LITERAL `.gtd/...` paths matching these vars' DEFAULT values. Repointing a\n# file var changes what templates read/write WITHOUT changing what the `on`\n# patterns match, desyncing the machine (see docs/configuration.md).\n\nvars:\n testCommand: npm test\n todoFile: .gtd/TODO.md\n requirementsFile: .gtd/REQUIREMENTS.md\n architectureFile: .gtd/ARCHITECTURE.md\n packagesDir: .gtd/packages\n nextFile: .gtd/NEXT.md\n reviewFile: .gtd/REVIEW.md\n reviewRawFile: .gtd/REVIEW_RAW.md\n reviewFeedbackFile: .gtd/REVIEW_FEEDBACK.md\n feedbackFile: .gtd/FEEDBACK.md\n specFeedbackFile: .gtd/SPEC_FEEDBACK.md\n commitMsgFile: .gtd/COMMIT_MSG.md\n # The two opaque model tiers every agent state draws from — repoint them here\n # (or via a top-level `.gtdrc` `vars:` key / a `GTD_VAR_plannerModel` env var)\n # to change every state on that tier at once. `plannerModel` (default \"smart\")\n # is the heavier tier for the one-shot planning/architecting/review turns;\n # `coderModel` (default \"base\") is the tier for the build/fix turns.\n plannerModel: smart\n coderModel: base\n\nsubmachines:\n # ── DEDUP: the check/fix/escalate health triple ───────────────────────────\n # Runs the suite; a red run records .gtd/FEEDBACK.md, a green run cleans it up.\n # The `on` rules turn that into \"fix\" vs \"$onGreen\". The fix turn retries up to\n # 3× before escalating to a human. Invoked for the shared build tail\n # (checking/fixing/escalate -> reviewing) and the advanced package loop\n # (adv-checking/adv-fixing/adv-escalate -> spec-review).\n makeGreen:\n params: [onGreen, escalateDescribe, checkLabel, fixLabel, escalateLabel]\n states:\n check:\n actor: check\n label: $checkLabel\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A .gtd/FEEDBACK.md\": fix\n \"M .gtd/FEEDBACK.md\": fix\n \"D .gtd/FEEDBACK.md\": $onGreen\n \"C\": $onGreen\n fix:\n actor: agent\n label: $fixLabel\n file: <%= it.vars.feedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: fix\n retry:\n max: 3\n otherwise: escalate\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.feedbackFile %>`, which this prompt tells you to address.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue.\n\n Leave everything uncommitted and finish your turn — do not commit.\n on:\n \"* **\": check\n escalate:\n actor: human\n label: $escalateLabel\n file: <%= it.vars.feedbackFile %>\n message: |\n The agent could not get the check to pass after repeated attempts.\n `<%= it.vars.feedbackFile %>` holds the last failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n describe: $escalateDescribe\n\n # ── DEDUP: the green-baseline gate ─────────────────────────────────────────\n # Runs the suite before any work starts (its script is the health check's test\n # wrapper — red -> .gtd/FEEDBACK.md, green -> remove it); the `on` rules turn\n # that into \"halt at blocked\" vs \"proceed to $onGreen\". The script and blocked\n # message/describe differ per entry, so they ride in as `with:` params.\n assertGreen:\n params: [onGreen, checkScript, blockedMessage, blockedDescribe, checkLabel, blockedLabel]\n states:\n check:\n actor: check\n label: $checkLabel\n script: $checkScript\n on:\n \"A .gtd/FEEDBACK.md\": blocked\n \"M .gtd/FEEDBACK.md\": blocked\n \"D .gtd/FEEDBACK.md\": $onGreen\n \"C\": $onGreen\n blocked:\n actor: human\n label: $blockedLabel\n file: <%= it.vars.feedbackFile %>\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n describe: $blockedDescribe\n\n # ── DEDUP: the checkbox Q&A author/answer loop ─────────────────────────────\n # The agent develops a plan file, surfacing `## Open Questions` as checkbox\n # lists; the human answers (answerGate refuses an unanswered step) and either\n # advances or sends it back. Invoked for the product (adv-grilling) and\n # technical (architecting) phases of the advanced flow.\n qaLoop:\n params:\n [\n authorFile,\n authorPrompt,\n answerMessage,\n onAdvance,\n advanceDescribe,\n editDescribe,\n authorLabel,\n answerLabel,\n ]\n states:\n author:\n actor: agent\n label: $authorLabel\n file: $authorFile\n mode: qa\n model: <%= it.vars.plannerModel %>\n memory: plan\n prompt: $authorPrompt\n on:\n \"* **\": answer\n answer:\n actor: human\n label: $answerLabel\n file: $authorFile\n mode: qa\n answerGate: true\n message: $answerMessage\n on:\n \"C\":\n to: $onAdvance\n describe: $advanceDescribe\n \"* **\":\n to: author\n describe: $editDescribe\n\n # ── ENCAPSULATION: the simple flow's plan-iteration loop ───────────────────\n planLoop:\n params: [onAccept]\n states:\n planning:\n actor: agent\n label: Refining the plan\n file: <%= it.vars.todoFile %>\n model: <%= it.vars.plannerModel %>\n memory: plan\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.todoFile %>`, which this prompt tells you to develop.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build — or, on a\n return lap, the plan you already wrote plus the human's edits and comments\n on it. Read it, explore the codebase, and develop it into a concrete\n implementation plan: the files to change, the approach, and the steps to\n take.\n\n Do NOT ask questions and do NOT add an \"Open Questions\" section — decide\n every open point yourself and write the plan as a set of concrete\n decisions. Where a decision is genuinely a judgement call, state the\n choice you made and, in one line, why. The human iterates on the plan by\n editing it directly, not by answering questions.\n\n On a return lap, `<%= it.vars.todoFile %>` carries the human's edits and\n inline comments on your previous plan. Fold every edit and comment into a\n revised, self-contained plan and remove the addressed comments — the plan\n is always the current, clean proposal, never a diff.\n\n Leave `<%= it.vars.todoFile %>` uncommitted and finish your turn — do\n not commit, and do not run `gtd step agent` yourself; the harness does\n that.\n on:\n \"* **\": plan-review\n\n plan-review:\n actor: human\n label: Awaiting your review\n file: <%= it.vars.todoFile %>\n message: |\n `<%= it.vars.todoFile %>` holds the proposed implementation plan. Read it,\n then either accept it as-is or edit it — rewrite a step, add a\n constraint, or drop an inline comment — to send it back for another round.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: $onAccept\n describe: >-\n Change nothing to accept the plan as-is and move on to implementation\n (**building**).\n \"* **\":\n to: planning\n describe: >-\n Edit the plan — rewrite a step, add a constraint, or leave an inline\n comment — to send it back for another round (**planning**).\n\n # ── ENCAPSULATION: the shared review tail ──────────────────────────────────\n humanReview:\n params: [onSignoff]\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: <%= it.vars.reviewFile %>\n mode: review\n model: <%= it.vars.plannerModel %>\n memory: review\n # `gtd review <commitish>` (a plain non-gtd branch, e.g. a colleague's PR)\n # does NOT enter here directly — it enters `review-start-check` (reviewEntry)\n # first, which runs the green-baseline gate and, once green, transitions\n # here. The `Gtd-Review-Base:` trailer rides on that entry commit (the\n # process's oldest), so everything below still reuses the same\n # reviewing/await-review/feedback machinery over <commitish>..HEAD.\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewFile %>`, which this prompt tells you to write.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the diff below in\n this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.startCommit %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation of\n what changed and why, then one `- [ ] ./path/to/file.ts#42 — note` file\n pointer per hunk (checkboxes are for the human to tick, not you; every\n path is `./`-relative, with an optional `#line` and an optional\n ` — note`).\n\n The diff below is `it.reviewDiff` — on a feedback round it covers ONLY the\n changes made since the previous review, so you review just what changed,\n not items already signed off. Leave `<%= it.vars.reviewFile %>`\n uncommitted and finish your turn.\n <% if (it.reviewDiff.trim()) { %>\n\n ## Full diff under review\n\n ```diff\n <%~ it.reviewDiff %>\n ```\n <% } %>\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n label: Awaiting your review\n file: <%= it.vars.reviewFile %>\n mode: review\n # While the cycle rests here, gtd opens a \"review checkout window\": raw HEAD\n # is rewound to the review base (the most-recent `review-deciding` commit —\n # so a feedback round shows only `last-review..HEAD` — or the process start\n # on the first review) with the working tree untouched, surfacing the diff\n # as ordinary uncommitted changes. It closes on the next invocation.\n #\n # Every human step here routes to `review-deciding`, which decides sign-off\n # vs. feedback from the step's CONTENT (a comment, not a tick). Ticking a box\n # only records \"I reviewed this hunk\". Two dead ends can't be told apart by a\n # file-pattern edge, so the sign-off gate (`enforceReviewSignoffGate` in\n # src/program.ts) catches them BEFORE the step commits and refuses: an\n # unfinished review (a box still `- [ ]` with no comment) and a deleted\n # REVIEW.md.\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the cycle — one\n `- [ ]` checkbox per reviewable item, grouped into chunks. Tick a box\n (`- [x]`) as you review each hunk; ticking only records that you've read\n it, it is not sign-off.\n\n When you've been through the whole diff, run `gtd step human`:\n\n - Tick EVERY box and leave no comment — no note in\n `<%= it.vars.reviewFile %>`, no code edit — to sign off and collapse the\n cycle into one commit (**squashing**).\n - Leave a comment to request changes — a note on a\n `<%= it.vars.reviewFile %>` line, or a direct code edit — to send it to a\n build + re-review round (**review-deciding** → **feedback-building**). A\n code edit counts as your own fix: the agent completes the follow-through\n it implies and never reverts your lines.\n\n Stepping with a box still unticked and no comment is refused (finish\n reviewing first); so is deleting `<%= it.vars.reviewFile %>`.\n on:\n \"* **\": review-deciding\n\n review-deciding:\n actor: check\n label: Reviewing\n file: <%= it.vars.reviewFile %>\n mode: review\n # Anchors the INCREMENTAL review base: its most-recent in-process commit is\n # the point the previous review round ended, so the next `reviewing` turn\n # (and the review checkout window) covers only the changes since then. On\n # the first review there is no such commit, so the base is the cycle start.\n reviewBase: true\n script: |\n #!/usr/bin/env bash\n # gtd check turn — decide the human's review step from its CONTENT. HEAD is\n # the human's `await-review -> review-deciding` commit; HEAD^ is the\n # `reviewing` commit that wrote the agent's original REVIEW.md (the sign-off\n # gate refuses an incomplete step, so review-deciding is entered exactly\n # once per round — nothing commits in between). It is FEEDBACK when the\n # human left a comment: a note in REVIEW.md (any edit beyond a \"[ ]\"->\"[x]\"\n # tick) OR a hand-edit to any non-.gtd/ file this round. Otherwise it is a\n # clean sign-off. This turn only CAPTURES the raw material into REVIEW_RAW.md\n # (the `feedback-collecting` agent then turns it into instructions); it does\n # NOT interpret it. Either way, remove REVIEW.md; what the resulting diff\n # MEANS is decided by this state's own `on` rules — the A/M REVIEW_RAW.md\n # rows are declared BEFORE the D REVIEW.md row so a feedback round (which\n # also deletes REVIEW.md) is captured as feedback, not mistaken for a\n # sign-off.\n set +e\n mkdir -p .gtd\n # Hoist the paths once, at the TOP: Eta's autoTrim eats the newline after\n # an interpolation tag, so no tag may be the last token on a line — it\n # would glue the next line onto it and break the script. Below this point\n # everything is plain bash.\n reviewFile=\"<%~ it.vars.reviewFile %>\"\n reviewRawFile=\"<%~ it.vars.reviewRawFile %>\"\n # Which non-.gtd/ files did the human hand-edit this round?\n codeFiles=$(git diff-tree --no-commit-id --name-only -r HEAD | grep -v '^\\.gtd/')\n # A note is any REVIEW.md change beyond a checkbox flip: normalize every\n # \"[ ]\"/\"[x]\" to a placeholder, then compare the agent's original (HEAD^)\n # against the human's version (HEAD).\n notes=\"\"\n before=$(git show \"HEAD^:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n after=$(git show \"HEAD:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n if [ \"$before\" != \"$after\" ]; then notes=1; fi\n if [ -n \"$codeFiles\" ] || [ -n \"$notes\" ]; then\n {\n echo \"Raw review material captured for classification. The human reviewed the diff and left the feedback below; a downstream agent turns it into an instruction list. This file is machine-captured input, not instructions.\"\n if [ -n \"$notes\" ]; then\n echo\n echo \"## Notes the human added to REVIEW.md this round\"\n echo\n echo '```diff'\n git show HEAD --format= -- \"$reviewFile\"\n echo '```'\n fi\n if [ -n \"$codeFiles\" ]; then\n echo\n echo \"## Files the human hand-edited this round (already committed)\"\n echo\n echo '```diff'\n git show HEAD --format= -- $codeFiles\n echo '```'\n fi\n } > \"$reviewRawFile\"\n fi\n rm -f \"$reviewFile\"\n on:\n \"A .gtd/REVIEW_RAW.md\": feedback-collecting\n \"M .gtd/REVIEW_RAW.md\": feedback-collecting\n \"D .gtd/REVIEW.md\": $onSignoff\n\n feedback-collecting:\n actor: agent\n label: Collecting your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n model: <%= it.vars.plannerModel %>\n memory: review\n prompt: |\n You are an autonomous coding agent turning raw review feedback into an\n explicit instruction list for a builder. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewRawFile %>` (delete it when done) and\n `<%= it.vars.reviewFeedbackFile %>` (write it). Do NOT change any other\n file — you classify, you do not build.\n\n The raw review material is: <%~ it.read(it.vars.reviewRawFile) %>\n\n Classify every piece of it into `<%= it.vars.reviewFeedbackFile %>` as a\n flat, numbered instruction list. Each item is `N. ./path/to/file.ts#line —\n <imperative>`. Read the two RAW sections and apply these rules exactly:\n\n 1. NOTES the human added to REVIEW.md — each note is a MANDATORY change,\n regardless of whether its checkbox is ticked (ticking means only \"I read\n this hunk\", never sign-off). Anchor each to the `./path#line` its review\n item names (or the chunk it sits under). Emit it as a concrete\n imperative.\n 2. Every COMMENT the human ADDED this round (a `+` line that is a code\n comment, in the hand-edited files) is a MANDATORY instruction the human\n is giving the builder — even a plain-prose one, even if its box is\n ticked. Emit an imperative to satisfy it and note that the comment line\n itself must be REMOVED once addressed. If a comment turns out to describe\n behaviour the code already has, the instruction is still \"confirm the\n code matches and remove the comment line\".\n 3. Every NON-comment code line the human hand-edited is an already-committed\n INTENT, NOT a do-then-delete item. Emit an instruction that names those\n files and says: their lines are final — complete ONLY the follow-through\n they imply (callers, tests, types, docs) and NEVER revert or rewrite\n them. Do not turn their own edit into a task.\n\n Split each hand-edited hunk per line: comment `+` lines follow rule 2, the\n surrounding real code follows rule 3.\n\n If — and only if — nothing in the raw material is actionable (e.g. the human\n left only an approving remark), write `<%= it.vars.reviewFeedbackFile %>`\n containing exactly one line: `NOTHING ACTIONABLE — <one-sentence reason>`.\n Never invent work to satisfy a non-actionable remark.\n\n Delete `<%= it.vars.reviewRawFile %>`, leave everything else uncommitted,\n and finish your turn.\n on:\n \"A .gtd/REVIEW_FEEDBACK.md\": feedback-building\n \"M .gtd/REVIEW_FEEDBACK.md\": feedback-building\n\n feedback-building:\n actor: agent\n label: Addressing your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: build\n # Refuse a work-free turn: a step whose only pending change is deleting the\n # instructions file (the original \"captured then discarded\" bug). The edge\n # gate (`enforceFeedbackProgressGate` in src/program.ts) reads this flag and\n # exempts a `NOTHING ACTIONABLE` sentinel (a non-actionable review round,\n # which legitimately makes no code change). The pure engine carries it inert.\n requireProgress: true\n prompt: |\n You are an autonomous coding agent addressing review feedback. `.gtd/`\n holds this workflow's own state — never create, edit, or delete anything\n under `.gtd/` except `<%= it.vars.reviewFeedbackFile %>`, which this\n prompt tells you to address and then delete.\n\n The instructions to implement are: <%~ it.read(it.vars.reviewFeedbackFile) %>\n\n This is a flat instruction list, not a diff — execute every item exactly as\n written, no Q&A, no re-planning, using TDD discipline:\n\n - An item that says the human's own lines are final: build ON TOP of them to\n complete only the follow-through it names (callers, tests, types, docs).\n NEVER revert or rewrite the lines they already committed.\n - An item derived from a human comment: satisfy it AND delete that comment\n line from the code (the human's review comments are transient — none of\n them survive this round).\n\n If the file's sole content is a `NOTHING ACTIONABLE` line, make no code\n change; just delete the instructions file.\n\n Delete `<%= it.vars.reviewFeedbackFile %>` once every item is addressed.\n Leave everything else uncommitted and finish your turn. Deleting the\n instructions file WITHOUT making the changes it asks for is refused — the\n feedback must actually be addressed.\n on:\n \"* **\": checking\n\n # ── ENCAPSULATION: the advanced flow's per-package agentic review ──────────\n specGate:\n params: [onApproved]\n states:\n spec-review:\n actor: agent\n label: Reviewing the package\n model: <%= it.vars.plannerModel %>\n memory: review\n # Bounded per cycle: once entered `max` times, a transition into it is\n # redirected to `closing` (force-close the package and move on — the\n # unresolved concern is deferred to the shared human review at the tail,\n # which sees the whole diff anyway). NOTE the cap is PROCESS-scoped, so it\n # pools across packages — the same documented limitation the shared\n # `fixing` cap has (see STATES.md / docs/configuration.md).\n retry:\n max: 3\n otherwise: $onApproved\n prompt: |\n You are an autonomous coding agent reviewing a freshly-built work package\n against its own spec. `.gtd/` holds this workflow's own state — never\n create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.specFeedbackFile %>`, which this prompt tells you to write\n when — and only when — you find problems.\n\n The package spec is: <%~ it.read(it.vars.nextFile) %>\n\n Read that spec, then verify the package's implementation (the diff below)\n against it: are all its tasks done, acceptance criteria met, and the code\n sound and consistent with the codebase?\n\n - If it fully satisfies the spec, write NOTHING and finish your turn — a\n clean turn is your approval.\n - If it does not, write `<%= it.vars.specFeedbackFile %>` listing the\n concrete problems for a fix turn to address. Be specific.\n\n Do not fix anything yourself and do not delete the package file — a later\n step owns that. Finish your turn.\n <% if (it.processDiff.trim()) { %>\n\n ## Diff under review\n\n ```diff\n <%~ it.processDiff %>\n ```\n <% } %>\n on:\n \"A .gtd/SPEC_FEEDBACK.md\": spec-fix\n \"M .gtd/SPEC_FEEDBACK.md\": spec-fix\n \"D .gtd/SPEC_FEEDBACK.md\": $onApproved\n \"C\": $onApproved\n\n spec-fix:\n actor: agent\n label: Fixing review feedback\n file: <%= it.vars.specFeedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: fix\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.specFeedbackFile %>`, which this prompt tells you to address\n and then delete.\n\n Read `<%= it.vars.specFeedbackFile %>` (the reviewer's concerns about the\n current package) and the package spec (`<%= it.read(it.vars.nextFile) %>`),\n then fix the code to resolve every concern. Keep the change focused.\n\n Delete `<%= it.vars.specFeedbackFile %>` once every concern is addressed.\n Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": adv-checking\n\n # ── ENCAPSULATION: the advanced flow's per-package build queue ─────────────\n packageLoop:\n params: [onDrained]\n states:\n picking:\n actor: check\n label: Picking the next package\n script: |\n #!/usr/bin/env bash\n # gtd check turn (the package queue arbiter) — the loop driver executes\n # this verbatim, then steps the check actor. Mechanics only: take the\n # first package file (by name) under the packages dir into NEXT.md, or\n # remove NEXT.md when the queue is empty. What NEXT.md's presence/absence\n # MEANS (another package to build vs. the queue closing out to review) is\n # decided by this state's own `on` rules at capture time — never here.\n set +e\n # Hoist the NEXT path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a line —\n # it would glue the next line's `else`/`fi` onto it and break the script.\n # Below this point everything is plain bash.\n nextFile=\"<%~ it.vars.nextFile %>\"\n next=$(ls <%~ it.vars.packagesDir %>/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n printf '%s' \"$next\" > \"$nextFile\"\n else\n rm -f \"$nextFile\"\n fi\n on:\n \"D .gtd/NEXT.md\": $onDrained\n \"* .gtd/NEXT.md\": adv-building\n \"C\": $onDrained\n\n adv-building:\n actor: agent\n label: Building\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/`; in\n particular do NOT delete the package file (the `spec-review` gate reads it\n after you) and never touch `<%= it.vars.nextFile %>` — the `picking` state\n owns it.\n\n The package to implement is: <%~ it.read(it.vars.nextFile) %>\n\n That file describes a set of INDEPENDENT tasks. Implement ALL of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement EXACTLY the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"* **\": adv-checking\n\n closing:\n actor: check\n label: Closing out the package\n script: |\n #!/usr/bin/env bash\n # gtd check turn (the package closer) — remove the just-reviewed package\n # file (its path is in NEXT.md) plus any leftover spec feedback, so\n # `picking` selects the NEXT package (or falls through to review when the\n # queue is empty). Reached on approval (spec-review clean) and on the\n # spec-review retry cap (force-close, deferring the concern to the tail).\n set +e\n pkg=$(cat <%~ it.vars.nextFile %> 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f <%~ it.vars.specFeedbackFile %> <%~ it.vars.nextFile %>\n on:\n \"* **\": picking\n\nuse:\n # Shared health triple: checking/fixing/escalate -> reviewing.\n - submachine: makeGreen\n name: build\n as: { check: checking, fix: fixing, escalate: escalate }\n with:\n onGreen: reviewing\n escalateDescribe: >-\n Fix the failure yourself — edit the code and/or `.gtd/FEEDBACK.md` —\n to re-run the check (**checking**).\n checkLabel: Running checks\n fixLabel: Fixing the check\n escalateLabel: Escalating to a human\n # Advanced package health triple: adv-checking/adv-fixing/adv-escalate -> spec-review.\n - submachine: makeGreen\n name: pkg\n as: { check: adv-checking, fix: adv-fixing, escalate: adv-escalate }\n with:\n onGreen: spec-review\n escalateDescribe: >-\n Fix the failure yourself — edit the code and/or\n `.gtd/FEEDBACK.md` — to re-run the check (**adv-checking**).\n checkLabel: Running checks\n fixLabel: Fixing the check\n escalateLabel: Escalating to a human\n\n # Green-baseline gate: simple flow entry.\n - submachine: assertGreen\n name: start\n as: { check: start-check, blocked: start-blocked }\n with:\n onGreen: planning\n checkScript: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite before starting new work; a red run\n # records .gtd/FEEDBACK.md (halt), a green run cleans it up (proceed).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n Fix the failing tests — edit the code and/or `.gtd/FEEDBACK.md` — to\n re-run the baseline check (**start-check**). To repair the baseline as\n its own separate reviewed commit instead, abandon this start and run\n `gtd fix` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # Green-baseline gate: advanced flow entry.\n - submachine: assertGreen\n name: adv-start\n as: { check: adv-start-check, blocked: adv-start-blocked }\n with:\n onGreen: adv-grilling\n checkScript: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite before starting the advanced flow; a red\n # run records .gtd/FEEDBACK.md (halt), a green run cleans it up (proceed).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n Fix the failing tests — edit the code and/or `.gtd/FEEDBACK.md` — to\n re-run the baseline check (**adv-start-check**). To repair the baseline\n as its own separate reviewed commit instead, abandon this start and run\n `gtd fix` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # Green-baseline gate: `gtd review` entry (reviewEntry -> reviewing).\n - submachine: assertGreen\n name: review-start\n as: { check: review-start-check, blocked: review-start-blocked }\n set:\n check: { reviewEntry: true }\n with:\n onGreen: reviewing\n checkScript: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite before reviewing; a red run records\n # .gtd/FEEDBACK.md (halt), a green run cleans it up (proceed to reviewing).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n blockedMessage: |\n The test baseline is red — gtd will not start a review on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n Fix the failing tests — edit the code and/or `.gtd/FEEDBACK.md` — to\n re-run the baseline check (**review-start-check**).\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # Advanced flow: product Q&A loop (adv-grilling/adv-grilling-answer -> architecting).\n - submachine: qaLoop\n name: grill\n as: { author: adv-grilling, answer: adv-grilling-answer }\n with:\n authorFile: <%= it.vars.requirementsFile %>\n onAdvance: architecting\n authorPrompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.requirementsFile %>`, which this prompt tells you to develop.\n\n `<%= it.vars.requirementsFile %>` holds the product requirements (or the\n plan you already wrote plus the human's answers on a return lap). Read it,\n explore the codebase, and develop it into a concrete, PRODUCT-level plan\n in this one turn.\n\n Scope: product and user-facing decisions only. Leave implementation\n details (file/module structure, data models, tech-stack choices) for the\n next phase (`<%= it.vars.architectureFile %>`).\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading near the top: one `### <question>` sub-heading\n each, followed by a checkbox list of candidate answers — aim for TWO\n concrete options plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself — the human ticks exactly one per question. The\n last `- [ ] _your answer_` line is the human's own-answer slot; write it\n with the literal `_your answer_` placeholder and leave it for them.\n\n On a RETURN lap the human has ticked one option per question (or replaced\n `_your answer_` with their own text). For each answered question: fold the\n chosen answer into the plan prose, then MOVE the question down into a\n `## Answered Questions` section as `### <question>` followed by the\n resolved answer in plain PROSE (drop the checkboxes). Never re-raise a\n question the human deleted; if the human deleted the whole\n `## Open Questions` section, treat the plan as accepted and surface no new\n questions unless something is genuinely still blocking. Omit\n `## Open Questions` entirely once there are none left.\n\n Leave `<%= it.vars.requirementsFile %>` uncommitted and finish your turn.\n answerMessage: |\n `<%= it.vars.requirementsFile %>` holds the product plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n advanceDescribe: >-\n With no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to accept the\n product plan and move on to technical architecting (**architecting**).\n editDescribe: >-\n Tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**adv-grilling**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n authorLabel: Refining the product plan\n answerLabel: Awaiting your answers\n\n # Advanced flow: technical Q&A loop (architecting/architecting-answer -> decompose).\n - submachine: qaLoop\n name: arch\n as: { author: architecting, answer: architecting-answer }\n with:\n authorFile: <%= it.vars.architectureFile %>\n onAdvance: decompose\n authorPrompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.architectureFile %>` and `<%= it.vars.requirementsFile %>`,\n which this prompt tells you to write and delete respectively.\n\n Read `<%= it.vars.requirementsFile %>` (the converged product plan) and\n develop `<%= it.vars.architectureFile %>` from it in this one turn:\n file/module structure, data models, library/tech-stack choices,\n error-handling strategy — the *how*, building on the *what* already\n settled. Do not re-open product-level decisions; treat them as settled.\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading exactly as the grilling phase does: one\n `### <question>` each, followed by a checkbox list — TWO concrete options\n plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself. On a RETURN lap the human has ticked one option\n per question (or replaced `_your answer_` with their own text): fold each\n chosen answer into the plan prose, then MOVE the question into a\n `## Answered Questions` section as `### <question>` + the resolved answer\n in plain PROSE (drop the checkboxes). Never re-raise a deleted question;\n treat a deleted `## Open Questions` section as acceptance. Omit\n `## Open Questions` once there are none left.\n\n Once `<%= it.vars.architectureFile %>` is written, delete\n `<%= it.vars.requirementsFile %>` — its content has been folded in and it\n must not linger. Leave everything uncommitted and finish your turn.\n answerMessage: |\n `<%= it.vars.architectureFile %>` holds the technical plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n advanceDescribe: >-\n With no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to accept the\n technical plan and move on to decomposing the work into packages\n (**decompose**).\n editDescribe: >-\n Tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**architecting**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n authorLabel: Refining the technical plan\n answerLabel: Awaiting your answers\n\n # Simple flow plan-iteration loop (planning/plan-review -> building).\n - submachine: planLoop\n with:\n onAccept: building\n\n # Shared review tail (reviewing -> await-review -> ... -> squashing).\n - submachine: humanReview\n with:\n onSignoff: squashing\n\n # Advanced per-package agentic review (spec-review/spec-fix -> closing).\n - submachine: specGate\n with:\n onApproved: closing\n\n # Advanced per-package build queue (picking/adv-building/closing -> reviewing).\n - submachine: packageLoop\n with:\n onDrained: reviewing\n\nstates:\n # ── Shared entry ────────────────────────────────────────────────────────\n idle:\n actor: human\n initial: true\n label: Idle\n # Every human-gate `message:` below closes with the same rendered \"What\n # each change does next\" list, built from `it.edges` (the resting state's\n # own `on` edges — each edge's `describe:` is the single source of truth).\n message: |\n No active gtd cycle.\n\n To start one, create a steering file describing what you want built:\n\n - `.gtd/TODO.md` — a short sketch — starts the SIMPLE flow (plan, build,\n review).\n - `.gtd/REQUIREMENTS.md` — the product requirements — starts the ADVANCED\n flow (product + technical Q&A, package decomposition, parallel build,\n per-package agentic review).\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* .gtd/REQUIREMENTS.md\":\n to: adv-start-check\n describe: >-\n Save the product requirements to `.gtd/REQUIREMENTS.md` to start the\n ADVANCED flow — gtd first checks the test baseline is green, then the\n agent develops them through product then technical Q&A, decomposes the\n work into packages, and builds each in parallel (**adv-start-check**).\n \"* **\":\n to: start-check\n describe: >-\n Save a short sketch to `.gtd/TODO.md` (or any other change) to start\n the SIMPLE flow — gtd first checks the test baseline is green, then the\n agent reads it, explores the codebase, and develops it into a concrete\n implementation plan (**start-check**).\n\n # ── Simple flow: monolithic build (plan loop is the `planLoop` sub-machine) ─\n building:\n actor: agent\n label: Building\n file: <%= it.vars.todoFile %>\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create or edit anything under `.gtd/` except deleting\n `<%= it.vars.todoFile %>`, which this prompt tells you to delete once its\n work is complete.\n\n The plan to implement is: <%~ it.read(it.vars.todoFile) %>\n\n Read that plan and implement it in full. Use TDD discipline: one test,\n then the implementation that passes it, then the next — never write all\n the tests first.\n\n Delete `<%= it.vars.todoFile %>` once the work it describes is complete\n and verified. Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": checking\n\n # ── Advanced flow: decomposition (Q&A loops are the `qaLoop` sub-machine) ───\n decompose:\n actor: agent\n label: Decomposing into packages\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except the\n package files this prompt tells you to write (under\n `<%= it.vars.packagesDir %>/`), and `<%= it.vars.architectureFile %>`,\n which it tells you to delete.\n\n Read `<%= it.vars.architectureFile %>` (the converged technical plan) and\n decompose it into an ordered set of WORK PACKAGES, one file each under\n `<%= it.vars.packagesDir %>/` (e.g. `<%= it.vars.packagesDir %>/01-name.md`,\n `<%= it.vars.packagesDir %>/02-name.md`, ...). Order the packages so the\n test suite can pass again after each one lands.\n\n Each package file describes a set of INDEPENDENT tasks that can be\n implemented concurrently without ordering constraints between them — a\n single build turn will fan them out to parallel subagents. Give each task\n a clear description, acceptance criteria as `- [ ]` checkboxes, and the\n relevant paths. Also state the package's overall requirement so it can be\n reviewed against its spec. Do NOT put ordering-dependent tasks in the same\n package — split them across ordered packages instead.\n\n No package file may reference any other `.gtd/` file. Once the package\n files are written, delete `<%= it.vars.architectureFile %>`. Leave\n everything uncommitted and finish your turn.\n on:\n \"* .gtd/packages/**\": picking\n\n # ── Green-baseline gate: `gtd fix` entry ──────────────────────────────────\n # `gtd fix` enters HERE (fixEntry): run the suite, then a red run drops\n # straight into the shared `fixing` loop (-> checking -> review + squash\n # tail); a green run is a no-op back to `idle` (nothing to fix).\n fix-check:\n actor: check\n fixEntry: true\n label: Checking the baseline\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite to find what needs fixing; a red run\n # records .gtd/FEEDBACK.md (-> fixing), a green run cleans it up (-> idle).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A .gtd/FEEDBACK.md\": fixing\n \"M .gtd/FEEDBACK.md\": fixing\n \"D .gtd/FEEDBACK.md\": idle\n \"C\": idle\n\n # ── Shared tail: squash finale (review loop is the `humanReview` sub-machine) ─\n squashing:\n actor: agent\n label: Squashing\n file: <%= it.vars.commitMsgFile %>\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. The cycle is approved and done.\n `.gtd/` holds this workflow's own state — never create, edit, or delete\n anything under `.gtd/` except `<%= it.vars.commitMsgFile %>`, which this\n prompt tells you to write.\n\n Leave `<%= it.vars.commitMsgFile %>` uncommitted and finish your turn —\n entering this file squashes everything this process retains into one\n commit using its content as the message. The diff below is exactly what\n that commit will contain: `it.retainedDiff`, the changes THIS process\n made (its trace/retry boundary onward). For a normal build cycle that is\n the whole feature; for a `gtd review` process it is only the fixes made\n during the review — NOT the changeset that was under review.\n <% if (it.retainedDiff.trim()) { %>\n\n Write `<%= it.vars.commitMsgFile %>` with ONE conventional-commits message\n for the changes below (subject line `type(scope): subject`, imperative\n mood, <= 72 characters; a body explaining the why, trade-offs, and key\n decisions). Plain text, no markdown wrapper.\n\n ## Retained diff\n\n ```diff\n <%~ it.retainedDiff %>\n ```\n <% } else { %>\n\n This process retained NO changes (a clean review sign-off with no fixes).\n Write `<%= it.vars.commitMsgFile %>` with exactly this one line and\n nothing else:\n\n chore: human review\n <% } %>\n on:\n \"A .gtd/COMMIT_MSG.md\": done\n \"M .gtd/COMMIT_MSG.md\": done\n\n done:\n commit: |\n <%~ it.read(it.vars.commitMsgFile) %>\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n";
190597
+ var unified_default = "# The unified gtd workflow template — gtd's BUILT-IN DEFAULT workflow. When no\n# `workflow:` key is configured anywhere in the cwd→home config chain,\n# src/Config.ts runs this template directly, so a state command works with no\n# configuration at all. `gtd init` seeds only the settings a project tunes\n# (vars.testCommand + a modes: formatting suggestion), never the workflow\n# itself. See STATES.md §10 for the full walkthrough.\n#\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/templates.ts does the compiling). Every content string below\n# is INLINE (no `./`-relative file references): the bundled template ships\n# inside a single-file build (`dist/gtd.bundle.mjs`), so its content must\n# already be resolved at BUILD time. `renderInitConfig` (src/workflows/\n# templates.ts) can MATERIALIZE this inline source into a `.gtdrc.json`\n# `workflow:` value — the way to eject/customize the machine, and the hermetic\n# test fixture — but `gtd init` itself no longer writes the workflow.\n#\n# ── Authored with SUB-MACHINES (Option C) ──────────────────────────────────\n#\n# This template is written with `submachines:`/`use:` (see src/Submachines.ts\n# and STATES.md). Expansion is a compile-time raw → raw pre-pass with NO runtime\n# footprint: the pure engine only ever sees the flat concrete states, and each\n# invocation's `as:` rename map reproduces the exact state names the flat\n# template used — so the compiled `WorkflowDefinition` is BYTE-IDENTICAL to the\n# frozen `unified.flat.yaml` (pinned by src/workflows/golden.test.ts). The\n# sub-machines split into DEDUP (a gate/loop authored once, invoked several\n# times) and ENCAPSULATION (a single-instance cluster grouped for reading):\n#\n# * makeGreen (dedup ×2) — the check/fix/escalate health triple shared by\n# the simple build tail and the advanced package\n# loop (identical script/prompt/message).\n# * assertGreen (dedup ×3) — the green-baseline gate (check + human blocked)\n# guarding the simple, advanced and review entries.\n# * qaLoop (dedup ×2) — the checkbox Q&A author/answer loop shared by the\n# product (adv-grilling) and technical (architecting)\n# phases of the advanced flow.\n# * planLoop (encap) — the simple flow's plan-iteration loop.\n# * humanReview (encap) — the shared review tail (reviewing → await-review →\n# review-deciding → feedback collect/build).\n# * specGate (encap) — the advanced flow's per-package agentic review.\n# * packageLoop (encap) — the advanced flow's per-package build queue.\n#\n# ── Four entry points behind a green-baseline gate, one shared tail ─────────\n#\n# EVERY entry first runs the test suite and only proceeds when it is GREEN — you\n# never start new work (or a review) on top of a red baseline. A red run halts\n# at a human `*-start-blocked` gate (fix the failures and re-run, exactly like\n# the `escalate` states), or is what `gtd fix` exists to repair.\n#\n# The initial `idle` state FORKS on which steering file the human creates,\n# routing into that flow's OWN start gate:\n#\n# * .gtd/REQUIREMENTS.md -> `adv-start-check` -> the ADVANCED flow (product +\n# technical Q&A, package decomposition, per-package parallel build +\n# agentic spec-review).\n# * anything else (e.g. .gtd/TODO.md) -> `start-check` -> the SIMPLE flow (one\n# grilling Q&A loop, a monolithic build, no decomposition, no agentic\n# review).\n#\n# A third entry, `gtd review <commitish>`, jumps to `review-start-check`\n# (reviewEntry) and, once green, to `reviewing` to review a foreign branch\n# through the same shared tail. A fourth, `gtd fix`, jumps to `fix-check`\n# (fixEntry): a red suite drops straight into the shared `fixing` loop and out\n# through the review + squash tail; a green suite is a no-op back to `idle`.\n#\n# All three converge at `reviewing` -> `await-review`. Ticking a REVIEW.md box\n# means only \"I reviewed this hunk\"; a COMMENT (not an unticked box) is what asks\n# for changes. Every human step at `await-review` goes to `review-deciding`,\n# which inspects the step and routes:\n# * no comment (every box ticked, no REVIEW.md note, no code edit) -> the diff\n# is a bare REVIEW.md deletion -> squashing -> done (the ONLY path to the\n# finale — the whole cycle collapses into one commit).\n# * any comment — a note in REVIEW.md OR a hand-edit to code — ->\n# review-deciding writes REVIEW_FEEDBACK.md -> feedback-building implements\n# it (completing any code the human hand-edited, never reverting their\n# lines) -> checking -> reviewing regenerates an INCREMENTAL REVIEW.md\n# (review-deciding carries `reviewBase: true`, so each feedback round's\n# review + checkout window covers only `last-review..HEAD`, i.e. the agent's\n# follow-through, not the human's own edit) -> await-review, looping until\n# sign-off.\n# Two dead ends never commit: the sign-off gate (src/program.ts) REFUSES a step\n# that leaves a box unticked with no comment (finish reviewing first) and a\n# deleted REVIEW.md (restore it and tick the boxes).\n#\n# The shared health segment (`checking`/`fixing`/`escalate`) serves the simple\n# build and the feedback loop — its green edge targets\n# `reviewing`. The advanced per-package loop keeps its OWN health triple\n# (`adv-checking`/`adv-fixing`/`adv-escalate`) because its green edge targets\n# the per-package `spec-review` gate instead.\n#\n# Every `on` pattern's path below is written as the matching var's Eta\n# template (e.g. `\"A <%= it.vars.feedbackFile %>\"`), not a literal `.gtd/...`\n# path — repointing a var reroutes the machine along with every template that\n# reads/writes it (see docs/configuration.md).\n\nvars:\n testCommand: npm test\n todoFile: .gtd/TODO.md\n requirementsFile: .gtd/REQUIREMENTS.md\n architectureFile: .gtd/ARCHITECTURE.md\n packagesDir: .gtd/packages\n nextFile: .gtd/NEXT.md\n reviewFile: .gtd/REVIEW.md\n reviewRawFile: .gtd/REVIEW_RAW.md\n reviewFeedbackFile: .gtd/REVIEW_FEEDBACK.md\n feedbackFile: .gtd/FEEDBACK.md\n specFeedbackFile: .gtd/SPEC_FEEDBACK.md\n commitMsgFile: .gtd/COMMIT_MSG.md\n # The two opaque model tiers every agent state draws from — repoint them here\n # (or via a top-level `.gtdrc` `vars:` key / a `GTD_VAR_plannerModel` env var)\n # to change every state on that tier at once. `plannerModel` (default \"smart\")\n # is the heavier tier for the one-shot planning/architecting/review turns;\n # `coderModel` (default \"base\") is the tier for the build/fix turns.\n plannerModel: smart\n coderModel: base\n\nsubmachines:\n # ── DEDUP: the check/fix/escalate health triple ───────────────────────────\n # Runs the suite; a red run records .gtd/FEEDBACK.md, a green run cleans it up.\n # The `on` rules turn that into \"fix\" vs \"$onGreen\". The fix turn retries up to\n # 3× before escalating to a human. Invoked for the shared build tail\n # (checking/fixing/escalate -> reviewing) and the advanced package loop\n # (adv-checking/adv-fixing/adv-escalate -> spec-review).\n makeGreen:\n params: [onGreen, escalateDescribe, checkLabel, fixLabel, escalateLabel]\n states:\n check:\n actor: check\n label: $checkLabel\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A <%= it.vars.feedbackFile %>\": fix\n \"M <%= it.vars.feedbackFile %>\": fix\n \"D <%= it.vars.feedbackFile %>\": $onGreen\n \"C\": $onGreen\n fix:\n actor: agent\n label: $fixLabel\n file: <%= it.vars.feedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: fix\n retry:\n max: 3\n otherwise: escalate\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.feedbackFile %>`, which this prompt tells you to address.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue.\n\n Leave everything uncommitted and finish your turn — do not commit.\n on:\n \"* **\": check\n escalate:\n actor: human\n label: $escalateLabel\n file: <%= it.vars.feedbackFile %>\n message: |\n The agent could not get the check to pass after repeated attempts.\n `<%= it.vars.feedbackFile %>` holds the last failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n describe: $escalateDescribe\n\n # ── DEDUP: the green-baseline gate ─────────────────────────────────────────\n # Runs the suite before any work starts (its script is the health check's test\n # wrapper — red -> .gtd/FEEDBACK.md, green -> remove it); the `on` rules turn\n # that into \"halt at blocked\" vs \"proceed to $onGreen\". The script and blocked\n # message/describe differ per entry, so they ride in as `with:` params.\n assertGreen:\n params: [onGreen, checkScript, blockedMessage, blockedDescribe, checkLabel, blockedLabel]\n states:\n check:\n actor: check\n label: $checkLabel\n script: $checkScript\n on:\n \"A <%= it.vars.feedbackFile %>\": blocked\n \"M <%= it.vars.feedbackFile %>\": blocked\n \"D <%= it.vars.feedbackFile %>\": $onGreen\n \"C\": $onGreen\n blocked:\n actor: human\n label: $blockedLabel\n file: <%= it.vars.feedbackFile %>\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n describe: $blockedDescribe\n\n # ── DEDUP: the checkbox Q&A author/answer loop ─────────────────────────────\n # The agent develops a plan file, surfacing `## Open Questions` as checkbox\n # lists; the human answers (answerGate refuses an unanswered step) and either\n # advances or sends it back. Invoked for the product (adv-grilling) and\n # technical (architecting) phases of the advanced flow.\n qaLoop:\n params:\n [\n authorFile,\n authorPrompt,\n answerMessage,\n onAdvance,\n advanceDescribe,\n editDescribe,\n authorLabel,\n answerLabel,\n ]\n states:\n author:\n actor: agent\n label: $authorLabel\n file: $authorFile\n mode: qa\n model: <%= it.vars.plannerModel %>\n memory: plan\n prompt: $authorPrompt\n on:\n \"* **\": answer\n answer:\n actor: human\n label: $answerLabel\n file: $authorFile\n mode: qa\n answerGate: true\n message: $answerMessage\n on:\n \"C\":\n to: $onAdvance\n describe: $advanceDescribe\n \"* **\":\n to: author\n describe: $editDescribe\n\n # ── ENCAPSULATION: the simple flow's plan-iteration loop ───────────────────\n planLoop:\n params: [onAccept]\n states:\n planning:\n actor: agent\n label: Refining the plan\n file: <%= it.vars.todoFile %>\n model: <%= it.vars.plannerModel %>\n memory: plan\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.todoFile %>`, which this prompt tells you to develop.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build — or, on a\n return lap, the plan you already wrote plus the human's edits and comments\n on it. Read it, explore the codebase, and develop it into a concrete\n implementation plan: the files to change, the approach, and the steps to\n take.\n\n Do NOT ask questions and do NOT add an \"Open Questions\" section — decide\n every open point yourself and write the plan as a set of concrete\n decisions. Where a decision is genuinely a judgement call, state the\n choice you made and, in one line, why. The human iterates on the plan by\n editing it directly, not by answering questions.\n\n On a return lap, `<%= it.vars.todoFile %>` carries the human's edits and\n inline comments on your previous plan. Fold every edit and comment into a\n revised, self-contained plan and remove the addressed comments — the plan\n is always the current, clean proposal, never a diff.\n\n Leave `<%= it.vars.todoFile %>` uncommitted and finish your turn — do\n not commit, and do not run `gtd step agent` yourself; the harness does\n that.\n on:\n \"* **\": plan-review\n\n plan-review:\n actor: human\n label: Awaiting your review\n file: <%= it.vars.todoFile %>\n message: |\n `<%= it.vars.todoFile %>` holds the proposed implementation plan. Read it,\n then either accept it as-is or edit it — rewrite a step, add a\n constraint, or drop an inline comment — to send it back for another round.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: $onAccept\n describe: >-\n Change nothing to accept the plan as-is and move on to implementation\n (**building**).\n \"* **\":\n to: planning\n describe: >-\n Edit the plan — rewrite a step, add a constraint, or leave an inline\n comment — to send it back for another round (**planning**).\n\n # ── ENCAPSULATION: the shared review tail ──────────────────────────────────\n humanReview:\n params: [onSignoff]\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: <%= it.vars.reviewFile %>\n mode: review\n model: <%= it.vars.plannerModel %>\n memory: review\n # `gtd review <commitish>` (a plain non-gtd branch, e.g. a colleague's PR)\n # does NOT enter here directly — it enters `review-start-check` (reviewEntry)\n # first, which runs the green-baseline gate and, once green, transitions\n # here. The `Gtd-Review-Base:` trailer rides on that entry commit (the\n # process's oldest), so everything below still reuses the same\n # reviewing/await-review/feedback machinery over <commitish>..HEAD.\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewFile %>`, which this prompt tells you to write.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the diff below in\n this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.startCommit %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation of\n what changed and why, then one `- [ ] ./path/to/file.ts#42 — note` file\n pointer per hunk (checkboxes are for the human to tick, not you; every\n path is `./`-relative, with an optional `#line` and an optional\n ` — note`).\n\n The diff below is `it.reviewDiff` — on a feedback round it covers ONLY the\n changes made since the previous review, so you review just what changed,\n not items already signed off. Leave `<%= it.vars.reviewFile %>`\n uncommitted and finish your turn.\n <% if (it.reviewDiff.trim()) { %>\n\n ## Full diff under review\n\n ```diff\n <%~ it.reviewDiff %>\n ```\n <% } %>\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n label: Awaiting your review\n file: <%= it.vars.reviewFile %>\n mode: review\n # While the cycle rests here, gtd opens a \"review checkout window\": raw HEAD\n # is rewound to the review base (the most-recent `review-deciding` commit —\n # so a feedback round shows only `last-review..HEAD` — or the process start\n # on the first review) with the working tree untouched, surfacing the diff\n # as ordinary uncommitted changes. It closes on the next invocation.\n #\n # Every human step here routes to `review-deciding`, which decides sign-off\n # vs. feedback from the step's CONTENT (a comment, not a tick). Ticking a box\n # only records \"I reviewed this hunk\". Two dead ends can't be told apart by a\n # file-pattern edge, so the sign-off gate (`enforceReviewSignoffGate` in\n # src/program.ts) catches them BEFORE the step commits and refuses: an\n # unfinished review (a box still `- [ ]` with no comment) and a deleted\n # REVIEW.md.\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the cycle — one\n `- [ ]` checkbox per reviewable item, grouped into chunks. Tick a box\n (`- [x]`) as you review each hunk; ticking only records that you've read\n it, it is not sign-off.\n\n When you've been through the whole diff, run `gtd step human`:\n\n - Tick EVERY box and leave no comment — no note in\n `<%= it.vars.reviewFile %>`, no code edit — to sign off and collapse the\n cycle into one commit (**squashing**).\n - Leave a comment to request changes — a note on a\n `<%= it.vars.reviewFile %>` line, or a direct code edit — to send it to a\n build + re-review round (**review-deciding** → **feedback-building**). A\n code edit counts as your own fix: the agent completes the follow-through\n it implies and never reverts your lines.\n\n Stepping with a box still unticked and no comment is refused (finish\n reviewing first); so is deleting `<%= it.vars.reviewFile %>`.\n on:\n \"* **\": review-deciding\n\n review-deciding:\n actor: check\n label: Reviewing\n file: <%= it.vars.reviewFile %>\n mode: review\n # Anchors the INCREMENTAL review base: its most-recent in-process commit is\n # the point the previous review round ended, so the next `reviewing` turn\n # (and the review checkout window) covers only the changes since then. On\n # the first review there is no such commit, so the base is the cycle start.\n reviewBase: true\n script: |\n #!/usr/bin/env bash\n # gtd check turn — decide the human's review step from its CONTENT. HEAD is\n # the human's `await-review -> review-deciding` commit; HEAD^ is the\n # `reviewing` commit that wrote the agent's original REVIEW.md (the sign-off\n # gate refuses an incomplete step, so review-deciding is entered exactly\n # once per round — nothing commits in between). It is FEEDBACK when the\n # human left a comment: a note in REVIEW.md (any edit beyond a \"[ ]\"->\"[x]\"\n # tick) OR a hand-edit to any non-.gtd/ file this round. Otherwise it is a\n # clean sign-off. This turn only CAPTURES the raw material into REVIEW_RAW.md\n # (the `feedback-collecting` agent then turns it into instructions); it does\n # NOT interpret it. Either way, remove REVIEW.md; what the resulting diff\n # MEANS is decided by this state's own `on` rules — the A/M REVIEW_RAW.md\n # rows are declared BEFORE the D REVIEW.md row so a feedback round (which\n # also deletes REVIEW.md) is captured as feedback, not mistaken for a\n # sign-off.\n set +e\n mkdir -p .gtd\n # Hoist the paths once, at the TOP: Eta's autoTrim eats the newline after\n # an interpolation tag, so no tag may be the last token on a line — it\n # would glue the next line onto it and break the script. Below this point\n # everything is plain bash.\n reviewFile=\"<%~ it.vars.reviewFile %>\"\n reviewRawFile=\"<%~ it.vars.reviewRawFile %>\"\n # Which non-.gtd/ files did the human hand-edit this round?\n codeFiles=$(git diff-tree --no-commit-id --name-only -r HEAD | grep -v '^\\.gtd/')\n # A note is any REVIEW.md change beyond a checkbox flip: normalize every\n # \"[ ]\"/\"[x]\" to a placeholder, then compare the agent's original (HEAD^)\n # against the human's version (HEAD).\n notes=\"\"\n before=$(git show \"HEAD^:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n after=$(git show \"HEAD:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n if [ \"$before\" != \"$after\" ]; then notes=1; fi\n if [ -n \"$codeFiles\" ] || [ -n \"$notes\" ]; then\n {\n echo \"Raw review material captured for classification. The human reviewed the diff and left the feedback below; a downstream agent turns it into an instruction list. This file is machine-captured input, not instructions.\"\n if [ -n \"$notes\" ]; then\n echo\n echo \"## Notes the human added to REVIEW.md this round\"\n echo\n echo '```diff'\n git show HEAD --format= -- \"$reviewFile\"\n echo '```'\n fi\n if [ -n \"$codeFiles\" ]; then\n echo\n echo \"## Files the human hand-edited this round (already committed)\"\n echo\n echo '```diff'\n git show HEAD --format= -- $codeFiles\n echo '```'\n fi\n } > \"$reviewRawFile\"\n fi\n rm -f \"$reviewFile\"\n on:\n \"A <%= it.vars.reviewRawFile %>\": feedback-collecting\n \"M <%= it.vars.reviewRawFile %>\": feedback-collecting\n \"D <%= it.vars.reviewFile %>\": $onSignoff\n\n feedback-collecting:\n actor: agent\n label: Collecting your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n model: <%= it.vars.plannerModel %>\n memory: review\n prompt: |\n You are an autonomous coding agent turning raw review feedback into an\n explicit instruction list for a builder. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewRawFile %>` (delete it when done) and\n `<%= it.vars.reviewFeedbackFile %>` (write it). Do NOT change any other\n file — you classify, you do not build.\n\n The raw review material is: <%~ it.read(it.vars.reviewRawFile) %>\n\n Classify every piece of it into `<%= it.vars.reviewFeedbackFile %>` as a\n flat, numbered instruction list. Each item is `N. ./path/to/file.ts#line —\n <imperative>`. Read the two RAW sections and apply these rules exactly:\n\n 1. NOTES the human added to REVIEW.md — each note is a MANDATORY change,\n regardless of whether its checkbox is ticked (ticking means only \"I read\n this hunk\", never sign-off). Anchor each to the `./path#line` its review\n item names (or the chunk it sits under). Emit it as a concrete\n imperative.\n 2. Every COMMENT the human ADDED this round (a `+` line that is a code\n comment, in the hand-edited files) is a MANDATORY instruction the human\n is giving the builder — even a plain-prose one, even if its box is\n ticked. Emit an imperative to satisfy it and note that the comment line\n itself must be REMOVED once addressed. If a comment turns out to describe\n behaviour the code already has, the instruction is still \"confirm the\n code matches and remove the comment line\".\n 3. Every NON-comment code line the human hand-edited is an already-committed\n INTENT, NOT a do-then-delete item. Emit an instruction that names those\n files and says: their lines are final — complete ONLY the follow-through\n they imply (callers, tests, types, docs) and NEVER revert or rewrite\n them. Do not turn their own edit into a task.\n\n Split each hand-edited hunk per line: comment `+` lines follow rule 2, the\n surrounding real code follows rule 3.\n\n If — and only if — nothing in the raw material is actionable (e.g. the human\n left only an approving remark), write `<%= it.vars.reviewFeedbackFile %>`\n containing exactly one line: `NOTHING ACTIONABLE — <one-sentence reason>`.\n Never invent work to satisfy a non-actionable remark.\n\n Delete `<%= it.vars.reviewRawFile %>`, leave everything else uncommitted,\n and finish your turn.\n on:\n \"A <%= it.vars.reviewFeedbackFile %>\": feedback-building\n \"M <%= it.vars.reviewFeedbackFile %>\": feedback-building\n\n feedback-building:\n actor: agent\n label: Addressing your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: build\n # Refuse a work-free turn: a step whose only pending change is deleting the\n # instructions file (the original \"captured then discarded\" bug). The edge\n # gate (`enforceFeedbackProgressGate` in src/program.ts) reads this flag and\n # exempts a `NOTHING ACTIONABLE` sentinel (a non-actionable review round,\n # which legitimately makes no code change). The pure engine carries it inert.\n requireProgress: true\n prompt: |\n You are an autonomous coding agent addressing review feedback. `.gtd/`\n holds this workflow's own state — never create, edit, or delete anything\n under `.gtd/` except `<%= it.vars.reviewFeedbackFile %>`, which this\n prompt tells you to address and then delete.\n\n The instructions to implement are: <%~ it.read(it.vars.reviewFeedbackFile) %>\n\n This is a flat instruction list, not a diff — execute every item exactly as\n written, no Q&A, no re-planning, using TDD discipline:\n\n - An item that says the human's own lines are final: build ON TOP of them to\n complete only the follow-through it names (callers, tests, types, docs).\n NEVER revert or rewrite the lines they already committed.\n - An item derived from a human comment: satisfy it AND delete that comment\n line from the code (the human's review comments are transient — none of\n them survive this round).\n\n If the file's sole content is a `NOTHING ACTIONABLE` line, make no code\n change; just delete the instructions file.\n\n Delete `<%= it.vars.reviewFeedbackFile %>` once every item is addressed.\n Leave everything else uncommitted and finish your turn. Deleting the\n instructions file WITHOUT making the changes it asks for is refused — the\n feedback must actually be addressed.\n on:\n \"* **\": checking\n\n # ── ENCAPSULATION: the advanced flow's per-package agentic review ──────────\n specGate:\n params: [onApproved]\n states:\n spec-review:\n actor: agent\n label: Reviewing the package\n model: <%= it.vars.plannerModel %>\n memory: review\n # Bounded per cycle: once entered `max` times, a transition into it is\n # redirected to `closing` (force-close the package and move on — the\n # unresolved concern is deferred to the shared human review at the tail,\n # which sees the whole diff anyway). NOTE the cap is PROCESS-scoped, so it\n # pools across packages — the same documented limitation the shared\n # `fixing` cap has (see STATES.md / docs/configuration.md).\n retry:\n max: 3\n otherwise: $onApproved\n prompt: |\n You are an autonomous coding agent reviewing a freshly-built work package\n against its own spec. `.gtd/` holds this workflow's own state — never\n create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.specFeedbackFile %>`, which this prompt tells you to write\n when — and only when — you find problems.\n\n The package spec is: <%~ it.read(it.vars.nextFile) %>\n\n Read that spec, then verify the package's implementation (the diff below)\n against it: are all its tasks done, acceptance criteria met, and the code\n sound and consistent with the codebase?\n\n - If it fully satisfies the spec, write NOTHING and finish your turn — a\n clean turn is your approval.\n - If it does not, write `<%= it.vars.specFeedbackFile %>` listing the\n concrete problems for a fix turn to address. Be specific.\n\n Do not fix anything yourself and do not delete the package file — a later\n step owns that. Finish your turn.\n <% if (it.processDiff.trim()) { %>\n\n ## Diff under review\n\n ```diff\n <%~ it.processDiff %>\n ```\n <% } %>\n on:\n \"A <%= it.vars.specFeedbackFile %>\": spec-fix\n \"M <%= it.vars.specFeedbackFile %>\": spec-fix\n \"D <%= it.vars.specFeedbackFile %>\": $onApproved\n \"C\": $onApproved\n\n spec-fix:\n actor: agent\n label: Fixing review feedback\n file: <%= it.vars.specFeedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: fix\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.specFeedbackFile %>`, which this prompt tells you to address\n and then delete.\n\n Read `<%= it.vars.specFeedbackFile %>` (the reviewer's concerns about the\n current package) and the package spec (`<%= it.read(it.vars.nextFile) %>`),\n then fix the code to resolve every concern. Keep the change focused.\n\n Delete `<%= it.vars.specFeedbackFile %>` once every concern is addressed.\n Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": adv-checking\n\n # ── ENCAPSULATION: the advanced flow's per-package build queue ─────────────\n packageLoop:\n params: [onDrained]\n states:\n picking:\n actor: check\n label: Picking the next package\n script: |\n #!/usr/bin/env bash\n # gtd check turn (the package queue arbiter) — the loop driver executes\n # this verbatim, then steps the check actor. Mechanics only: take the\n # first package file (by name) under the packages dir into NEXT.md, or\n # remove NEXT.md when the queue is empty. What NEXT.md's presence/absence\n # MEANS (another package to build vs. the queue closing out to review) is\n # decided by this state's own `on` rules at capture time — never here.\n set +e\n # Hoist the NEXT path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a line —\n # it would glue the next line's `else`/`fi` onto it and break the script.\n # Below this point everything is plain bash.\n nextFile=\"<%~ it.vars.nextFile %>\"\n next=$(ls <%~ it.vars.packagesDir %>/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n printf '%s' \"$next\" > \"$nextFile\"\n else\n rm -f \"$nextFile\"\n fi\n on:\n \"D <%= it.vars.nextFile %>\": $onDrained\n \"* <%= it.vars.nextFile %>\": adv-building\n \"C\": $onDrained\n\n adv-building:\n actor: agent\n label: Building\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/`; in\n particular do NOT delete the package file (the `spec-review` gate reads it\n after you) and never touch `<%= it.vars.nextFile %>` — the `picking` state\n owns it.\n\n The package to implement is: <%~ it.read(it.vars.nextFile) %>\n\n That file describes a set of INDEPENDENT tasks. Implement ALL of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement EXACTLY the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"* **\": adv-checking\n\n closing:\n actor: check\n label: Closing out the package\n script: |\n #!/usr/bin/env bash\n # gtd check turn (the package closer) — remove the just-reviewed package\n # file (its path is in NEXT.md) plus any leftover spec feedback, so\n # `picking` selects the NEXT package (or falls through to review when the\n # queue is empty). Reached on approval (spec-review clean) and on the\n # spec-review retry cap (force-close, deferring the concern to the tail).\n set +e\n pkg=$(cat <%~ it.vars.nextFile %> 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f <%~ it.vars.specFeedbackFile %> <%~ it.vars.nextFile %>\n on:\n \"* **\": picking\n\nuse:\n # Shared health triple: checking/fixing/escalate -> reviewing.\n - submachine: makeGreen\n name: build\n as: { check: checking, fix: fixing, escalate: escalate }\n with:\n onGreen: reviewing\n escalateDescribe: >-\n Fix the failure yourself — edit the code and/or `.gtd/FEEDBACK.md` —\n to re-run the check (**checking**).\n checkLabel: Running checks\n fixLabel: Fixing the check\n escalateLabel: Escalating to a human\n # Advanced package health triple: adv-checking/adv-fixing/adv-escalate -> spec-review.\n - submachine: makeGreen\n name: pkg\n as: { check: adv-checking, fix: adv-fixing, escalate: adv-escalate }\n with:\n onGreen: spec-review\n escalateDescribe: >-\n Fix the failure yourself — edit the code and/or\n `.gtd/FEEDBACK.md` — to re-run the check (**adv-checking**).\n checkLabel: Running checks\n fixLabel: Fixing the check\n escalateLabel: Escalating to a human\n\n # Green-baseline gate: simple flow entry.\n - submachine: assertGreen\n name: start\n as: { check: start-check, blocked: start-blocked }\n with:\n onGreen: planning\n checkScript: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite before starting new work; a red run\n # records .gtd/FEEDBACK.md (halt), a green run cleans it up (proceed).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n Fix the failing tests — edit the code and/or `.gtd/FEEDBACK.md` — to\n re-run the baseline check (**start-check**). To repair the baseline as\n its own separate reviewed commit instead, abandon this start and run\n `gtd fix` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # Green-baseline gate: advanced flow entry.\n - submachine: assertGreen\n name: adv-start\n as: { check: adv-start-check, blocked: adv-start-blocked }\n with:\n onGreen: adv-grilling\n checkScript: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite before starting the advanced flow; a red\n # run records .gtd/FEEDBACK.md (halt), a green run cleans it up (proceed).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n Fix the failing tests — edit the code and/or `.gtd/FEEDBACK.md` — to\n re-run the baseline check (**adv-start-check**). To repair the baseline\n as its own separate reviewed commit instead, abandon this start and run\n `gtd fix` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # Green-baseline gate: `gtd review` entry (reviewEntry -> reviewing).\n - submachine: assertGreen\n name: review-start\n as: { check: review-start-check, blocked: review-start-blocked }\n set:\n check: { reviewEntry: true }\n with:\n onGreen: reviewing\n checkScript: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite before reviewing; a red run records\n # .gtd/FEEDBACK.md (halt), a green run cleans it up (proceed to reviewing).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n blockedMessage: |\n The test baseline is red — gtd will not start a review on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n Fix the failing tests — edit the code and/or `.gtd/FEEDBACK.md` — to\n re-run the baseline check (**review-start-check**).\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # Advanced flow: product Q&A loop (adv-grilling/adv-grilling-answer -> architecting).\n - submachine: qaLoop\n name: grill\n as: { author: adv-grilling, answer: adv-grilling-answer }\n with:\n authorFile: <%= it.vars.requirementsFile %>\n onAdvance: architecting\n authorPrompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.requirementsFile %>`, which this prompt tells you to develop.\n\n `<%= it.vars.requirementsFile %>` holds the product requirements (or the\n plan you already wrote plus the human's answers on a return lap). Read it,\n explore the codebase, and develop it into a concrete, PRODUCT-level plan\n in this one turn.\n\n Scope: product and user-facing decisions only. Leave implementation\n details (file/module structure, data models, tech-stack choices) for the\n next phase (`<%= it.vars.architectureFile %>`).\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading near the top: one `### <question>` sub-heading\n each, followed by a checkbox list of candidate answers — aim for TWO\n concrete options plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself — the human ticks exactly one per question. The\n last `- [ ] _your answer_` line is the human's own-answer slot; write it\n with the literal `_your answer_` placeholder and leave it for them.\n\n On a RETURN lap the human has ticked one option per question (or replaced\n `_your answer_` with their own text). For each answered question: fold the\n chosen answer into the plan prose, then MOVE the question down into a\n `## Answered Questions` section as `### <question>` followed by the\n resolved answer in plain PROSE (drop the checkboxes). Never re-raise a\n question the human deleted; if the human deleted the whole\n `## Open Questions` section, treat the plan as accepted and surface no new\n questions unless something is genuinely still blocking. Omit\n `## Open Questions` entirely once there are none left.\n\n Leave `<%= it.vars.requirementsFile %>` uncommitted and finish your turn.\n answerMessage: |\n `<%= it.vars.requirementsFile %>` holds the product plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n advanceDescribe: >-\n With no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to accept the\n product plan and move on to technical architecting (**architecting**).\n editDescribe: >-\n Tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**adv-grilling**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n authorLabel: Refining the product plan\n answerLabel: Awaiting your answers\n\n # Advanced flow: technical Q&A loop (architecting/architecting-answer -> decompose).\n - submachine: qaLoop\n name: arch\n as: { author: architecting, answer: architecting-answer }\n with:\n authorFile: <%= it.vars.architectureFile %>\n onAdvance: decompose\n authorPrompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.architectureFile %>` and `<%= it.vars.requirementsFile %>`,\n which this prompt tells you to write and delete respectively.\n\n Read `<%= it.vars.requirementsFile %>` (the converged product plan) and\n develop `<%= it.vars.architectureFile %>` from it in this one turn:\n file/module structure, data models, library/tech-stack choices,\n error-handling strategy — the *how*, building on the *what* already\n settled. Do not re-open product-level decisions; treat them as settled.\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading exactly as the grilling phase does: one\n `### <question>` each, followed by a checkbox list — TWO concrete options\n plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself. On a RETURN lap the human has ticked one option\n per question (or replaced `_your answer_` with their own text): fold each\n chosen answer into the plan prose, then MOVE the question into a\n `## Answered Questions` section as `### <question>` + the resolved answer\n in plain PROSE (drop the checkboxes). Never re-raise a deleted question;\n treat a deleted `## Open Questions` section as acceptance. Omit\n `## Open Questions` once there are none left.\n\n Once `<%= it.vars.architectureFile %>` is written, delete\n `<%= it.vars.requirementsFile %>` — its content has been folded in and it\n must not linger. Leave everything uncommitted and finish your turn.\n answerMessage: |\n `<%= it.vars.architectureFile %>` holds the technical plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n advanceDescribe: >-\n With no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to accept the\n technical plan and move on to decomposing the work into packages\n (**decompose**).\n editDescribe: >-\n Tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**architecting**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n authorLabel: Refining the technical plan\n answerLabel: Awaiting your answers\n\n # Simple flow plan-iteration loop (planning/plan-review -> building).\n - submachine: planLoop\n with:\n onAccept: building\n\n # Shared review tail (reviewing -> await-review -> ... -> squashing).\n - submachine: humanReview\n with:\n onSignoff: squashing\n\n # Advanced per-package agentic review (spec-review/spec-fix -> closing).\n - submachine: specGate\n with:\n onApproved: closing\n\n # Advanced per-package build queue (picking/adv-building/closing -> reviewing).\n - submachine: packageLoop\n with:\n onDrained: reviewing\n\nstates:\n # ── Shared entry ────────────────────────────────────────────────────────\n idle:\n actor: human\n initial: true\n label: Idle\n # Every human-gate `message:` below closes with the same rendered \"What\n # each change does next\" list, built from `it.edges` (the resting state's\n # own `on` edges — each edge's `describe:` is the single source of truth).\n message: |\n No active gtd cycle.\n\n To start one, create a steering file describing what you want built:\n\n - `.gtd/TODO.md` — a short sketch — starts the SIMPLE flow (plan, build,\n review).\n - `.gtd/REQUIREMENTS.md` — the product requirements — starts the ADVANCED\n flow (product + technical Q&A, package decomposition, parallel build,\n per-package agentic review).\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* <%= it.vars.requirementsFile %>\":\n to: adv-start-check\n describe: >-\n Save the product requirements to `.gtd/REQUIREMENTS.md` to start the\n ADVANCED flow — gtd first checks the test baseline is green, then the\n agent develops them through product then technical Q&A, decomposes the\n work into packages, and builds each in parallel (**adv-start-check**).\n \"* **\":\n to: start-check\n describe: >-\n Save a short sketch to `.gtd/TODO.md` (or any other change) to start\n the SIMPLE flow — gtd first checks the test baseline is green, then the\n agent reads it, explores the codebase, and develops it into a concrete\n implementation plan (**start-check**).\n\n # ── Simple flow: monolithic build (plan loop is the `planLoop` sub-machine) ─\n building:\n actor: agent\n label: Building\n file: <%= it.vars.todoFile %>\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create or edit anything under `.gtd/` except deleting\n `<%= it.vars.todoFile %>`, which this prompt tells you to delete once its\n work is complete.\n\n The plan to implement is: <%~ it.read(it.vars.todoFile) %>\n\n Read that plan and implement it in full. Use TDD discipline: one test,\n then the implementation that passes it, then the next — never write all\n the tests first.\n\n Delete `<%= it.vars.todoFile %>` once the work it describes is complete\n and verified. Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": checking\n\n # ── Advanced flow: decomposition (Q&A loops are the `qaLoop` sub-machine) ───\n decompose:\n actor: agent\n label: Decomposing into packages\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except the\n package files this prompt tells you to write (under\n `<%= it.vars.packagesDir %>/`), and `<%= it.vars.architectureFile %>`,\n which it tells you to delete.\n\n Read `<%= it.vars.architectureFile %>` (the converged technical plan) and\n decompose it into an ordered set of WORK PACKAGES, one file each under\n `<%= it.vars.packagesDir %>/` (e.g. `<%= it.vars.packagesDir %>/01-name.md`,\n `<%= it.vars.packagesDir %>/02-name.md`, ...). Order the packages so the\n test suite can pass again after each one lands.\n\n Each package file describes a set of INDEPENDENT tasks that can be\n implemented concurrently without ordering constraints between them — a\n single build turn will fan them out to parallel subagents. Give each task\n a clear description, acceptance criteria as `- [ ]` checkboxes, and the\n relevant paths. Also state the package's overall requirement so it can be\n reviewed against its spec. Do NOT put ordering-dependent tasks in the same\n package — split them across ordered packages instead.\n\n No package file may reference any other `.gtd/` file. Once the package\n files are written, delete `<%= it.vars.architectureFile %>`. Leave\n everything uncommitted and finish your turn.\n on:\n \"* <%= it.vars.packagesDir %>/**\": picking\n\n # ── Green-baseline gate: `gtd fix` entry ──────────────────────────────────\n # `gtd fix` enters HERE (fixEntry): run the suite, then a red run drops\n # straight into the shared `fixing` loop (-> checking -> review + squash\n # tail); a green run is a no-op back to `idle` (nothing to fix).\n fix-check:\n actor: check\n fixEntry: true\n label: Checking the baseline\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite to find what needs fixing; a red run\n # records .gtd/FEEDBACK.md (-> fixing), a green run cleans it up (-> idle).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A <%= it.vars.feedbackFile %>\": fixing\n \"M <%= it.vars.feedbackFile %>\": fixing\n \"D <%= it.vars.feedbackFile %>\": idle\n \"C\": idle\n\n # ── Shared tail: squash finale (review loop is the `humanReview` sub-machine) ─\n squashing:\n actor: agent\n label: Squashing\n file: <%= it.vars.commitMsgFile %>\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. The cycle is approved and done.\n `.gtd/` holds this workflow's own state — never create, edit, or delete\n anything under `.gtd/` except `<%= it.vars.commitMsgFile %>`, which this\n prompt tells you to write.\n\n Leave `<%= it.vars.commitMsgFile %>` uncommitted and finish your turn —\n entering this file squashes everything this process retains into one\n commit using its content as the message. The diff below is exactly what\n that commit will contain: `it.retainedDiff`, the changes THIS process\n made (its trace/retry boundary onward). For a normal build cycle that is\n the whole feature; for a `gtd review` process it is only the fixes made\n during the review — NOT the changeset that was under review.\n <% if (it.retainedDiff.trim()) { %>\n\n Write `<%= it.vars.commitMsgFile %>` with ONE conventional-commits message\n for the changes below (subject line `type(scope): subject`, imperative\n mood, <= 72 characters; a body explaining the why, trade-offs, and key\n decisions). Plain text, no markdown wrapper.\n\n ## Retained diff\n\n ```diff\n <%~ it.retainedDiff %>\n ```\n <% } else { %>\n\n This process retained NO changes (a clean review sign-off with no fixes).\n Write `<%= it.vars.commitMsgFile %>` with exactly this one line and\n nothing else:\n\n chore: human review\n <% } %>\n on:\n \"A <%= it.vars.commitMsgFile %>\": done\n \"M <%= it.vars.commitMsgFile %>\": done\n\n done:\n commit: |\n <%~ it.read(it.vars.commitMsgFile) %>\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n";
190594
190598
  //#endregion
190595
190599
  //#region src/workflows/templates.ts
190596
190600
  /**
@@ -192612,6 +192616,33 @@ var Eta = class extends Eta$1 {
192612
192616
  };
192613
192617
  //#endregion
192614
192618
  //#region src/PatternTemplates.ts
192619
+ /**
192620
+ * The stub `TemplateContext` an `on` pattern key renders against at the edge
192621
+ * (see `Edge.ts`'s `renderOnEdges`): only `vars` (and, optionally, `state`)
192622
+ * are real — every commit-ish/diff field is empty, `processCost` is `0`,
192623
+ * `edges` is empty, and `read` throws, since a pattern names a path and never
192624
+ * legitimately needs a working tree or git history to resolve. This is also
192625
+ * `Lsp.ts`'s `buildFileModeMap` map-building context, factored out here to
192626
+ * avoid building the same stub in two places.
192627
+ */
192628
+ const varsOnlyContext = (vars, state = "") => ({
192629
+ startCommit: "",
192630
+ currentCommit: "",
192631
+ previousCommit: "",
192632
+ state,
192633
+ actor: "",
192634
+ processDiff: "",
192635
+ reviewDiff: "",
192636
+ retainedDiff: "",
192637
+ lastDiff: "",
192638
+ processCost: 0,
192639
+ processCostByModel: [],
192640
+ read: (path) => {
192641
+ throw new Error(`no working tree to read from while rendering against a vars-only context (path: ${path})`);
192642
+ },
192643
+ vars,
192644
+ edges: []
192645
+ });
192615
192646
  const eta = new Eta();
192616
192647
  eta.readFile = null;
192617
192648
  eta.resolvePath = null;
@@ -192869,7 +192900,7 @@ const resolveVars = (workflowVars, rcVars, env) => ({
192869
192900
  ...rcVars,
192870
192901
  ...envVarsFrom(env)
192871
192902
  });
192872
- /** Map a state's raw `on` edges to the `{ pattern, target, describe? }` shape templates see as `it.edges`. `undefined` (a commit state, no `on`) yields an empty list. */
192903
+ /** Map a state's raw `on` edges to the `{ pattern, target, describe? }` shape templates see as `it.edges`. `undefined` (a commit state, no `on`) yields an empty list. Callers pass already-rendered edges (see `renderOnEdges`) — this never renders anything itself. */
192873
192904
  const toTemplateEdges = (edges) => (edges ?? []).map(([pattern, target, describe]) => describe !== void 0 ? {
192874
192905
  pattern,
192875
192906
  target,
@@ -192879,10 +192910,48 @@ const toTemplateEdges = (edges) => (edges ?? []).map(([pattern, target, describe
192879
192910
  target
192880
192911
  });
192881
192912
  /**
192913
+ * Render every `on` edge's pattern key as an Eta template over `vars` ONLY
192914
+ * (`PatternTemplates.varsOnlyContext`) — a pattern names a path; it never
192915
+ * needs diffs/commit hashes, and restricting to `vars` avoids an ordering
192916
+ * circularity (the full `TemplateContext`'s `it.edges` is itself derived from
192917
+ * these same `on` edges). `target`/`describe` pass through verbatim —
192918
+ * `describe` is inert to the engine and is never rendered, unlike the pattern
192919
+ * key. Throws whatever Eta throws on a malformed pattern template; the caller
192920
+ * turns that into a step refusal / command error, exactly like a content
192921
+ * render failure.
192922
+ */
192923
+ const renderOnEdges = (edges, vars) => {
192924
+ const ctx = varsOnlyContext(vars);
192925
+ return (edges ?? []).map(([pattern, target, describe]) => describe !== void 0 ? [
192926
+ renderStateTemplate(pattern, ctx),
192927
+ target,
192928
+ describe
192929
+ ] : [renderStateTemplate(pattern, ctx), target]);
192930
+ };
192931
+ /**
192932
+ * A shallow clone of `def` whose `state`'s `on` is replaced by
192933
+ * `renderedOnEdges` — used to feed `PatternMachine.step`, which matches only
192934
+ * `def.states[state].on` for the state it's invoked at. Only the RESTING
192935
+ * state needs patching: `step`'s retry/target logic keys on state NAMES, not
192936
+ * on any other state's `on`, so every other state is left as-is.
192937
+ */
192938
+ const withRenderedOn = (def, state, renderedOnEdges) => ({
192939
+ ...def,
192940
+ states: {
192941
+ ...def.states,
192942
+ [state]: {
192943
+ ...def.states[state],
192944
+ on: renderedOnEdges
192945
+ }
192946
+ }
192947
+ });
192948
+ /**
192882
192949
  * Build the `PatternTemplates.TemplateContext` for rendering `state`'s content
192883
192950
  * at the resolved rest. `vars` is the already-merged three-layer map (see
192884
- * `resolveVars`); `edges` is the resting state's own `on` edges (see
192885
- * `toTemplateEdges`). `currentCost`/`currentModel` are the in-flight step's own
192951
+ * `resolveVars`); `edges` is the resting state's own `on` edges, ALREADY
192952
+ * RENDERED by the caller (`renderOnEdges`) — this function never renders a
192953
+ * pattern itself, it only maps the given edges into `it.edges`
192954
+ * (`toTemplateEdges`). `currentCost`/`currentModel` are the in-flight step's own
192886
192955
  * `--cost`/`--model` (folded into the process's committed cost entries so a
192887
192956
  * `commit:` squash template sees the whole-process total AND per-model
192888
192957
  * breakdown including the squashing step) — `0`/absent for the pure emitters
@@ -193002,7 +193071,7 @@ const renderRest = (rest, context) => gen(function* () {
193002
193071
  kind,
193003
193072
  content,
193004
193073
  ...omitUndefined(hints),
193005
- edges: toTemplateEdges(rest.stateDef.on)
193074
+ edges: context.edges
193006
193075
  };
193007
193076
  });
193008
193077
  /**
@@ -204837,24 +204906,7 @@ const buildFileModeMap = (def, vars, root) => {
204837
204906
  if (stateDef.file === void 0 || stateDef.mode === void 0) continue;
204838
204907
  let rendered;
204839
204908
  try {
204840
- rendered = renderStateTemplate(stateDef.file, {
204841
- startCommit: "",
204842
- currentCommit: "",
204843
- previousCommit: "",
204844
- state: name,
204845
- actor: "",
204846
- processDiff: "",
204847
- reviewDiff: "",
204848
- retainedDiff: "",
204849
- lastDiff: "",
204850
- processCost: 0,
204851
- processCostByModel: [],
204852
- read: (path) => {
204853
- throw new Error(`"file:" is not readable while building the path→mode map (path: ${path})`);
204854
- },
204855
- vars,
204856
- edges: []
204857
- });
204909
+ rendered = renderStateTemplate(stateDef.file, varsOnlyContext(vars, name));
204858
204910
  } catch (e) {
204859
204911
  warnings.push(`state "${name}": "file:" failed to render, skipped — ${e instanceof Error ? e.message : String(e)}`);
204860
204912
  continue;
@@ -205076,8 +205128,8 @@ const stripUndefined = (o) => {
205076
205128
  for (const key of Object.keys(o)) if (o[key] === void 0) delete o[key];
205077
205129
  return o;
205078
205130
  };
205079
- /** Describe one compiled state for the viewer (optional fields omitted when unset). */
205080
- const toVizState = (name, def, group, incoming) => stripUndefined({
205131
+ /** Describe one compiled state for the viewer (optional fields omitted when unset). `onEdges` is that state's `on`, ALREADY RENDERED against `it.vars` (see `buildVizModel`) — never `def.on` directly, which would show the unrendered literal. */
205132
+ const toVizState = (name, def, onEdges, group, incoming) => stripUndefined({
205081
205133
  name,
205082
205134
  actor: def.actor,
205083
205135
  kind: contentKindOf(def) ?? "unknown",
@@ -205089,15 +205141,31 @@ const toVizState = (name, def, group, incoming) => stripUndefined({
205089
205141
  mode: def.mode,
205090
205142
  retry: def.retry,
205091
205143
  flags: flagsOf(def),
205092
- on: (def.on ?? []).map(edgeToViz),
205144
+ on: onEdges.map(edgeToViz),
205093
205145
  incoming,
205094
205146
  group
205095
205147
  });
205148
+ /** Render one `on` pattern key against `vars` (see `Edge.ts`'s `renderOnEdges`), falling back to the raw pattern string on a render failure — the viewer is best-effort, unlike a real step (which refuses). */
205149
+ const renderPatternOrRaw = (pattern, vars) => {
205150
+ try {
205151
+ return renderStateTemplate(pattern, varsOnlyContext(vars));
205152
+ } catch {
205153
+ return pattern;
205154
+ }
205155
+ };
205156
+ /** Render every `on` edge of every state against `vars` (pattern key only — `target`/`describe` pass through verbatim), keyed by state name. */
205157
+ const renderedOnByState = (workflow, vars) => new Map(Object.entries(workflow.states).map(([name, def]) => [name, (def.on ?? []).map(([pattern, target, describe]) => describe !== void 0 ? [
205158
+ renderPatternOrRaw(pattern, vars),
205159
+ target,
205160
+ describe
205161
+ ] : [renderPatternOrRaw(pattern, vars), target])]));
205096
205162
  /**
205097
205163
  * Build the viewer's JSON description from the active COMPILED workflow plus its
205098
205164
  * RAW value (the pre-expansion `submachines:`/`use:` form — `rawWorkflow` from
205099
205165
  * `ConfigService`), which is the only place the sub-machine grouping survives
205100
- * (`compileWorkflowConfig` flattens it away). `vars` is shown for reference.
205166
+ * (`compileWorkflowConfig` flattens it away). `vars` is shown for reference, AND
205167
+ * used to render every state's `on` pattern (see `renderedOnByState`) so the
205168
+ * diagram shows real paths rather than a repointed var's stale literal.
205101
205169
  */
205102
205170
  const buildVizModel = (workflow, rawWorkflow, vars) => {
205103
205171
  const groups = collectGroups(rawWorkflow);
@@ -205107,6 +205175,7 @@ const buildVizModel = (workflow, rawWorkflow, vars) => {
205107
205175
  const cur = groupOf.get(s);
205108
205176
  if (cur === void 0 || sizeOf.get(g.name) < sizeOf.get(cur)) groupOf.set(s, g.name);
205109
205177
  }
205178
+ const renderedOn = renderedOnByState(workflow, vars);
205110
205179
  const incoming = /* @__PURE__ */ new Map();
205111
205180
  const addIncoming = (target, from, pattern) => {
205112
205181
  const list = incoming.get(target) ?? [];
@@ -205117,11 +205186,11 @@ const buildVizModel = (workflow, rawWorkflow, vars) => {
205117
205186
  incoming.set(target, list);
205118
205187
  };
205119
205188
  for (const [name, def] of Object.entries(workflow.states)) {
205120
- for (const [pattern, to] of def.on ?? []) addIncoming(to, name, pattern);
205189
+ for (const [pattern, to] of renderedOn.get(name) ?? []) addIncoming(to, name, pattern);
205121
205190
  if (def.retry) addIncoming(def.retry.otherwise, name, `retry ×${def.retry.max}`);
205122
205191
  }
205123
205192
  return {
205124
- states: Object.entries(workflow.states).map(([name, def]) => toVizState(name, def, groupOf.get(name), incoming.get(name) ?? [])),
205193
+ states: Object.entries(workflow.states).map(([name, def]) => toVizState(name, def, renderedOn.get(name) ?? [], groupOf.get(name), incoming.get(name) ?? [])),
205125
205194
  initial: initialStateOf(workflow),
205126
205195
  groups: groups.map((g) => ({
205127
205196
  name: g.name,
@@ -205135,12 +205204,15 @@ const buildVizModel = (workflow, rawWorkflow, vars) => {
205135
205204
  * Describe the currently-rested state for the viewer: its `on` edges each
205136
205205
  * flagged with whether it's the one that would fire on the CURRENT pending
205137
205206
  * changes (same first-match semantics `PatternMachine.step` decides a real
205138
- * step with), plus the retry redirect and pending changes verbatim. `group`
205139
- * is threaded in by the caller (already computed once by `buildVizModel`
205140
- * from the same workflow) rather than recomputed here.
205141
- */
205142
- const buildCurrentStateModel = (rest, changes, group) => {
205143
- const onEdges = rest.stateDef.on ?? [];
205207
+ * step with), plus the retry redirect and pending changes verbatim. `onEdges`
205208
+ * is the resting state's `on` edges ALREADY RENDERED against `it.vars` (see
205209
+ * `Edge.ts`'s `renderOnEdges`) — this function never renders anything itself,
205210
+ * it only matches/emits from what it's given (never `rest.stateDef.on`, which
205211
+ * would be the unrendered literal). `group` is threaded in by the caller
205212
+ * (already computed once by `buildVizModel` from the same workflow) rather
205213
+ * than recomputed here.
205214
+ */
205215
+ const buildCurrentStateModel = (rest, changes, onEdges, group) => {
205144
205216
  const matchedIndex = onEdges.findIndex(([patternStr]) => {
205145
205217
  const parsed = parsePattern(patternStr);
205146
205218
  return parsed !== void 0 && matchesPattern(parsed, changes);
@@ -205356,6 +205428,10 @@ Commands:
205356
205428
  an agent through gtd next/gtd step calls until the
205357
205429
  workflow returns to its initial state again. A bare gtd
205358
205430
  invocation and gtd loop both launch it identically
205431
+ edit [path] Open \${VISUAL:-$EDITOR} on <path>, or — with no path — on
205432
+ the resolved rest's declared file (or the repo directory
205433
+ if it declares none). Handled entirely by bin/gtd, not
205434
+ this bundle
205359
205435
  init Scaffold a minimal .gtdrc.json for this repo, seeding the
205360
205436
  default variables you are most likely to change (the test
205361
205437
  command) and a Prettier formatting suggestion. gtd runs its
@@ -205538,13 +205614,20 @@ const runInitCommand = (argv, json, write) => gen(function* () {
205538
205614
  }) + "\n");
205539
205615
  else write("Wrote .gtdrc.json seeding the default variables (the test command) and a\nPrettier formatting suggestion. gtd runs its built-in workflow by default — add\na workflow: key only if you want to customize the machine itself.\n\n" + (inRepo ? "Review and commit it before starting: an uncommitted .gtdrc.json counts as a\npending change, so the initial state would capture it on the first step. Once\ncommitted, run `gtd step human` to begin.\n" : "This directory is not a git repository, so there is nothing to commit here. The\nconfig applies to any gtd repository nested below it — gtd discovers it by\nwalking up from the repository root. Run `gtd step human` from such a repo to\nbegin.\n"));
205540
205616
  });
205617
+ /** Render a state's `on` edges against `vars` (see `renderOnEdges`), surfacing a malformed pattern template as a plain command error, exactly like a content render failure. */
205618
+ const renderOnEdgesOrFail = (onEdges, vars) => try_({
205619
+ try: () => renderOnEdges(onEdges, vars),
205620
+ catch: (e) => e instanceof Error ? e : new Error(String(e))
205621
+ });
205541
205622
  /**
205542
205623
  * Resolve HEAD's rest, the current process run, and the template context for
205543
205624
  * rendering that rest's OWN state/actor — the common prefix shared by `gtd
205544
205625
  * next` and `gtd status` (each fetches `git` itself first, since
205545
205626
  * `gtd status` also needs it for `pendingChanges`; `stepAsActor`'s squash
205546
205627
  * path renders a DIFFERENT state, so it builds its own context inline
205547
- * instead of sharing this helper).
205628
+ * instead of sharing this helper). `renderedOn` is the resting state's `on`
205629
+ * edges already rendered against `it.vars` (`renderOnEdges`) — `next`/`status`
205630
+ * reuse it instead of re-rendering the same patterns themselves.
205548
205631
  */
205549
205632
  const resolveRestContext = (git) => gen(function* () {
205550
205633
  const config = yield* (yield* ConfigService).load;
@@ -205553,11 +205636,13 @@ const resolveRestContext = (git) => gen(function* () {
205553
205636
  const rest = yield* resolveRest();
205554
205637
  const run = yield* computeProcessRun(git, rest.def);
205555
205638
  const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
205639
+ const renderedOn = yield* renderOnEdgesOrFail(rest.stateDef.on, vars);
205556
205640
  const reviewBase = yield* reviewBaseHash(git, rest.def, run);
205557
205641
  return {
205558
205642
  rest,
205559
205643
  run,
205560
- context: yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars, rest.stateDef.on, 0, void 0, reviewBase)
205644
+ context: yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars, renderedOn, 0, void 0, reviewBase),
205645
+ renderedOn
205561
205646
  };
205562
205647
  });
205563
205648
  /** The user-facing message for a `step` refusal — out-of-turn names the awaited actor, no-match names every declared pattern. */
@@ -205577,7 +205662,9 @@ const stepAsActor = (invoker, cost, model) => gen(function* () {
205577
205662
  const rest = yield* resolveRest();
205578
205663
  const run = yield* computeProcessRun(git, rest.def);
205579
205664
  const changes = yield* pendingChanges(git);
205580
- const decision = step(rest.def, rest.state, invoker, {
205665
+ const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
205666
+ const renderedOn = yield* renderOnEdgesOrFail(rest.stateDef.on, vars);
205667
+ const decision = step(withRenderedOn(rest.def, rest.state, renderedOn), rest.state, invoker, {
205581
205668
  changes,
205582
205669
  processTrace: run.trace
205583
205670
  });
@@ -205589,9 +205676,9 @@ const stepAsActor = (invoker, cost, model) => gen(function* () {
205589
205676
  model: null
205590
205677
  };
205591
205678
  const executable = decision;
205592
- const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
205593
205679
  const renderedState = decision.kind === "squash" ? decision.state : rest.state;
205594
- const context = yield* buildTemplateContext(git, worktree.read, renderedState, invoker, run, vars, rest.def.states[renderedState]?.on, cost ?? 0, model);
205680
+ const renderedStateOn = renderedState === rest.state ? renderedOn : yield* renderOnEdgesOrFail(rest.def.states[renderedState]?.on, vars);
205681
+ const context = yield* buildTemplateContext(git, worktree.read, renderedState, invoker, run, vars, renderedStateOn, cost ?? 0, model);
205595
205682
  yield* enforceSteeringGate(worktree.read, invoker, rest, context, decision.kind);
205596
205683
  yield* enforceReviewSignoffGate(worktree.read, invoker, rest, context, changes, decision.kind);
205597
205684
  yield* enforceFeedbackProgressGate(invoker, rest, context, changes, decision.kind);
@@ -206064,7 +206151,7 @@ const runValidateCommand = (argv, json, write) => gen(function* () {
206064
206151
  }
206065
206152
  return yield* fail$2(/* @__PURE__ */ new Error(`${file} is not valid:\n${errors.map((e) => ` - ${e}`).join("\n")}`));
206066
206153
  });
206067
- /** Which declared `on` pattern (if any) each pending change matches — the pure computation `gtd status` reports (both plain and `--json`). */
206154
+ /** Which declared `on` pattern (if any) each pending change matches — the pure computation `gtd status` reports (both plain and `--json`). `onEdges` is ALREADY RENDERED against `it.vars` (`renderOnEdges`) — the reported pattern is the one a real `gtd step` would match against. */
206068
206155
  const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
206069
206156
  const matchedRow = onEdges.find(([patternStr]) => {
206070
206157
  const parsed = parsePattern(patternStr);
@@ -206089,9 +206176,9 @@ const costStatusLines = (cost, byModel) => {
206089
206176
  if (breakdownIsInformative(byModel)) for (const m of byModel) lines.push(` ${m.model}: ${m.cost}`);
206090
206177
  return lines;
206091
206178
  };
206092
- /** `gtd status --json`'s emission — `{state, actor, changes, model?, memory?, label?, file?, mode?, cost?, costByModel?, edges?}`. */
206093
- const writeStatusJson = (write, rest, statusChanges, model, memory, label, file, cost, costByModel) => {
206094
- const edges = toTemplateEdges(rest.stateDef.on);
206179
+ /** `gtd status --json`'s emission — `{state, actor, changes, model?, memory?, label?, file?, mode?, cost?, costByModel?, edges?}`. `renderedOn` is the resting state's `on` edges already rendered against `it.vars` (`renderOnEdges`), so the emitted `edges[].pattern` carries the same rendered path as `changes[].pattern`. */
206180
+ const writeStatusJson = (write, rest, statusChanges, renderedOn, model, memory, label, file, cost, costByModel) => {
206181
+ const edges = toTemplateEdges(renderedOn);
206095
206182
  write(JSON.stringify({
206096
206183
  state: rest.state,
206097
206184
  actor: rest.actor,
@@ -206126,14 +206213,14 @@ const writeStatusPlain = (write, rest, statusChanges, model, memory, label, file
206126
206213
  const runStatusCommand = (argv, json, write) => gen(function* () {
206127
206214
  yield* rejectExtraArgs("status", argv);
206128
206215
  const git = yield* GitService;
206129
- const { rest, context } = yield* resolveRestContext(git);
206216
+ const { rest, context, renderedOn } = yield* resolveRestContext(git);
206130
206217
  const changes = yield* pendingChanges(git);
206131
206218
  const model = yield* renderModel(rest.stateDef, context);
206132
206219
  const memory = yield* renderMemory(rest.stateDef, context);
206133
206220
  const label = yield* renderLabel(rest.stateDef, context);
206134
206221
  const file = yield* renderFile(rest.stateDef, context);
206135
- const statusChanges = computeStatusChanges(rest.stateDef.on ?? [], changes);
206136
- if (json) writeStatusJson(write, rest, statusChanges, model, memory, label, file, context.processCost, context.processCostByModel);
206222
+ const statusChanges = computeStatusChanges(renderedOn, changes);
206223
+ if (json) writeStatusJson(write, rest, statusChanges, renderedOn, model, memory, label, file, context.processCost, context.processCostByModel);
206137
206224
  else writeStatusPlain(write, rest, statusChanges, model, memory, label, file, context.processCost, context.processCostByModel);
206138
206225
  });
206139
206226
  /** Parse a `--port` value: an integer 0–65535, or `undefined` if invalid/absent. */
@@ -206194,11 +206281,15 @@ const parseVisualizeOptions = (rest) => {
206194
206281
  */
206195
206282
  const computeCurrentState = (model) => gen(function* () {
206196
206283
  const git = yield* GitService;
206284
+ const config = yield* (yield* ConfigService).load;
206285
+ const envVars = yield* EnvVars;
206197
206286
  const reviewHead = yield* git.readRefOption(REVIEW_HEAD_REF);
206198
206287
  const currentRest = yield* resolveRest(getOrUndefined(reviewHead));
206199
206288
  const changes = yield* pendingChanges(git);
206289
+ const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
206290
+ const renderedOn = yield* renderOnEdgesOrFail(currentRest.stateDef.on, vars);
206200
206291
  const group = model.states.find((s) => s.name === currentRest.state)?.group;
206201
- return buildCurrentStateModel(currentRest, changes, group);
206292
+ return buildCurrentStateModel(currentRest, changes, renderedOn, group);
206202
206293
  });
206203
206294
  /**
206204
206295
  * `gtd visualize`: serve an interactive diagram of the ACTIVE workflow on a
package/docs/cli.md CHANGED
@@ -45,6 +45,10 @@ Commands:
45
45
  commits that would be lost
46
46
  next Print the resolved rest's rendered script/prompt/message
47
47
  (no mutation)
48
+ edit [path] Open <path> (repo-relative) in $VISUAL/$EDITOR, blocking
49
+ until the editor exits. With no argument, opens the
50
+ resolved rest's declared file (or the repo root if it
51
+ declares none). Never reads --no-edit/GTD_NO_EDIT
48
52
  status Print the resolved rest's state/actor and which declared
49
53
  pattern (if any) each pending change matches (no mutation)
50
54
  validate Format and validate the steering file the resolved rest
@@ -63,6 +67,8 @@ Options:
63
67
  --no-open (gtd visualize only) do not open the browser
64
68
  --cost=<n> (gtd step only) record the invocation's token cost
65
69
  --model=<name> (gtd step only, with --cost) tag that cost's model
70
+ --no-edit (bare gtd or gtd loop only) disable the loop's automatic
71
+ editor launching at human gates
66
72
  --version, -v Print version and exit
67
73
  --help, -h Print this help and exit
68
74
  ```
@@ -79,12 +85,32 @@ history relative to cwd, so it refuses with a clear error if invoked from a
79
85
  subdirectory.
80
86
 
81
87
  `--json`, `--cost=<n>`, and `--model=<name>` (the latter two only for
82
- `gtd step`) are the only long options. Any other `--` option (including a typo
83
- like `--jsn`) is rejected with a usage error rather than silently ignored, so a
84
- mistyped flag can never degrade a JSON caller to plain-text mode. A bare
85
- `--cost`/`--model` with no value, a non-numeric or negative `--cost`, an empty
86
- `--model`, `--model` without `--cost`, or either flag on any command other than
87
- `gtd step` are all usage errors.
88
+ `gtd step`) are the only long options the compiled bundle recognizes. Any other
89
+ `--` option (including a typo like `--jsn`) is rejected with a usage error
90
+ rather than silently ignored, so a mistyped flag can never degrade a JSON caller
91
+ to plain-text mode. A bare `--cost`/`--model` with no value, a non-numeric or
92
+ negative `--cost`, an empty `--model`, `--model` without `--cost`, or either
93
+ flag on any command other than `gtd step` are all usage errors.
94
+
95
+ `--no-edit` is a separate, bash-level flag handled entirely by `bin/gtd` itself,
96
+ stripped before anything reaches the bundle — see [`--no-edit`](#--no-edit)
97
+ below.
98
+
99
+ ## `--no-edit`
100
+
101
+ A flag on the loop driver only — bare `gtd --no-edit` or `gtd loop --no-edit`
102
+ (the two are equivalent, since bare `gtd` and `gtd loop` are themselves
103
+ equivalent). Recognized in exactly those two positions; any other
104
+ placement/combination (e.g. `gtd step --no-edit`, or combined with any other
105
+ argument) is a usage error. The `GTD_NO_EDIT` environment variable (any
106
+ non-empty value) does the same thing.
107
+
108
+ Disables the loop's default behavior of launching an editor at human gates,
109
+ restoring the previous halt-and-print-and-exit behavior with no editor involved.
110
+ It governs only the loop's own automatic launching — `gtd edit` invoked directly
111
+ never reads it (see [`gtd edit`](#gtd-edit-path) above). See
112
+ [Driving the loop](loop.md) and `skills/loop/SKILL.md` for the full gate-flow
113
+ description.
88
114
 
89
115
  ## `gtd init`
90
116
 
@@ -408,6 +434,40 @@ which are JSON-only. `--json` emits
408
434
  when the state has no `on` (a commit state); a per-edge `describe` is likewise
409
435
  omitted when that edge declares none.
410
436
 
437
+ ## `gtd edit [path]`
438
+
439
+ Handled entirely in `bin/gtd`'s own bash — unlike every other subcommand, it is
440
+ **never forwarded** to the compiled bundle (`dist/gtd.bundle.mjs`).
441
+
442
+ With `<path>` given (repo-relative), opens it in `${VISUAL:-$EDITOR}` (git's own
443
+ precedence — `$VISUAL` first, then `$EDITOR`; no fallback to `vi`), blocking in
444
+ the foreground until the editor exits. It creates nothing if `<path>` doesn't
445
+ exist — the path is handed straight to the editor command verbatim. The editor's
446
+ own exit code is ignored entirely; success is judged from tree state afterward,
447
+ not from this command.
448
+
449
+ With no argument, it peeks with `gtd next --json` to find the resolved rest's
450
+ declared `.file`, and opens that (repo-relative), or the repo directory (`.`)
451
+ when the resolved state declares no `file:`. If `gtd next --json` itself fails
452
+ (e.g. not in a repo), it prints an error and exits non-zero without attempting
453
+ to open an editor:
454
+
455
+ ```
456
+ gtd edit: could not determine the next step:
457
+ <gtd next --json's own error output>
458
+ ```
459
+
460
+ `gtd edit` **never** reads `--no-edit`/`GTD_NO_EDIT` — those two govern only the
461
+ loop's own automatic editor launching at human gates (see
462
+ [Driving the loop](loop.md) and `skills/loop/SKILL.md`), not this command.
463
+ Invoked directly, `gtd edit` always launches, unconditionally.
464
+
465
+ If neither `$VISUAL` nor `$EDITOR` is set, no editor is launched:
466
+
467
+ ```
468
+ gtd: no editor configured — set $EDITOR (or $VISUAL)
469
+ ```
470
+
411
471
  ## Running `script` rests (no `gtd` subcommand)
412
472
 
413
473
  There is no `gtd` subcommand that executes a workflow script — gtd never runs a
@@ -135,12 +135,12 @@ An `on` row's value is normally the target state name. It may instead be an
135
135
  object `{ to: <target>, describe: <sentence> }`, attaching a human-readable
136
136
  `describe` — a plain sentence explaining what making that kind of change does
137
137
  next. `describe` is **inert to the engine**: it plays no part in matching, is
138
- never Eta-rendered (exactly like the pattern key itself — see the "Known
139
- limitation" note below), and never affects a decision. It exists only to be
140
- surfaced. A state's own edges are handed to its content template as `it.edges`
141
- (an array of `{ pattern, target, describe? }`, in declaration order), so a human
142
- gate's `message:` can render a "what each change does next" list from the same
143
- routing the engine uses — one source of truth:
138
+ never Eta-rendered — UNLIKE the pattern key itself, which IS an Eta template
139
+ (see below) — and never affects a decision. It exists only to be surfaced. A
140
+ state's own edges are handed to its content template as `it.edges` (an array of
141
+ `{ pattern, target, describe? }`, `pattern` already rendered, in declaration
142
+ order), so a human gate's `message:` can render a "what each change does next"
143
+ list from the same routing the engine uses — one source of truth:
144
144
 
145
145
  ```yaml
146
146
  workflow:
@@ -174,6 +174,14 @@ skipped by the `if (e.describe)` guard. `gtd next --json` and
174
174
  no `on` (a commit state), and omit a per-edge `describe` when that edge declares
175
175
  none.
176
176
 
177
+ **The pattern key (everything before the value) is an Eta template, rendered
178
+ against `it.vars` alone** — the same vars-only slice `on` templates could ever
179
+ need, since a pattern only ever names a path, never a diff or commit hash. A
180
+ literal path (`"A .gtd/FEEDBACK.md": fix`) still works unchanged; naming a var
181
+ instead (`"A <%= it.vars.feedbackFile %>": fix`) makes that row track a
182
+ repointed `feedbackFile` the same way `file:` does — see
183
+ ["Variables"](#variables).
184
+
177
185
  ### `model:` — the opaque harness hint, template-rendered
178
186
 
179
187
  A state may declare `model: <string>` — an OPAQUE label (e.g. `smart`, `fast`,
@@ -343,14 +351,19 @@ the built-in names only.
343
351
  resolved state declares none — exactly like `model`. Plain `gtd status` prints
344
352
  `File:`/`Mode:` lines (right after `Model:`, when present) when set.
345
353
 
346
- **Known limitation — `on` pattern keys are NOT Eta templates.** A workflow's
347
- `on` patterns keep LITERAL `.gtd/…` paths, so repointing a filename var
348
- (`.gtdrc`'s top-level `vars:`, or a `GTD_VAR_` override) without ALSO overriding
349
- the workflow's `on` patterns desyncs the machine: `file:` (and any template
350
- reading/writing that path) follows the var, but the `on` map that decides what a
351
- change to that path MEANS keeps matching the old literal path. The vars are a
352
- DRY mechanism inside templates and the state↔file association, not a rename
353
- switch. (Making pattern keys var-aware at compile time is possible future work.)
354
+ **`on` pattern keys are Eta templates too.** A workflow's `on` patterns may name
355
+ a filename var the same way `file:`/content does — e.g.
356
+ `"A <%= it.vars.feedbackFile %>": fix` instead of a literal
357
+ `"A .gtd/FEEDBACK.md": fix`. The edge (`src/Edge.ts`'s `renderOnEdges`) renders
358
+ every pattern's key against `it.vars` ALONE (no diffs/commit hashes — a pattern
359
+ only ever needs to name a path) before handing the edges to the pure engine,
360
+ which only ever matches plain, already-rendered strings. This is what lets a
361
+ repointed filename var (`.gtdrc`'s top-level `vars:`, or a `GTD_VAR_` override)
362
+ reroute the machine consistently: `file:` (and any template reading/ writing
363
+ that path) and the `on` map that decides what a change to it MEANS both follow
364
+ the same var. A pattern written with a literal `.gtd/…` path instead of the var
365
+ still works exactly as before — only a workflow that templates its patterns
366
+ benefits from the repoint.
354
367
 
355
368
  ### `modes:` — pluggable steering-file modes
356
369
 
package/docs/loop.md CHANGED
@@ -41,16 +41,35 @@ loop picks their change up.
41
41
 
42
42
  **A `"message"` rest is a real handoff, not a slow beat.** No driver signs off
43
43
  for the human: in the bundled workflow the loop runs the whole cycle unattended
44
- up to `await-review` and then STOPS there with the review checkout window open
44
+ up to `await-review` and then rests there with the review checkout window open
45
45
  (the reviewable diff surfaced as uncommitted changes — see
46
46
  [STATES.md §11](../STATES.md#11-the-review-checkout-window)). Advancing takes a
47
47
  human edit to `.gtd/REVIEW.md`: ticking every box with **no** comment signs off
48
48
  into the squash finale, while any comment (a note, or a code edit of your own)
49
- is feedback that routes into another build + re-review round. Re-launching the
50
- loop after that edit picks the decision up. Until then the loop halts on every
51
- run — that is the contract, not a stall. A review nobody is going to finish is
52
- ended with [`gtd abandon`](cli.md#gtd-abandon---json), which drops the process's
53
- commits and keeps their content as uncommitted changes.
49
+ is feedback that routes into another build + re-review round. That gate is the
50
+ contract, not a stall — nothing advances until the edit is captured, whether the
51
+ capture happens within the same run or a later one. A review nobody is going to
52
+ finish is ended with [`gtd abandon`](cli.md#gtd-abandon---json), which drops the
53
+ process's commits and keeps their content as uncommitted changes.
54
+
55
+ **Editing is on by default.** Reaching a `"message"` rest, `bin/gtd` opens
56
+ `${VISUAL:-$EDITOR}` on the rest's declared `.file` (or the repo root `.` when
57
+ the state declares none), blocking until the editor exits, then runs
58
+ `gtd step human` to capture whatever changed:
59
+
60
+ - a new commit was captured → the loop keeps driving on its own; re-launching is
61
+ never necessary;
62
+ - zero commits were captured (a clean tree once the editor closed) → the loop
63
+ halts, exit 0 — "done for now";
64
+ - the step refuses (a malformed steering file, or an edit matching no declared
65
+ pattern) → the refusal is surfaced verbatim and the loop halts non-zero,
66
+ exactly as a manual `gtd step human` refusal would.
67
+
68
+ Set `GTD_NO_EDIT` (any non-empty value), or pass `--no-edit` — see
69
+ [`cli.md`'s `--no-edit`](cli.md#--no-edit) for its exact syntax and positioning
70
+ — to disable this and restore the original halt-and-print behavior: the loop
71
+ prints the gate via `gtd next` and exits 0 without touching an editor, leaving
72
+ the human to edit and re-launch the loop themselves.
54
73
 
55
74
  ```bash
56
75
  gtd next --json # ask who's up and what they should do
@@ -73,16 +92,18 @@ when the machine rests at a `"message"` gate — so `gtd` is the only command a
73
92
  human runs. `bin/gtd` is the packaged entry point: invoked bare, or with `loop`
74
93
  as its first argument, it runs this exact script; any other first argument (e.g.
75
94
  `next`, `step`, `status`) hands off to `node dist/gtd.bundle.mjs` instead. The
76
- loop body it runs adds four things on top of the reference script above: it
77
- stops with a diagnostic if the same `"prompt"` state/content repeat with no
78
- progress (see `skills/loop/SKILL.md`'s "Stall detection"); it lets
79
- `GTD_LOOP_AGENT_CMD` swap in any coding agent CLI in place of the default
80
- `claude -p`, receiving the prompt via `$GTD_LOOP_PROMPT`; it exports the
81
- resolved state's optional `model` hint as `$GTD_LOOP_MODEL`, appending
82
- `--model "$GTD_LOOP_MODEL"` to the default `claude -p` invocation whenever it's
83
- non-empty; and it acts on the optional `memory` scope hint (see "Agent memory
84
- scope" below) — continuing one agent session across consecutive same-scope turns
85
- and starting fresh when the scope changes.
95
+ loop body it runs adds five things on top of the reference script above: it
96
+ opens an editor at every `"message"` gate instead of just printing it (see
97
+ "Editing is on by default" above; `--no-edit`/`GTD_NO_EDIT` fall back to the
98
+ reference script's plain print-and-exit); it stops with a diagnostic if the same
99
+ `"prompt"` state/content repeat with no progress (see `skills/loop/SKILL.md`'s
100
+ "Stall detection"); it lets `GTD_LOOP_AGENT_CMD` swap in any coding agent CLI in
101
+ place of the default `claude -p`, receiving the prompt via `$GTD_LOOP_PROMPT`;
102
+ it exports the resolved state's optional `model` hint as `$GTD_LOOP_MODEL`,
103
+ appending `--model "$GTD_LOOP_MODEL"` to the default `claude -p` invocation
104
+ whenever it's non-empty; and it acts on the optional `memory` scope hint (see
105
+ "Agent memory scope" below) — continuing one agent session across consecutive
106
+ same-scope turns and starting fresh when the scope changes.
86
107
 
87
108
  `bin/gtd` resolves both its bundle hand-off and the loop's own internal `gtd`
88
109
  calls against `dist/gtd.bundle.mjs` next to its own location. Set `GTD_BIN` — a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "7.4.1",
3
+ "version": "7.5.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {