@pmelab/gtd 9.3.0 → 9.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.
@@ -189839,6 +189839,30 @@ const STATE_FIELDS = {
189839
189839
  doc: "When true, a step at this state is refused unless every open question in its qa-mode `file:` is answered — exactly one checkbox ticked per question. Requires a `file:` and `mode: qa`. Forbidden on a commit state."
189840
189840
  },
189841
189841
  /**
189842
+ * Optional. When `true`, a step at this state is REFUSED unless the paths
189843
+ * the human's own review-round commit touched have actually been reverted
189844
+ * out of the working tree — a `git apply -R` that silently applied nothing
189845
+ * (atomic without `--reject`/`-3`, so a failed patch leaves the tree
189846
+ * byte-for-byte unchanged) must not be mistaken for the note-only round its
189847
+ * `C` row also serves. Like the other guarded flags, the PURE engine never
189848
+ * reads it: the check lives at the edge (the require-revert guard in
189849
+ * `src/StepGuards.ts`), which re-establishes the fact from the tree itself
189850
+ * rather than trusting any signal the script left behind — idempotent, so a
189851
+ * script-revert, a hand-revert, or nothing-to-revert all answer alike.
189852
+ * Requires a `file:` (the state's own steering file, exempted by exact path
189853
+ * from the paths the guard considers residue); forbidden on a commit state
189854
+ * (never at rest — see `validateDefinition`).
189855
+ */
189856
+ requireRevert: {
189857
+ kind: "flag",
189858
+ surface: "def",
189859
+ authored: "state",
189860
+ commit: "forbidden",
189861
+ requires: "file",
189862
+ viz: "flag",
189863
+ doc: "When true, a step at this state is refused unless the paths the human's own review-round commit touched have actually been reverted out of the working tree — re-established from the tree itself, never from the script's own exit code. Requires a `file:`. Forbidden on a commit state."
189864
+ },
189865
+ /**
189842
189866
  * Authoring-only: an EXTRA reachability root (`WorkflowEntries.manual`),
189843
189867
  * enterable via `gtd --entry <this state's qualified name>`.
189844
189868
  * Never lands on the compiled `StateDef` — `compileState` validates its
@@ -189921,6 +189945,65 @@ const validateFieldRules = (name, state, key, spec) => {
189921
189945
  */
189922
189946
  /** Every mode name `def` declares in `modes:` (empty when it declares none) — the whole vocabulary a state's `mode:` may name, per this module (see `StateMode`'s doc comment for where the registry names come from). */
189923
189947
  const knownModes = (def) => Object.keys(def.modes ?? {});
189948
+ /**
189949
+ * `def`'s declared plumbing directory — the raw Eta template source a
189950
+ * workflow carried on `stateDir:`, or `DEFAULT_STATE_DIR` when it declared
189951
+ * none (so a workflow declaring nothing, and every hand-built test fixture
189952
+ * that skipped the compiler, behaves exactly as before this key existed).
189953
+ * Rendering the template is an edge concern, exactly like `entryBaseTemplateOf`
189954
+ * — this module only carries and defaults the raw string.
189955
+ */
189956
+ const stateDirOf = (def) => def.stateDir ?? ".gtd";
189957
+ /**
189958
+ * The leading-`./` + trailing-`/` strip shared by `stateDirError` and
189959
+ * `src/Edge.ts`'s `renderStateDir` — the one tolerated rewrite of a
189960
+ * `stateDir` declaration, covering the two conventional spellings a
189961
+ * hand-written template or `.gtdrc` value commonly carries. Exported as one
189962
+ * function rather than duplicated at the edge so the two call sites can never
189963
+ * drift into tolerating different affixes. Anything beyond these two is a
189964
+ * spelling `stateDirError` rejects rather than rewrites — the strip is
189965
+ * tolerance, not full canonicalization; it does not touch an internal `.` or
189966
+ * empty segment.
189967
+ */
189968
+ const canonicalStateDir = (value) => value.replace(/^\.\//, "").replace(/\/+$/, "");
189969
+ /**
189970
+ * The one place the engine interprets a `stateDir` VALUE (never the var
189971
+ * NAME) — pure and total, so both the edge (on the rendered, normalized
189972
+ * value, before any consumer sees it — see `src/Edge.ts`'s
189973
+ * `renderStateDirOrFail`) and a test can call it directly. Returns the
189974
+ * error message when `value` cannot name a usable plumbing directory,
189975
+ * `undefined` when it can.
189976
+ *
189977
+ * Rejects, in this order: blank/whitespace-only; the repo root (`.`, `./`,
189978
+ * `/`, or empty once a leading `./` and trailing `/` are stripped); an
189979
+ * absolute path (a leading `/` after that stripping); any `..` segment (an
189980
+ * escape); any other `.` or empty path segment (a non-canonical spelling —
189981
+ * `a/./state`, `a//state`, `a/.` — rejected rather than silently
189982
+ * canonicalized, since every downstream consumer compares `it.stateDir`
189983
+ * rather than re-normalizing it). The `..` escape check runs BEFORE the
189984
+ * segment-canonicality check so a value carrying BOTH is always reported as
189985
+ * an escape, never as a spelling to rewrite — `a/.././state` (segments `a`,
189986
+ * `..`, `.`, `state`) would, under the reverse order, suggest the canonical
189987
+ * spelling `"a/../state"`, a rewrite that still escapes the repo. `..` is
189988
+ * deliberately not in the canonicality predicate — it is an escape, not a
189989
+ * spelling. Deliberately does its own stripping rather than requiring an
189990
+ * already-normalized `value` — a caller may hand it either the raw
189991
+ * declaration or the edge's normalized form and get the same verdict, which
189992
+ * is what lets a unit test exercise `"./"` directly.
189993
+ */
189994
+ const stateDirError = (value) => {
189995
+ const invalid = () => `"stateDir": must name a directory inside the repository, not the repo root, an absolute path, or a path outside it (got ${JSON.stringify(value)})`;
189996
+ if (value.trim() === "") return invalid();
189997
+ const stripped = canonicalStateDir(value);
189998
+ if (stripped === "" || stripped === ".") return invalid();
189999
+ if (stripped.startsWith("/")) return invalid();
190000
+ const segments = stripped.split("/");
190001
+ if (segments.includes("..")) return invalid();
190002
+ if (segments.some((segment) => segment === "." || segment === "")) {
190003
+ const canonical = segments.filter((segment) => segment !== "." && segment !== "").join("/");
190004
+ return `"stateDir": ${JSON.stringify(stripped)} is not a canonical path — write it as ${JSON.stringify(canonical)}`;
190005
+ }
190006
+ };
189924
190007
  /** Which content kind a state declares, or `undefined` if none (a validation error). */
189925
190008
  const contentKindOf = (state) => {
189926
190009
  if (state.script !== void 0) return "script";
@@ -189943,6 +190026,8 @@ const entryBaseTemplateOf = (def, state) => {
189943
190026
  const isRequireProgressState = (def, state) => def.states[state]?.requireProgress === true;
189944
190027
  /** True when a step at `state` must be refused unless every open question in its `qa`-mode file is answered (see `StateDef.answerGate`). Safe for an unknown state name (returns `false`). */
189945
190028
  const isAnswerGateState = (def, state) => def.states[state]?.answerGate === true;
190029
+ /** True when a step at `state` must be refused unless the human's review-round paths have actually been reverted from the working tree (see `StateDef.requireRevert`). Safe for an unknown state name (returns `false`). */
190030
+ const isRequireRevertState = (def, state) => def.states[state]?.requireRevert === true;
189946
190031
  /**
189947
190032
  * Every declared state that is NOT a commit state, sorted (plain string
189948
190033
  * sort). This is intentionally ALL non-commit states, not just ones that
@@ -190357,6 +190442,18 @@ const validateKnownMode = (def, name, state) => {
190357
190442
  return [`state "${name}": "mode" must name a mode this workflow knows (${known.length > 0 ? known.join(", ") : "none declared"}) (got "${state.mode}")`];
190358
190443
  };
190359
190444
  /**
190445
+ * `stateDir`, when DECLARED, must be non-blank — the same
190446
+ * blank/whitespace-only check `validateReviewBaseTemplate` runs for a string
190447
+ * `reviewBase` template. Deliberately checks nothing else: `stateDirError`'s
190448
+ * repo-root/absolute/escape rules apply to the RENDERED value, which this
190449
+ * load-time pass can't see (the bundled declaration is itself a template,
190450
+ * e.g. `<%= it.vars.stateDir %>`, whose var can arrive at runtime via a
190451
+ * `GTD_STATEDIR` override no load-time check could ever observe — see
190452
+ * `renderStateDirOrFail` in `src/Edge.ts` for the one site that does own
190453
+ * that rule).
190454
+ */
190455
+ const validateStateDir = (def) => def.stateDir !== void 0 && def.stateDir.trim() === "" ? [`"stateDir" template must not be blank`] : [];
190456
+ /**
190360
190457
  * When `reviewBase` is a STRING (the template form — see `StateDef.reviewBase`),
190361
190458
  * its source must be non-blank: a literal `reviewBase: ""` (or whitespace-only)
190362
190459
  * is an authoring error, distinct from the runtime concern of the RENDERED
@@ -190461,7 +190558,9 @@ const validateState = (def, name, names) => {
190461
190558
  * `entries.manual` itself (see `validateEntries`); every state declares
190462
190559
  * exactly one content kind; commit states carry no `actor` and no `on`;
190463
190560
  * non-commit states carry an `actor`; every `modes:` entry declares at least
190464
- * one non-blank `format`/`validate` command; every state is reachable from an
190561
+ * one non-blank `format`/`validate` command; a DECLARED `stateDir` template is
190562
+ * non-blank (`validateStateDir` — the value-shape rule, `stateDirError`, is a
190563
+ * runtime/edge concern, not checked here); every state is reachable from an
190465
190564
  * entry root (`def.entries` — `default` plus every `entries.manual` state) by
190466
190565
  * walking `on` targets and `retry.otherwise` redirects (checked only when
190467
190566
  * `validateEntries` itself found no problem — see `validateReachability`).
@@ -190484,6 +190583,7 @@ const validateDefinition = (def) => {
190484
190583
  return [
190485
190584
  ...entriesErrors,
190486
190585
  ...validateModes(def),
190586
+ ...validateStateDir(def),
190487
190587
  ...names.flatMap((name) => validateState(def, name, names)),
190488
190588
  ...entriesErrors.length === 0 ? validateReachability(def, names) : []
190489
190589
  ];
@@ -191420,6 +191520,7 @@ const isSeededValidateCommand = (mode, command) => command === seededValidateCom
191420
191520
  * <name>:
191421
191521
  * format: <shell command> # at least one of format/validate
191422
191522
  * validate: <shell command>
191523
+ * stateDir: <string>? # optional — an Eta template naming where this workflow keeps its own plumbing; defaults to ".gtd" (see PatternMachine.stateDirOf)
191423
191524
  * entry:
191424
191525
  * default: <machine name> # which machine is the ROOT instance
191425
191526
  * machines:
@@ -191481,6 +191582,21 @@ const isSeededValidateCommand = (mode, command) => command === seededValidateCom
191481
191582
  * `isSeededValidateCommand` lets the edge (`src/SteeringMode.ts`) tell gtd's
191482
191583
  * own seeding apart from a genuine user override.
191483
191584
  *
191585
+ * ## `stateDir:` — where this workflow keeps its own plumbing
191586
+ *
191587
+ * A sibling `stateDir:` key INSIDE the `workflow:` value declares the raw Eta
191588
+ * template source for gtd's own scratch/bookkeeping directory (see
191589
+ * `PatternMachine.WorkflowDefinition.stateDir`/`stateDirOf`) — a
191590
+ * DEFINITION-level declaration, not a var, because the value must reach
191591
+ * `enforceStepGuards`'s `hasCodeChange` (`src/StepGuards.ts`), computed once
191592
+ * per call before any one guard runs, rather than any guard reaching into
191593
+ * `it.vars` itself (a blessed-config-key shape this repo forbids). Absent
191594
+ * compiles to `undefined`, so `stateDirOf`'s `.gtd` default applies — a
191595
+ * workflow declaring nothing behaves exactly as before this key existed. The
191596
+ * bundled template renders this from its own ordinary `vars.stateDir` (the
191597
+ * knob a user actually overrides) — this compiler never renders it itself,
191598
+ * the same discipline as a state's own `file:`.
191599
+ *
191484
191600
  * ## File references
191485
191601
  *
191486
191602
  * A content value (`script`/`prompt`/`message`/`commit`) is a FILE REFERENCE
@@ -191605,13 +191721,31 @@ const mergeModes = (base, override) => {
191605
191721
  };
191606
191722
  return merged;
191607
191723
  };
191724
+ /**
191725
+ * Compile the `stateDir:` key — the raw Eta template source for where this
191726
+ * workflow keeps its own plumbing (see `PatternMachine.WorkflowDefinition.stateDir`).
191727
+ * An absent key compiles to `undefined` — the definition then carries none,
191728
+ * and `stateDirOf`'s `.gtd` default applies. A non-string value pushes one
191729
+ * load error and drops the value, never guessed at. Never rendered here —
191730
+ * the pure engine carries the string verbatim, the same discipline as a
191731
+ * state's own `file:`.
191732
+ */
191733
+ const compileStateDir = (raw, errors) => {
191734
+ if (raw === void 0) return void 0;
191735
+ if (typeof raw !== "string") {
191736
+ errors.push(`"stateDir" must be a string, got ${describeType(raw)}`);
191737
+ return;
191738
+ }
191739
+ return raw;
191740
+ };
191608
191741
  /** Every state property a flattened state may carry — `STATE_FIELDS`'s own key set, in table order. */
191609
191742
  const KNOWN_STATE_KEYS = new Set(Object.keys(STATE_FIELDS));
191610
191743
  const KNOWN_TOP_KEYS = /* @__PURE__ */ new Set([
191611
191744
  "entry",
191612
191745
  "machines",
191613
191746
  "vars",
191614
- "modes"
191747
+ "modes",
191748
+ "stateDir"
191615
191749
  ]);
191616
191750
  const KNOWN_MACHINE_KEYS = /* @__PURE__ */ new Set([
191617
191751
  "params",
@@ -192122,6 +192256,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
192122
192256
  const vars = compileVarsMap(raw.vars, errors);
192123
192257
  const seeded = Object.fromEntries(builtInModeNames().map((name) => [name, { validate: seededValidateCommand(name) }]));
192124
192258
  const modes = mergeModes(mergeModes(seeded, compileModesMap(raw.modes, errors)), rcModes);
192259
+ const stateDir = compileStateDir(raw.stateDir, errors);
192125
192260
  validateMachinesShape(raw.machines, errors);
192126
192261
  const flattened = flattenMachines(raw, errors);
192127
192262
  if (flattened.entries === void 0) throw new Error(formatErrors(errors));
@@ -192138,7 +192273,8 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
192138
192273
  default: flattened.entries.default,
192139
192274
  manual
192140
192275
  },
192141
- modes
192276
+ modes,
192277
+ ...stateDir !== void 0 ? { stateDir } : {}
192142
192278
  };
192143
192279
  assertScopesCoverStates(Object.keys(states), flattened.scopes, errors);
192144
192280
  const definitionErrors = validateDefinition(definition);
@@ -192153,7 +192289,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
192153
192289
  };
192154
192290
  //#endregion
192155
192291
  //#region src/workflows/unified.yaml
192156
- 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 the README's \"How it works\" and \"Driving the loop\" sections for\n# 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 raw 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 MACHINES (entry:/machines:) ───────────────────────────────\n#\n# This template is written as a tree of reusable, parameterized MACHINES (see\n# src/Machines.ts) — a gate/loop written once and instantiated several times\n# with different `with:` bindings (dedup), or a complex cluster grouped under\n# one name for source comprehension (encapsulation). Every reference below is\n# an ordinary `{ machine: <name>, with: { ... } }` local, expanded at\n# compile-time into concrete, qualified states before the pure engine ever\n# sees the definition — no runtime footprint.\n#\n# MACHINE BOUNDARIES LINE UP WITH CONVERSATIONAL IDENTITY: a machine that holds\n# an identity (a planner or a coder persona) declares its own `model:` ONCE, at\n# the machine level (stamped onto every one of its own `prompt` states — see\n# src/Machines.ts's `resolveInstanceModel`), instead of repeating `model:` on\n# every state; no state anywhere declares `model:` or `memory:` directly any\n# more (the memory key is computed from the machine tree — see\n# src/PatternMachine.ts's `memoryScopeAt` / src/Edge.ts's `memoryKeyFor`).\n#\n# * entryGate (dedup ×2) — the green-baseline gate (check + human blocked)\n# guarding the single start (`start-gate`) and\n# the review (`review-gate`) entries. Its check\n# script IS the shared `&suiteCheck` anchor\n# (also aliased by `fix-precheck`, outside this\n# machine) — every entry blocks on any red tree,\n# full stop; there is no baseline-aware second\n# run any more, since `unwind` (below) already\n# put the tree back to the baseline before this\n# gate ever runs. Identity-free.\n# * healthGate (dedup ×2) — the check/escalate health pair shared by the\n# build tail's own tail (`build.health.*`) and\n# the per-package build's own tail\n# (`packages.item.health.*`) — each caller's OWN\n# `fix` state (a coder-identity action) lives\n# beside this machine, not inside it, since\n# fixing IS a coder action and this machine has\n# no identity of its own. Identity-free.\n# * questionGate (dedup ×2) — the check/answer pair shared by the design and\n# architecture phases' own open-questions gate\n# (`design.gate.*` / `architecture.gate.*`): a\n# shared probe script decides whether the\n# phase's steering file still has unanswered\n# questions, and only then rests the process at\n# a human answer gate. Identity-free.\n# * designPlan (encap) — the triage phase's one conversation: group the\n# diff that started the process into ordered,\n# classified concerns and resolve the PRODUCT\n# ones' open questions (`design.*`: triage,\n# gate.check, gate.answer). ▸ planner.\n# * archPlan (encap) — the technical phase's one conversation: a COLD\n# read of the settled concerns, resolving the\n# TECHNICAL open questions, then a mechanical\n# decomposition into package files\n# (`architecture.*`: author, gate.check,\n# gate.answer, decompose). A separate machine\n# from `designPlan` (not one fused machine) —\n# its own memory scope, since there may be a\n# handover between the user-facing and the\n# technical phase. ▸ planner.\n# * humanReview (encap) — the shared review tail, nested INSIDE\n# `buildTail` (`build.review.*`: reviewing →\n# await-review → deciding → collecting) — see\n# \"Why humanReview is nested, not a root sibling\"\n# below. ▸ planner — the implementer's OWN\n# follow-through on feedback is a coder action,\n# not a reviewer one, so it lives in\n# `build.addressing` instead.\n# * specReview (encap) — the per-package agentic review, a SINGLE\n# state (`packages.item.spec.review`) — a\n# machine whose entire job is to be a separate\n# mind from the implementer. ▸ planner — fixing\n# the reviewer's own findings is a coder action,\n# so it lives in `packages.item.fix-spec`\n# instead.\n# * packageLoop (encap) — the per-package build queue\n# (`packages.picking`). Identity-free — the\n# actual per-package build identity lives in its\n# own `item` child (packageItem, below).\n# * packageItem (encap) — the per-package build identity (`packages.\n# item.*`: building, fix-suite, fix-spec,\n# closing), plus its own `health` and `spec`\n# children. ▸ coder.\n# * buildTail (encap) — the process's build identity (`build.*`:\n# squashing, addressing, fix), plus its own\n# `health` child AND its own `review` child\n# (humanReview, above). No monolithic \"build the\n# whole plan\" state any more — every package is\n# built inside `packages.item`, and the queue\n# drains straight into `build.review`. ▸ coder.\n#\n# ── Why humanReview is nested, not a root sibling ───────────────────────────\n#\n# `humanReview` sits UNDER `buildTail` (`build.review`) rather than beside it\n# at the root, because a machine's memory scope is its dotted instance path\n# (see src/PatternMachine.ts's `memoryScopeAt`): entering a DESCENDANT scope\n# never breaks the parent's unbroken run, but entering a sibling does. The\n# review tail sits between the two coder turns that most need continuity —\n# `build.addressing` (follows through on review feedback) and `build.squashing`\n# (drafts the finale from the range this process built) — so hoisting it back\n# to a root sibling would force both of those turns to re-derive the feature\n# from scratch on every pass through the tail, instead of resuming the session\n# that built it. The reviewer itself is unaffected either way: `build.review.*`\n# is its own scope regardless of where it's nested, so it never resumes the\n# builder's session.\n#\n# ── One flow: triage into concerns, then packages, then the shared tail ────\n#\n# ANY change to the tree starts the process — a hand-edit to real code, a\n# scratch note, anything at all — because it's a SKETCH, not finished work:\n# there is no fork on which steering file the human happened to create;\n# `idle` has exactly one outgoing edge, into `unwind`, which reverts that\n# whole diff back out of the working tree before planning starts (its intent\n# survives in history for `design.triage` to read; the squash finale means it\n# never reaches the final commit either way). `unwind` itself has exactly one\n# outgoing edge, into `start-gate`.\n#\n# EVERY entry gates on a green baseline before proceeding, and all three now\n# keep the exact same plain rule: block on any red tree, full stop. A red run\n# at any of them halts at a human blocked gate (fix the failures and re-run,\n# exactly like the escalate states). `start-gate` needs no baseline-aware\n# second run any more — by the time it runs, `unwind` has already put the\n# tree back to the baseline, so ONE suite run there IS the baseline verdict.\n#\n# Once green, `design.triage` reads the diff that started the process (never\n# rendered into the prompt — it names the start commit and reads the range up\n# to the unwind commit itself) and groups it into an ORDERED list of\n# CONCERNS, each one leaving the suite green on its own, classifying each as\n# PRODUCT or TECHNICAL. It raises open questions for the product ones only,\n# and folds EVERYTHING the start diff added into the concerns — a scratch\n# sketch and a real hand-edit alike, since `unwind` already emptied the tree\n# of both; nothing is left to delete. `design.gate` is a shared check+answer\n# pair: its check probes `requirementsFile` for unanswered questions and only\n# rests the process at a human answer gate when some remain — a\n# question-free phase skips the human stop entirely.\n#\n# Once the product questions are settled, `architecture.author` picks up as a\n# COLD reader (a separate machine, a separate memory scope — it reads\n# `requirementsFile` in full rather than resuming design's conversation),\n# develops the *how* for each settled concern, and raises only TECHNICAL open\n# questions — routed through the same shared `questionGate` shape\n# (`architecture.gate`) over `architectureFile`. Once those are settled,\n# `architecture.decompose` is a purely mechanical write-out: one package file\n# per concern, in the settled order — no merge/split judgement, since that\n# already happened in triage's grouping.\n#\n# From there the per-package build queue (`packages.*`) and the shared review\n# + squash tail (`build.*`) are UNCHANGED from before this restructure — see\n# their own machine doc comments below for the per-package build/review/\n# health mechanics.\n#\n# A second entry, `gtd --entry review-gate.check`, jumps there directly and,\n# once green, to `build.review.reviewing` to review a foreign branch through\n# the same shared tail. A third, `gtd --entry fix-precheck`, jumps there: a\n# red suite drops straight into the shared `build.fix` loop and out through the\n# review + squash tail; a green suite is a no-op back to `idle`. Both states\n# declare `entry: true` — otherwise unreachable from `entries.default`.\n#\n# All entries converge at `build.review.reviewing` -> `build.review.await-review`.\n# Ticking a REVIEW.md box means only \"I reviewed this hunk\"; a COMMENT (not an\n# unticked box) is what asks for changes. Every human step at\n# `build.review.await-review` goes to `build.review.deciding`, which inspects\n# 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 -> build.squashing -> done (the ONLY path\n# to the finale — the whole process collapses into one commit).\n# * any comment — a note in REVIEW.md OR a hand-edit to code — ->\n# build.review.deciding writes REVIEW_FEEDBACK.md -> build.review.collecting\n# implements it (completing any code the human hand-edited, never\n# reverting their lines) -> build.addressing (a coder action, not a\n# reviewer one — see \"Why humanReview is nested\" above) -> back through\n# `build.health.check` (the shared health pair, bound via the\n# `$onFeedback`/local `review` reference) -> build.review.reviewing\n# regenerates an INCREMENTAL REVIEW.md (build.review.deciding carries\n# `reviewBase: true`, so each feedback round's review + checkout window\n# covers only `last-review..HEAD`, i.e. the agent's follow-through, not the\n# human's own edit) -> await-review, looping until sign-off.\n# Two dead ends never commit: the review-signoff guard (src/StepGuards.ts)\n# REFUSES a step that leaves a box unticked with no comment (finish reviewing\n# first) and a deleted REVIEW.md (restore it and tick the boxes).\n#\n# The shared health pair (`build.health.check`/`.escalate`, plus its own\n# `build.fix`) serves the feedback loop — its green edge targets\n# `build.review.reviewing`. The per-package loop keeps its OWN health pair\n# (`packages.item.health.check`/`.escalate`, plus `packages.item.fix-suite`)\n# because its green edge targets the per-package `packages.item.spec.review`\n# 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 the README's \"Variables\" section).\n\nvars:\n testCommand: npm test\n # Where gtd keeps its own scratch/bookkeeping (the check scripts' temp output\n # file, review-deciding's state-file exclusion). Independent of the per-file\n # vars below — relocating a file wholesale means setting both.\n stateDir: .gtd\n # The only per-file var gtd never writes and never reads — the human's own\n # sketch pad, named so the one \"open steering file\" keybinding has an\n # answer at `idle` too.\n todoFile: .gtd/TODO.md\n requirementsFile: .gtd/REQUIREMENTS.md\n architectureFile: .gtd/ARCHITECTURE.md\n # Stamped (never authored, never read for its content) by design.gate.check\n # / architecture.gate.check — its mere presence is the routing signal that\n # the current phase's steering file still has unanswered questions.\n questionsFile: .gtd/QUESTIONS.md\n packagesDir: .gtd/packages\n nextFile: .gtd/NEXT.md\n # Written by packages.item.building when a package's acceptance criteria are\n # already met (an earlier package's fix turn pulled the work in); read by\n # the spec reviewer as the evidence a clean turn there is a real approval,\n # not a lazy/crashed one; removed by packages.item.closing.\n satisfiedFile: .gtd/SATISFIED.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_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 triage/design/review turns;\n # `coderModel` (default \"base\") is the tier for the build/fix turns.\n plannerModel: smart\n coderModel: base\n # The fixed diff base for `gtd --entry review-gate.check --var\n # reviewBase=<commitish>` — a plain non-gtd branch reviewed against that\n # commitish rather than the process start. Declared here (rather than left\n # undeclared) so `--var reviewBase=...` passes the CLI's declared-name\n # check; the default empty string renders blank, which the entry command\n # refuses (a review entry always requires an explicit base).\n reviewBase: \"\"\n\nentry:\n default: unified\n\nmachines:\n # ── DEDUP: the green-baseline entry gate (identity-free) ──────────────────\n # Runs the suite before any work starts — both `start-gate` and\n # `review-gate` reach this one check, and its script IS the shared\n # `&suiteCheck` anchor (defined once, right here, and aliased by\n # `fix-precheck` below); the `on` rules turn the result into \"halt at\n # blocked\" vs \"proceed to $onGreen\". Kept SEPARATE from `healthGate` (below)\n # even though both are a check+something-else shape: this gate runs BEFORE\n # planning, so it must NOT sweep any steering file `healthGate` does.\n entryGate:\n params: [onGreen, blockedMessage, blockedDescribe, checkLabel, blockedLabel, reviewBase]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: &suiteCheck |\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 \"<%~ it.vars.stateDir %>\"\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 mkdir -p \"$(dirname \"$feedback\")\"\n <%~ it.vars.testCommand %> > <%~ it.vars.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.vars.stateDir %>/.check-output ]; then\n mv <%~ it.vars.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.vars.stateDir %>/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n # A repeat identical failure would otherwise leave FEEDBACK.md\n # byte-identical to its committed copy — no diff, so the check would\n # look GREEN. Stamp each red run with the current HEAD (which always\n # advances between checks) so a still-red re-run always re-registers\n # as an M/A edit and takes the red edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$feedback\"\n else\n rm -f <%~ it.vars.stateDir %>/.check-output\n rm -f \"$feedback\"\n fi\n # `entry: true` on the shared local marks both `entryGate` instances\n # (start-gate/review-gate) as manual entries — only `review-gate.check`\n # actually needs the reachability root (it is otherwise unreachable\n # from `entries.default`), but the shared dedup makes marking one mark\n # both. Accepted as correct: both ARE the workflow's entry gates.\n entry: true\n # `reviewBase: $reviewBase` — only `review-gate` binds a real\n # template (`<%= it.vars.reviewBase %>`), fixing the whole process's\n # diff base to whatever `gtd --entry review-gate.check --var\n # reviewBase=<commitish>` supplies. `start-gate` binds the literal\n # empty string, which compiles away to \"no base\" here — it is reached\n # from `idle`, never manually entered with a base.\n reviewBase: $reviewBase\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 action: Retry check\n describe: $blockedDescribe\n\n # ── DEDUP: the check/escalate health pair (identity-free) ─────────────────\n # Runs the suite; a red run records the configured feedbackFile and routes to\n # the caller's OWN fixer (`$onRed` — a coder-identity action that lives\n # beside this machine, not inside it, since fixing IS a coder action and this\n # machine has no identity); a green run cleans it up and routes to\n # `$onGreen`. Escalation redirects here after the caller's fixer has retried\n # `max` times without success. Instantiated for the build tail's own tail\n # (`build.health.*`, caller's fixer is `build.fix`) and the per-package\n # build's own tail (`packages.item.health.*`, caller's fixer is\n # `packages.item.fix-suite`).\n healthGate:\n params: [onGreen, onRed, escalateDescribe, checkLabel, escalateLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # This script ALSO sweeps spent steering files (`reviewRawFile`,\n # `reviewFeedbackFile`) — a mechanic `entryGate.check` must NOT run,\n # since it executes before planning, while a fresh sketch legitimately\n # still exists.\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 \"<%~ it.vars.stateDir %>\"\n # Hoist every 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\n # 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 mkdir -p \"$(dirname \"$feedback\")\"\n # Sweep steering files spent by the states that led here: the raw\n # review capture (review.collecting) and the feedback instructions\n # (build.addressing) — each `rm -f` is a no-op when the file is\n # already gone or never existed on this path.\n reviewRaw=\"<%~ it.vars.reviewRawFile %>\"\n reviewFeedback=\"<%~ it.vars.reviewFeedbackFile %>\"\n rm -f \"$reviewRaw\" \"$reviewFeedback\"\n <%~ it.vars.testCommand %> > <%~ it.vars.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.vars.stateDir %>/.check-output ]; then\n mv <%~ it.vars.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.vars.stateDir %>/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n # A repeat identical failure would otherwise leave FEEDBACK.md\n # byte-identical to its committed copy — no diff, so the check would\n # look GREEN. Stamp each red run with the current HEAD (which always\n # advances between checks) so a still-red re-run always re-registers\n # as an M/A edit and takes the red edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$feedback\"\n else\n rm -f <%~ it.vars.stateDir %>/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A <%= it.vars.feedbackFile %>\": $onRed\n \"M <%= it.vars.feedbackFile %>\": $onRed\n \"* **\": $onGreen\n \"C\": $onGreen\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 land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $escalateDescribe\n\n # ── DEDUP: the open-questions check/answer pair (identity-free) ───────────\n # Shared by design and architecture's own gate (`design.gate`/\n # `architecture.gate`): `check`'s script probes whichever of\n # requirementsFile/architectureFile currently exists (see the script's own\n # comment) with `gtd check qa --open-questions` and stamps/removes\n # `questionsFile` on the result; only when it's present does the process\n # rest at `answer`, a human gate. A question-free phase never rests here at\n # all — `check`'s own `on` rows skip straight to `$onNone`. Takes NO `file:`\n # PARAM for the probe script itself: machine `$param` substitution is\n # whole-value only, so a path cannot be spliced into a shared script body —\n # `file` is instead the PARAM `answer` renders as its own `file:` (the\n # human-facing gate), a different use entirely from the probe.\n questionGate:\n params: [file, message, onNone, onRevise, checkLabel, answerLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: |\n #!/usr/bin/env bash\n # gtd check turn — probe the current phase's steering file for\n # unanswered questions; an answered/empty result removes the marker\n # (questionsFile), an unanswered one stamps it fresh. Design and\n # architecture share this one probe: whichever of\n # requirementsFile/architectureFile exists ON DISK right now IS this\n # phase's steering file — unambiguous because architecture.author\n # deletes requirementsFile in the same turn it writes\n # architectureFile, so exactly one of the two exists here.\n set +e\n mkdir -p \"<%~ it.vars.stateDir %>\"\n # Hoist every 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\n # 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 questions=\"<%~ it.vars.questionsFile %>\"\n mkdir -p \"$(dirname \"$questions\")\"\n requirements=\"<%~ it.vars.requirementsFile %>\"\n architecture=\"<%~ it.vars.architectureFile %>\"\n file=\"$requirements\"\n [ -f \"$file\" ] || file=\"$architecture\"\n gtd check qa \"$file\" --open-questions > /dev/null 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n printf 'open questions remain in %s\\n' \"$file\" > \"$questions\"\n # A repeat identical result would otherwise leave the marker\n # byte-identical to its committed copy — no diff, so the check\n # would look answered. Stamp each round with the current HEAD\n # (which always advances between checks) so a still-open re-run\n # always re-registers as an M/A edit and takes the answer edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$questions\"\n else\n rm -f \"$questions\"\n fi\n on:\n \"A <%= it.vars.questionsFile %>\": answer\n \"M <%= it.vars.questionsFile %>\": answer\n \"D <%= it.vars.questionsFile %>\": $onNone\n \"C\": $onNone\n answer:\n actor: human\n label: $answerLabel\n file: $file\n mode: qa\n answerGate: true\n message: $message\n on:\n \"* **\":\n to: $onRevise\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace\n `_your answer_` for your own) to send it back for the agent to\n fold your answers in. Delete a question to skip it, or delete\n the whole `## Open Questions` section to accept the plan as-is.\n\n # ── ENCAPSULATION: the triage phase's one conversation ─────────────────────\n # Groups the diff that started the process into ordered, classified\n # concerns and resolves the PRODUCT ones' open questions — one machine\n # identity across the whole back-and-forth, so the agent explores the\n # codebase once and the human's answer rationale survives every return lap.\n # ▸ planner identity — `model:` declared once here, stamped onto `triage`\n # (its only prompt state; `gate.answer` is a human turn and carries no\n # prompt).\n designPlan:\n model: <%= it.vars.plannerModel %>\n params: [onArchitecture]\n entry: triage\n states:\n triage:\n actor: agent\n label: Triaging the change\n file: <%= it.vars.requirementsFile %>\n mode: qa\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.requirementsFile %>`; develop it. Do\n not create other files to hold notes or output.\n\n `<%= it.vars.todoFile %>` is the likely home of the sketch that\n started this process. Despite living beside this workflow's own\n state files, it is the HUMAN'S INPUT — the sketch to be folded into\n the concerns below like any other part of the start diff. It is\n never a state file to preserve, and never gtd bookkeeping to ignore.\n\n This process started at commit `<%= it.startCommit %>`. The change\n that started it was reverted out of the working tree by the\n `unwind` beat right after it landed, so `git diff <%= it.startCommit %>`\n (against the working tree) is now empty — its content survives only\n in history. Find the entry commit yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\n (the process's very first turn, landed immediately after\n `<%= it.startCommit %>`, before any later baseline-repair commits) —\n then `git show` that commit to see everything that started this\n process: a hand-edit to real code, a scratch note in some file, or\n both.\n\n On the FIRST lap, `<%= it.vars.requirementsFile %>` does not exist\n yet (or holds whatever the human's own change happened to leave\n there). Build it from that start diff:\n\n 1. Group the diff into an ORDERED list of CONCERNS — each one a\n coherent piece of work that can leave the test suite GREEN on\n its own, in the order you'd want them built. If a concern would\n red-light tests that a later concern is meant to fix, that is\n not a valid split: merge the two into one concern.\n 2. Classify each concern as PRODUCT (a user-facing/requirements\n decision) or TECHNICAL (an implementation decision).\n 3. For every PRODUCT concern with an open point you cannot settle\n yourself, add a question under a `## Open Questions` heading near\n the top: one `### <question>` sub-heading each, followed by a\n checkbox list of candidate answers — aim for TWO concrete options\n 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 Raise questions ONLY for product concerns here — a technical\n concern's open points wait for the next phase. Never tick a box\n yourself — the human ticks exactly one per question.\n 4. Fold EVERYTHING the entry commit added into the concerns above —\n a scratch note and a real code edit are both just a SKETCH of\n the intent, a spec to be finished, never work to be preserved;\n `unwind` already reverted both out of the tree, so there is\n nothing left to delete. Describe each piece under the concern it\n belongs to.\n\n On a RETURN lap the human has ticked one option per question (or\n replaced `_your answer_` with their own text) in\n `<%= it.vars.requirementsFile %>`. For each answered question: fold\n the chosen answer into its concern's prose, then MOVE the question\n down into a `## Answered Questions` section as `### <question>`\n followed by the resolved answer in plain PROSE (drop the\n checkboxes). Never re-raise a question the human deleted; if the\n human deleted the whole `## Open Questions` section, treat every\n product concern as accepted and surface no new questions unless\n something is genuinely still blocking. Omit `## Open Questions`\n entirely once there are none left.\n\n Leave `<%= it.vars.requirementsFile %>` uncommitted and finish your\n turn.\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: <%= it.vars.requirementsFile %>\n onNone: $onArchitecture\n onRevise: triage\n checkLabel: Checking for open questions\n answerLabel: Awaiting your product answers\n message: |\n `<%= it.vars.requirementsFile %>` holds the concerns under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n # ── ENCAPSULATION: the technical phase's one conversation ──────────────────\n # A separate machine from `designPlan` — its own memory scope, since there\n # may be a handover between the user-facing and the technical phase: this\n # phase does NOT resume design's conversation, it reads\n # `<%= it.vars.requirementsFile %>` cold. Resolves the TECHNICAL open\n # questions, then mechanically decomposes the settled concerns into package\n # files (no merge/split judgement — that already happened in triage's\n # grouping). ▸ planner identity — `model:` declared once here, stamped onto\n # `author` and `decompose` (its own prompt states; `gate.answer` is a human\n # turn and carries no prompt).\n archPlan:\n model: <%= it.vars.plannerModel %>\n params: [onPackages]\n entry: author\n states:\n author:\n actor: agent\n label: Refining the technical plan\n file: <%= it.vars.architectureFile %>\n mode: qa\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state files this\n turn touches are `<%= it.vars.architectureFile %>` (write it) and\n `<%= it.vars.requirementsFile %>` (delete it once its content is\n folded in). Do not create other files to hold notes or output.\n\n You do NOT resume the design conversation — this is a separate\n machine with its own memory, a COLD read every time. Read\n `<%= it.vars.requirementsFile %>` (the settled, ordered, classified\n concerns) in full. Treat every product decision recorded there —\n plain prose or a resolved `## Answered Questions` entry — as\n SETTLED; never re-open it.\n\n On the FIRST lap, develop `<%= it.vars.architectureFile %>` from\n those concerns: for each one, in the same order, work out the *how*\n — file/module structure, data models, library/tech-stack choices,\n error-handling strategy — building on the *what* already settled.\n For every open TECHNICAL point you cannot settle yourself, add a\n question under a `## Open Questions` heading exactly as the\n requirements phase does: one `### <question>` each, followed by a\n checkbox list — TWO concrete options plus a final free-text slot,\n 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\n option per question (or replaced `_your answer_` with their own\n text): fold each chosen answer into the plan prose, then MOVE the\n question into a `## Answered Questions` section as `### <question>`\n + the resolved answer in plain PROSE (drop the checkboxes). Never\n re-raise a deleted question; treat a deleted `## Open Questions`\n section as acceptance. Omit `## Open Questions` once there are none\n left.\n\n Once `<%= it.vars.architectureFile %>` is written, delete\n `<%= it.vars.requirementsFile %>` — its content has been folded in\n and it must not linger. Leave everything uncommitted and finish\n your turn.\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: <%= it.vars.architectureFile %>\n onNone: decompose\n onRevise: author\n checkLabel: Checking for open questions\n answerLabel: Awaiting your technical answers\n message: |\n `<%= it.vars.architectureFile %>` holds the technical plan under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n decompose:\n actor: agent\n label: Decomposing into packages\n prompt: |\n You are an autonomous coding agent. This workflow steers itself through\n its own state files — treat them as its private scratchpad, never as\n project code or documentation. The only state files this turn touches\n are the package files it writes under `<%= it.vars.packagesDir %>/` and\n `<%= it.vars.architectureFile %>`, which it deletes. Do not create other\n files to hold notes or output.\n\n If you wrote `<%= it.vars.architectureFile %>` earlier in this\n conversation, work from what you already settled; otherwise read it\n (the converged technical plan) yourself. It already lists an ORDERED\n set of concerns with every merge/split judgement made — this turn is\n a mechanical write-out, not a planning one.\n\n Write one package file per concern, in the settled order, under\n `<%= it.vars.packagesDir %>/` (e.g.\n `<%= it.vars.packagesDir %>/01-name.md`,\n `<%= it.vars.packagesDir %>/02-name.md`, ...). Each package file\n carries that concern's requirement (so it can be reviewed against\n its spec), its independent tasks, and each task's acceptance\n criteria as `- [ ]` checkboxes and relevant paths. Do NOT merge or\n split concerns here — that judgement already happened upstream;\n carry the settled grouping over verbatim. No package file may\n reference any other `.gtd/` file.\n\n Once the package files are written, delete\n `<%= it.vars.architectureFile %>`. Leave everything uncommitted and\n finish your turn.\n on:\n \"* <%= it.vars.packagesDir %>/**\": $onPackages\n\n # ── ENCAPSULATION: the shared review tail ──────────────────────────────────\n # ▸ planner identity — `model:` declared once here, stamped onto `reviewing`\n # and `collecting` (its only prompt states). The implementer's OWN\n # follow-through on feedback is a coder action, not a reviewer one — it\n # lives in `build.addressing` (`$onFeedback`), not inside this machine.\n humanReview:\n model: <%= it.vars.plannerModel %>\n entry: reviewing\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: <%= it.vars.reviewFile %>\n mode: review\n # `gtd --entry review-gate.check` (a plain non-gtd branch, e.g. a\n # colleague's PR) does NOT enter here directly — it enters\n # `review-gate.check` first, which runs the green-baseline gate and,\n # once green, transitions here. The `Gtd-Review-Base:` trailer rides on that\n # entry commit (the process's oldest), so everything below still reuses\n # the same reviewing/await-review/feedback machinery over <commitish>..HEAD.\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.reviewFile %>`; write it at exactly\n that path. Do not create other files to hold notes or output.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the changes in\n this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.reviewBase %> -->`\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 This prompt carries no diff — read the changes yourself. The range runs\n from `<%= it.reviewBase %>` to the working tree (committed turns plus\n anything still pending); on a feedback round that base is the previous\n review's boundary, so it covers only what changed since then, not items\n already signed off.\n\n Leave `<%= it.vars.reviewFile %>` uncommitted and finish your turn.\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 process rests here, gtd opens a \"review checkout window\": raw HEAD\n # is rewound to the review base (the most-recent `deciding` commit — so a\n # feedback round shows only `last-review..HEAD` — or the process start on\n # 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 `deciding`, which decides sign-off vs.\n # 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 review-signoff guard (`src/StepGuards.ts`)\n # catches them BEFORE the step commits and refuses: an unfinished\n # review (a box still `- [ ]` with no comment) and a deleted REVIEW.md.\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the process — 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 land`:\n\n - **Sign off** — tick EVERY box and leave no comment — no note in\n `<%= it.vars.reviewFile %>`, no code edit — to collapse the process into\n one commit (**build.squashing**).\n - **Request changes** — leave a comment: 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** → **build.addressing**). 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 \"* **\": deciding\n\n 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 process 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 -> deciding` commit; HEAD^ is the `reviewing`\n # commit that wrote the agent's original REVIEW.md (the sign-off gate\n # refuses an incomplete step, so `deciding` is entered exactly once per\n # round — nothing commits in between). It is FEEDBACK when the human left\n # a comment: a note in REVIEW.md (any edit beyond a \"[ ]\"->\"[x]\" tick) OR a\n # hand-edit to any file this round other than `.gtd/` or the state's own\n # reviewFile (which is var-configurable and may live outside `.gtd/`, e.g.\n # a root-level REVIEW.md — issue #128). Otherwise it is a clean sign-off.\n # This turn only CAPTURES the raw material into REVIEW_RAW.md (the\n # `collecting` agent then turns it into instructions); it does NOT\n # 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 # 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 stateDir=\"<%~ it.vars.stateDir %>\"\n reviewFile=\"<%~ it.vars.reviewFile %>\"\n reviewRawFile=\"<%~ it.vars.reviewRawFile %>\"\n head=$(git rev-parse HEAD)\n mkdir -p \"$(dirname \"$reviewRawFile\")\"\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 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 # Did the human hand-edit anything this round besides stateDir and the\n # reviewFile itself (var-configurable — may live outside stateDir, so it\n # is excluded by exact path, not just the stateDir prefix)?\n if git diff-tree --no-commit-id --name-only -r HEAD \\\n | grep -v \"^$stateDir/\" | grep -vxF \"$reviewFile\" | grep -q . \\\n || [ \"$before\" != \"$after\" ]; then\n {\n echo \"Raw review material captured for classification. The human reviewed the changes and left the feedback below; a downstream agent turns it into an instruction list. This file is machine-captured input, not instructions — it names a commit, never inlined content.\"\n echo\n echo \"Commit: $head\"\n echo \"The human's notes are in $reviewFile at this commit; their hand edits,\"\n echo \"if any, are in that commit's other paths. Run: git show $head\"\n } > \"$reviewRawFile\"\n fi\n rm -f \"$reviewFile\"\n on:\n \"A <%= it.vars.reviewRawFile %>\": collecting\n \"M <%= it.vars.reviewRawFile %>\": collecting\n \"D <%= it.vars.reviewFile %>\": $onSignoff\n\n collecting:\n actor: agent\n label: Collecting your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n prompt: |\n You are an autonomous coding agent turning raw review feedback into an\n explicit instruction list for a builder. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state files this\n turn touches are `<%= it.vars.reviewRawFile %>` (read it, then\n delete it) and `<%= it.vars.reviewFeedbackFile %>` (write it) — do\n NOT change any other file, you classify, you do not build.\n\n The raw review material is: <%~ it.read(it.vars.reviewRawFile) %>\n\n It names a commit. If you wrote today's review earlier in this\n conversation, work from what you already reviewed; otherwise read that\n commit's diff yourself before classifying anything.\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>`. 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 %>\": $onFeedback\n \"M <%= it.vars.reviewFeedbackFile %>\": $onFeedback\n\n # ── ENCAPSULATION: the per-package agentic review ──────────────────────────\n # A single state: a machine whose entire job is to be a separate MIND from\n # the implementer, distinct from the coder identity that builds/fixes the\n # package (`packages.item.*`). ▸ planner identity — `model:` declared once\n # here, stamped onto `review` (its only prompt state). Fixing the reviewer's\n # own findings is a coder action, not a reviewer one — it lives in\n # `packages.item.fix-spec` (`$onFix`), not inside this machine.\n specReview:\n model: <%= it.vars.plannerModel %>\n params: [onApproved, onFix]\n entry: review\n states:\n review:\n actor: agent\n label: Reviewing the package\n # Bounded per process: once entered `max` times, a transition into it is\n # redirected to `$onApproved` (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 # `build.fix` cap has (see the README's \"Why\" section, \"Bounded, not\n # runaway\").\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. This workflow steers itself through its own\n state files — treat them as its private scratchpad, never as project\n code or documentation. The only state file this turn touches is\n `<%= it.vars.specFeedbackFile %>`; write it only when — and only\n when — you find problems. Do not create other files to hold notes or\n output.\n\n The package spec is: <%~ it.read(it.vars.nextFile) %>\n\n Read that spec, then verify the package's implementation against it: are\n all its tasks done, acceptance criteria met, and the code sound and\n consistent with the codebase? This prompt carries no diff — read it\n yourself: the range runs from `<%= it.startCommit %>` to the working\n tree.\n\n Note the range is process-wide (from the start of the whole build, not\n just this package), so it can span earlier packages too.\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 on:\n \"A <%= it.vars.specFeedbackFile %>\": $onFix\n \"M <%= it.vars.specFeedbackFile %>\": $onFix\n \"D <%= it.vars.specFeedbackFile %>\": $onApproved\n \"C\": $onApproved\n\n # ── ENCAPSULATION: the per-package build queue ─────────────────────────────\n # Identity-free (a bookkeeping queue, not a builder) — the actual per-package\n # build identity lives in its own `item` child (packageItem, below).\n packageLoop:\n params: [onDrained]\n entry: picking\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 # Sweep the design/architecture phases' spent steering files:\n # requirementsFile is consumed by architecture.author, architectureFile\n # by architecture.decompose, and questionsFile by whichever gate ran\n # last — all three are gone by the time the package queue starts.\n # No-op if already removed.\n requirements=\"<%~ it.vars.requirementsFile %>\"\n architecture=\"<%~ it.vars.architectureFile %>\"\n questions=\"<%~ it.vars.questionsFile %>\"\n rm -f \"$requirements\" \"$architecture\" \"$questions\"\n next=$(ls <%~ it.vars.packagesDir %>/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n mkdir -p \"$(dirname \"$nextFile\")\"\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 %>\": item.building\n \"C\": $onDrained\n\n # The per-package build identity — its own coder machine (below), so\n # deleting a package file's own lifecycle stays inside `item` while the\n # queue itself (`packages`) stays a single state.\n item:\n machine: packageItem\n with:\n onNext: picking\n\n # ── ENCAPSULATION: the per-package build identity ──────────────────────────\n # ▸ coder identity — `model:` declared once here, stamped onto `building`,\n # `fix-suite`, and `fix-spec` (its own prompt states). `closing` ends that\n # package file's own lifecycle; keeping it here (not in the queue) keeps\n # `packages` itself to a single state. `packages.item.closing` -> `picking`\n # is the ONE upward-resolving target in the whole file, so it MUST be a\n # param (`$onNext`, bound by `packages` — see the machine's own reference).\n packageItem:\n model: <%= it.vars.coderModel %>\n params: [onNext]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn may write is `<%= it.vars.satisfiedFile %>` (see below); do NOT\n delete the package file (the spec-review gate reads it after you)\n and never touch `<%= it.vars.nextFile %>` — the `picking` state owns\n it.\n\n The package to implement is: <%~ it.read(it.vars.nextFile) %>\n\n Before implementing anything, check the package's acceptance criteria\n against the current tree — an earlier package's fix turn may already\n have satisfied them. If **every** criterion is already met, implement\n nothing: write `<%= it.vars.satisfiedFile %>` recording each criterion\n and the concrete evidence it is already satisfied (the commit, file,\n or symbol that proves it), change nothing else, and finish your turn.\n If any criterion is outstanding, implement the package normally and\n do not write that file.\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 \"A <%= it.vars.satisfiedFile %>\": &satisfied\n to: health.check\n action: Close as already satisfied\n describe: >-\n record per-criterion evidence in .gtd/SATISFIED.md when this\n package's work already landed (an earlier package's fix turn\n pulled it in) — the package still runs the checks and the spec\n review, then closes out.\n \"M <%= it.vars.satisfiedFile %>\": *satisfied\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.feedbackFile %>`; fix it per its\n contents, or delete it if the feedback turns out to be wrong. Do not\n create other files to hold notes or output.\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. If the\n only way to green the suite is to implement work that another\n package file describes, make the smallest change that gets there and\n do it — that later package will then legitimately report itself\n already satisfied when its turn comes.\n\n Leave everything uncommitted and finish your turn — do not commit.\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n fix-spec:\n actor: agent\n label: Fixing review feedback\n file: <%= it.vars.specFeedbackFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.specFeedbackFile %>`; address it, then\n delete it. Do not create other files to hold notes or output.\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 \"* **\": health.check\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 and\n # already-satisfied evidence, so `picking` selects the NEXT package (or\n # falls through to review when the queue is empty). Reached on approval\n # (spec review clean) and on the spec review retry cap (force-close,\n # 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 %> <%~ it.vars.satisfiedFile %>\n on:\n \"* **\": $onNext\n\n # Per-package health pair: packages.item.health.check/.escalate ->\n # packages.item.spec.review (green) or packages.item.fix-suite (red).\n health:\n machine: healthGate\n with:\n onGreen: spec\n onRed: fix-suite\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**packages.item.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # Per-package agentic review: packages.item.spec.review -> closing\n # (approved), or packages.item.fix-spec (findings — re-verify via the\n # full health check before re-reviewing).\n spec:\n machine: specReview\n with:\n onApproved: closing\n onFix: fix-spec\n\n # ── ENCAPSULATION: the process's build identity ─────────────────────────────\n # ▸ coder identity — `model:` declared once here, stamped onto `squashing`,\n # `addressing`, and `fix` (its own prompt states). There is no monolithic\n # \"build the whole plan\" state any more — every package is built inside\n # `packages.item`, and the queue drains straight into `build.review`.\n # `addressing` is the implementer's OWN follow-through on review feedback\n # (a coder action) — reached via `$onFeedback`, bound at its own `review:`\n # reference below, not nested inside the (planner-only) `humanReview`\n # machine. `humanReview` is nested HERE (not at the root) so the review\n # tail's run stays inside the builder's own scope — see the header comment.\n buildTail:\n model: <%= it.vars.coderModel %>\n params: [onDone]\n # Nothing resolves this machine BARE (every reference into it names a\n # specific descendant, e.g. `build.review` / `build.fix`) — but a machine\n # must still declare an `entry:` local per its own grammar, so this names\n # an arbitrary one of its own states.\n entry: fix\n states:\n squashing:\n actor: agent\n label: Squashing\n file: <%= it.vars.commitMsgFile %>\n prompt: |\n You are an autonomous coding agent. The process is approved and done. This\n workflow steers itself through its own state files — treat them as its\n private scratchpad, never as project code or documentation. The only\n state file this turn touches is `<%= it.vars.commitMsgFile %>`; write\n it. Do not create other files to hold notes or output.\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. This prompt carries no diff —\n if you built that range yourself earlier in this conversation, work from\n what you already know; otherwise read the range yourself. It runs from\n `<%= it.retainedBase %>` to the working tree, and is exactly what that\n commit will contain, the changes THIS process made (its trace/retry\n boundary onward). For a normal build\n process that is the whole feature; for a `gtd --entry review-gate.check`\n process it is only the fixes made during the review — NOT the changeset\n that was under review.\n\n If that range is non-empty, write `<%= it.vars.commitMsgFile %>` with ONE\n conventional-commits message for the changes (subject line\n `type(scope): subject`, imperative mood, <= 72 characters; a body\n explaining the why, trade-offs, and key decisions). Plain text, no\n markdown wrapper.\n\n If that range is empty (a clean review sign-off with no fixes), write\n `<%= it.vars.commitMsgFile %>` with exactly this one line and nothing\n else:\n\n chore: human review\n on:\n \"A <%= it.vars.commitMsgFile %>\": $onDone\n \"M <%= it.vars.commitMsgFile %>\": $onDone\n\n addressing:\n actor: agent\n label: Addressing your feedback\n file: <%= it.vars.reviewFeedbackFile %>\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\n # feedback-progress guard (`src/StepGuards.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. This\n workflow steers itself through its own state files — treat them as\n its private scratchpad, never as project code or documentation. The\n only state file this turn touches is\n `<%= it.vars.reviewFeedbackFile %>`; address it, then delete it. Do\n not create other files to hold notes or output.\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 \"* **\": health.check\n\n fix:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.feedbackFile %>`; fix it per its\n contents, or delete it if the feedback turns out to be wrong. Do not\n create other files to hold notes or output.\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 retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n # Shared health pair: build.health.check/.escalate -> build.review.reviewing\n # (green) or build.fix (red).\n health:\n machine: healthGate\n with:\n onGreen: review\n onRed: fix\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**build.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # The shared review tail, nested HERE (not at the root) so its run stays\n # inside the builder's own scope across the feedback round-trip — see\n # the header comment. Sign-off -> squashing; feedback -> addressing (the\n # implementer's own coder-identity follow-through, not a reviewer action).\n review:\n machine: humanReview\n with:\n onSignoff: squashing\n onFeedback: addressing\n\n # The root machine — the whole one-flow tree described in this file's\n # header comment. WIRING ONLY: no identity of its own, just the shared\n # entry/exit states and the top-level machine tree.\n unified:\n entry: idle\n states:\n # ── Shared entry ────────────────────────────────────────────────────────\n idle:\n actor: human\n label: Idle\n file: <%= it.vars.todoFile %>\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 process.\n\n To start one, make ANY change — a hand-edit to real code, a scratch\n note, anything at all. <%= it.vars.todoFile %> is a good default\n place to start sketching. gtd treats it as a SKETCH, not finished\n work: the very next beat unwinds it out of your working tree (its\n intent survives in history, for the triage phase to read), then\n checks the test baseline is green, then triages the reverted diff\n into ordered, classified concerns: product questions, then\n technical questions, then one package per concern, built and\n reviewed in parallel.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: unwind\n action: Start\n describe: >-\n make any change — gtd unwinds it out of your working tree (its\n intent survives in history) before checking the test baseline is\n green, then triages the reverted diff into ordered, classified\n concerns and resolves product questions, then technical ones,\n before building each concern's package (**unwind** ->\n **start-gate.check**).\n\n # ── Green-baseline gate: `gtd --entry fix-precheck` entry ─────────────────\n # `gtd --entry fix-precheck` enters HERE: run the suite, then a red run\n # drops straight into the shared `build.fix` loop (-> build.health.check ->\n # review + squash tail); a green run is a no-op back to `idle` (nothing\n # to fix). Aliases the `&suiteCheck` anchor defined on `entryGate.check`'s\n # own `script:` above — the same plain suite check `start-gate`/\n # `review-gate` run.\n fix-precheck:\n actor: check\n label: Checking the baseline\n entry: true\n script: *suiteCheck\n on:\n \"A <%= it.vars.feedbackFile %>\": build.fix\n \"M <%= it.vars.feedbackFile %>\": build.fix\n \"D <%= it.vars.feedbackFile %>\": idle\n \"C\": idle\n\n # ── Shared tail: the finale ─────────────────────────────────────────────\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\n # ── Unwind: revert the entry commit's diff before planning starts ──────\n # Kept a SEPARATE state rather than folded into `start-gate.check`'s own\n # script: the `blocked -> check` retry edge re-runs the gate, so a\n # revert living there would discard the human's own baseline repair and\n # would need an idempotence guard. With exactly one inbound edge (from\n # `idle`), `unwind` is structurally reached once per process and needs\n # no guard at all. Names the reverted commit via `it.currentCommit`\n # (they're the same commit here), NOT a bare `HEAD` — pinning the\n # render-time hash keeps the script correct even if a driver runs it\n # late.\n unwind:\n actor: check\n label: Unwinding your input\n script: |\n #!/usr/bin/env bash\n # gtd check turn — revert the entry commit's diff out of the working\n # tree. Its content survives in history for design.triage to read.\n set +e\n # Hoist the commit hash once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last\n # token on a line.\n commit=\"<%~ it.currentCommit %>\"\n git revert --no-commit \"$commit\"\n on:\n \"* **\": start-gate\n\n # ── Green-baseline gate: the single start entry. ───────────────────────\n # Runs on the tree AFTER `unwind`, so this is now just the ordinary\n # green-baseline check `review-gate`/`fix-precheck` also run — no\n # baseline-aware second run, no detached checkout, no HEAD move\n # anywhere in gtd's emitted scripts any more.\n start-gate:\n machine: entryGate\n with:\n onGreen: design\n reviewBase: \"\"\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 land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**start-gate.check**). To repair the baseline as its own separate\n reviewed commit instead, abandon this start and run\n `gtd --entry fix-precheck` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── Green-baseline gate: `gtd --entry review-gate.check` entry -> build.review.reviewing. ─\n review-gate:\n machine: entryGate\n with:\n onGreen: build.review\n reviewBase: <%= it.vars.reviewBase %>\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 land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**review-gate.check**).\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── The triage phase (design.triage -> design.gate.check/.answer -> architecture). ──\n design:\n machine: designPlan\n with:\n onArchitecture: architecture\n\n # ── The technical phase (architecture.author -> architecture.gate.check/.answer ─\n # -> architecture.decompose -> packages). ──────────────────────────────\n architecture:\n machine: archPlan\n with:\n onPackages: packages\n\n # ── The per-package build queue (packages.* -> build.review). ─────────────\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n # ── The process's build identity (build.* -> build.review -> done). ───────\n build:\n machine: buildTail\n with:\n onDone: done\n";
192292
+ 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 the README's \"How it works\" and \"Driving the loop\" sections for\n# 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 raw 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 MACHINES (entry:/machines:) ───────────────────────────────\n#\n# This template is written as a tree of reusable, parameterized MACHINES (see\n# src/Machines.ts) — a gate/loop written once and instantiated several times\n# with different `with:` bindings (dedup), or a complex cluster grouped under\n# one name for source comprehension (encapsulation). Every reference below is\n# an ordinary `{ machine: <name>, with: { ... } }` local, expanded at\n# compile-time into concrete, qualified states before the pure engine ever\n# sees the definition — no runtime footprint.\n#\n# MACHINE BOUNDARIES LINE UP WITH CONVERSATIONAL IDENTITY: a machine that holds\n# an identity (a planner or a coder persona) declares its own `model:` ONCE, at\n# the machine level (stamped onto every one of its own `prompt` states — see\n# src/Machines.ts's `resolveInstanceModel`), instead of repeating `model:` on\n# every state; no state anywhere declares `model:` or `memory:` directly any\n# more (the memory key is computed from the machine tree — see\n# src/PatternMachine.ts's `memoryScopeAt` / src/Edge.ts's `memoryKeyFor`).\n#\n# * entryGate (dedup ×2) — the green-baseline gate (check + human blocked)\n# guarding the single start (`start-gate`) and\n# the review (`review-gate`) entries. Its check\n# script IS the shared `&suiteCheck` anchor\n# (also aliased by `fix-precheck`, outside this\n# machine) — every entry blocks on any red tree,\n# full stop; there is no baseline-aware second\n# run any more, since `unwind` (below) already\n# put the tree back to the baseline before this\n# gate ever runs. Identity-free.\n# * healthGate (dedup ×2) — the check/escalate health pair shared by the\n# build tail's own tail (`build.health.*`) and\n# the per-package build's own tail\n# (`packages.item.health.*`) — each caller's OWN\n# `fix` state (a coder-identity action) lives\n# beside this machine, not inside it, since\n# fixing IS a coder action and this machine has\n# no identity of its own. Identity-free.\n# * questionGate (dedup ×2) — the check/answer pair shared by the design and\n# architecture phases' own open-questions gate\n# (`design.gate.*` / `architecture.gate.*`): a\n# shared probe script decides whether the\n# phase's steering file still has unanswered\n# questions, and only then rests the process at\n# a human answer gate. Identity-free.\n# * designPlan (encap) — the triage phase's one conversation: group the\n# diff that started the process into ordered,\n# classified concerns and resolve the PRODUCT\n# ones' open questions (`design.*`: triage,\n# gate.check, gate.answer). ▸ planner.\n# * archPlan (encap) — the technical phase's one conversation: a COLD\n# read of the settled concerns, resolving the\n# TECHNICAL open questions, then a mechanical\n# decomposition into package files\n# (`architecture.*`: author, gate.check,\n# gate.answer, decompose). A separate machine\n# from `designPlan` (not one fused machine) —\n# its own memory scope, since there may be a\n# handover between the user-facing and the\n# technical phase. ▸ planner.\n# * humanReview (encap) — the shared review tail, nested INSIDE\n# `buildTail` (`build.review.*`: reviewing →\n# await-review → deciding → collecting) — see\n# \"Why humanReview is nested, not a root sibling\"\n# below. ▸ planner — `collecting` only judges\n# whether the round is actionable; there is no\n# coder-identity follow-through inside this\n# machine any more (see the root's own\n# `re-unwind` state).\n# * specReview (encap) — the per-package agentic review, a SINGLE\n# state (`packages.item.spec.review`) — a\n# machine whose entire job is to be a separate\n# mind from the implementer. ▸ planner — fixing\n# the reviewer's own findings is a coder action,\n# so it lives in `packages.item.fix-spec`\n# instead.\n# * packageLoop (encap) — the per-package build queue\n# (`packages.picking`). Identity-free — the\n# actual per-package build identity lives in its\n# own `item` child (packageItem, below).\n# * packageItem (encap) — the per-package build identity (`packages.\n# item.*`: building, fix-suite, fix-spec,\n# closing), plus its own `health` and `spec`\n# children. ▸ coder.\n# * buildTail (encap) — the process's build identity (`build.*`:\n# squashing, fix), plus its own `health` child\n# AND its own `review` child\n# (humanReview, above). No monolithic \"build the\n# whole plan\" state any more — every package is\n# built inside `packages.item`, and the queue\n# drains straight into `build.review`. ▸ coder.\n#\n# ── Why humanReview is nested, not a root sibling ───────────────────────────\n#\n# `humanReview` sits UNDER `buildTail` (`build.review`) rather than beside it\n# at the root, because a machine's memory scope is its dotted instance path\n# (see src/PatternMachine.ts's `memoryScopeAt`): entering a DESCENDANT scope\n# never breaks the parent's unbroken run, but entering a sibling does. On the\n# ORDINARY path (packages -> build.review -> squashing) that buys nothing —\n# `build`'s own scope has no prior turn to resume, since every package is\n# built inside `packages.item` instead. The one concrete case where it\n# matters is `gtd --entry fix-precheck`: a red suite drops into `build.fix`,\n# which opens `build`'s own scope, and the whole run from there —\n# `build.fix` -> `build.health.check` -> `build.review.*` -> `build.squashing`\n# — stays inside that one subtree, so `build.squashing` drafts the finale\n# from the SAME session that made the fixes, instead of a root-level sibling\n# forcing it to re-derive the range from scratch on every pass through the\n# tail. That one benefit is worth keeping over hoisting `humanReview` back to\n# a root sibling, which would otherwise be a repo-wide rename with no\n# behavioural gain. (The OTHER coder turn a feedback round used to resume —\n# the implementer's own follow-through on review feedback — is retired\n# outright: a hand-edit made during review is now re-planned from scratch via\n# the root's own `re-unwind` state, never built upon, so there is no\n# continuity to preserve on that path any more.)\n# The reviewer itself is unaffected either way: `build.review.*` is its own\n# scope regardless of where it's nested, so it never resumes the builder's\n# session.\n#\n# ── One flow: triage into concerns, then packages, then the shared tail ────\n#\n# ANY change to the tree starts the process — a hand-edit to real code, a\n# scratch note, anything at all — because it's a SKETCH, not finished work:\n# there is no fork on which steering file the human happened to create;\n# `idle` has exactly one outgoing edge, into `unwind`, which reverts that\n# whole diff back out of the working tree before planning starts (its intent\n# survives in history for `design.triage` to read; the squash finale means it\n# never reaches the final commit either way). `unwind` itself has exactly one\n# outgoing edge, into `start-gate`.\n#\n# EVERY entry gates on a green baseline before proceeding, and all three now\n# keep the exact same plain rule: block on any red tree, full stop. A red run\n# at any of them halts at a human blocked gate (fix the failures and re-run,\n# exactly like the escalate states). `start-gate` needs no baseline-aware\n# second run any more — by the time it runs, `unwind` has already put the\n# tree back to the baseline, so ONE suite run there IS the baseline verdict.\n#\n# Once green, `design.triage` reads the diff that started the process (never\n# rendered into the prompt — it names the start commit and reads the range up\n# to the unwind commit itself) and groups it into an ORDERED list of\n# CONCERNS, each one leaving the suite green on its own, classifying each as\n# PRODUCT or TECHNICAL. It raises open questions for the product ones only,\n# and folds EVERYTHING the start diff added into the concerns — a scratch\n# sketch and a real hand-edit alike, since `unwind` already emptied the tree\n# of both; nothing is left to delete. `design.gate` is a shared check+answer\n# pair: its check probes `requirementsFile` for unanswered questions and only\n# rests the process at a human answer gate when some remain — a\n# question-free phase skips the human stop entirely.\n#\n# Once the product questions are settled, `architecture.author` picks up as a\n# COLD reader (a separate machine, a separate memory scope — it reads\n# `requirementsFile` in full rather than resuming design's conversation),\n# develops the *how* for each settled concern, and raises only TECHNICAL open\n# questions — routed through the same shared `questionGate` shape\n# (`architecture.gate`) over `architectureFile`. Once those are settled,\n# `architecture.decompose` is a purely mechanical write-out: one package file\n# per concern, in the settled order — no merge/split judgement, since that\n# already happened in triage's grouping.\n#\n# From there the per-package build queue (`packages.*`) and the shared review\n# + squash tail (`build.*`) are UNCHANGED from before this restructure — see\n# their own machine doc comments below for the per-package build/review/\n# health mechanics.\n#\n# A second entry, `gtd --entry review-gate.check`, jumps there directly and,\n# once green, to `build.review.reviewing` to review a foreign branch through\n# the same shared tail. A third, `gtd --entry fix-precheck`, jumps there: a\n# red suite drops straight into the shared `build.fix` loop and out through the\n# review + squash tail; a green suite is a no-op back to `idle`. Both states\n# declare `entry: true` — otherwise unreachable from `entries.default`.\n#\n# All entries converge at `build.review.reviewing` -> `build.review.await-review`.\n# Ticking a REVIEW.md box means only \"I reviewed this hunk\"; a COMMENT (not an\n# unticked box) is what asks for changes. Every human step at\n# `build.review.await-review` goes to `build.review.deciding`, which CAPTURES\n# the raw material (never interprets it) and routes on the step's shape —\n# THREE edges out of the review tail, not one:\n# * the SIGN-OFF edge: no comment at all (every box ticked, no REVIEW.md\n# note, no code edit) -> the diff is a bare REVIEW.md deletion ->\n# build.squashing -> done (the whole process collapses into one commit).\n# * the NON-ACTIONABLE edge: some comment exists, but `build.review.collecting`\n# judges it non-actionable (the human left only an approving remark, e.g. a\n# compliment with no code edit and no substantive note) -> CONSUMES the raw\n# capture (deletes it, the only change this turn) -> straight to\n# build.squashing too, WITHOUT spending a design lap producing nothing, and\n# with nothing left over to leak into the finale. The outcome is encoded by\n# which paths `collecting`'s own diff touches, never by a sentinel a file\n# pattern can't tell apart from real content — and never by a CLEAN turn\n# either, which stays the ordinary fruitless-dispatch attempt (no `C` row\n# here), not a verdict.\n# * the ACTIONABLE edge: `collecting` judges the round actionable (a note in\n# REVIEW.md, or a hand-edit to code) -> marks the raw capture (a real\n# edit) -> the root's own `re-unwind` state reverts the human's\n# review-round commit out of the working tree, scoped to real code (never\n# the review file, already gone, and never `stateDir`) -> `design.triage`\n# re-derives the work from that history as intent only — the human's\n# hand-edit is a SKETCH to be re-planned, never built upon. This is a full\n# development lap (triage -> gate -> architecture -> gate -> packages ->\n# build.review again), not a quick fix-and-re-review; there is no\n# baseline check on the way back in (`re-unwind` is not an entry, so\n# `entryGate` never runs on this path).\n# Two dead ends never commit: the review-signoff guard (src/StepGuards.ts)\n# REFUSES a step that leaves a box unticked with no comment (finish reviewing\n# first) and a deleted REVIEW.md (restore it and tick the boxes).\n#\n# The shared health pair (`build.health.check`/`.escalate`, plus its own\n# `build.fix`) no longer serves a feedback loop at all — with the implementer's\n# own coder-identity follow-through retired, the only edge into it is\n# `gtd --entry fix-precheck`'s red branch; they are not dead code, just\n# reachable from one place instead of two. The\n# per-package loop keeps its OWN health pair (`packages.item.health.check`/\n# `.escalate`, plus `packages.item.fix-suite`) because its green edge targets\n# the per-package `packages.item.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 the README's \"Variables\" section).\n\n# The definition-level declaration `stateDirOf` (src/PatternMachine.ts) reads —\n# a DIFFERENT thing from the `vars.stateDir` below despite sharing a name\n# (see that var's own comment). Rendered from it here so the two stay in\n# lockstep: relocating the var relocates what the engine treats as plumbing.\nstateDir: <%= it.vars.stateDir %>\n\nvars:\n testCommand: npm test\n # Where gtd keeps its own scratch/bookkeeping (the check scripts' temp output\n # file, review-deciding's state-file exclusion). Independent of the per-file\n # vars below — relocating a file wholesale means setting both. An ordinary\n # var — the knob a user overrides — distinct from the top-level `stateDir:`\n # declaration above, which renders its value from this one; that is why the\n # two share a name.\n stateDir: .gtd\n # The only per-file var gtd never writes and never reads — the human's own\n # sketch pad, named so the one \"open steering file\" keybinding has an\n # answer at `idle` too.\n todoFile: .gtd/TODO.md\n requirementsFile: .gtd/REQUIREMENTS.md\n architectureFile: .gtd/ARCHITECTURE.md\n # Stamped (never authored, never read for its content) by design.gate.check\n # / architecture.gate.check — its mere presence is the routing signal that\n # the current phase's steering file still has unanswered questions.\n questionsFile: .gtd/QUESTIONS.md\n packagesDir: .gtd/packages\n nextFile: .gtd/NEXT.md\n # Written by packages.item.building when a package's acceptance criteria are\n # already met (an earlier package's fix turn pulled the work in); read by\n # the spec reviewer as the evidence a clean turn there is a real approval,\n # not a lazy/crashed one; removed by packages.item.closing.\n satisfiedFile: .gtd/SATISFIED.md\n reviewFile: .gtd/REVIEW.md\n reviewRawFile: .gtd/REVIEW_RAW.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_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 triage/design/review turns;\n # `coderModel` (default \"base\") is the tier for the build/fix turns.\n plannerModel: smart\n coderModel: base\n # The fixed diff base for `gtd --entry review-gate.check --var\n # reviewBase=<commitish>` — a plain non-gtd branch reviewed against that\n # commitish rather than the process start. Declared here (rather than left\n # undeclared) so `--var reviewBase=...` passes the CLI's declared-name\n # check; the default empty string renders blank, which the entry command\n # refuses (a review entry always requires an explicit base).\n reviewBase: \"\"\n # gtd's own voice for the files it generates — a specialisation of the\n # \"Spartan\" output style from alexgreensh/attention-span\n # (https://github.com/alexgreensh/attention-span), licensed AGPL-3.0,\n # written from a reading of that project's version 0.6. Rewritten in gtd's\n # own words for deliverables rather than chat replies; no upstream text\n # ships. A point-in-time derivation with no refresh mechanism — it will go\n # stale and nothing will notice.\n #\n # Keep/adapt/drop ledger — seventeen items reconciled: eight upstream rules\n # carried, five added, three dropped, rationale kept.\n #\n # Carried (six rules below cover eight upstream rules — two of them each\n # carry two upstream rules):\n # - the answer-first rule below carries both upstream's answer-first\n # rule and its cut-ruthlessly rule\n # - the never-trim rule below carries both upstream's\n # cut-elaboration-never-a-warning rule and its\n # numbers-and-thresholds-are-exact rule\n # - the deliverable rule carries upstream's answer-versus-deliverable\n # rule, minus its \"can't tell which? treat it as an answer\" tie-break —\n # gtd has no ambiguity to break, every injection site authors a file\n # - the blunt-and-imperative rule, the plain-words rule, and the bold\n # rule map one-to-one to upstream's equivalents\n #\n # Added and sharpened:\n # - five rules with no upstream equivalent: ship the artifact bare,\n # compressing is not dropping, one idea per block, flag risk in one\n # blunt line, never narrate the work\n # - two carried rules sharpened past upstream: the bold rule gains a\n # bold-only-reading test, and the plain-words rule gains a five-word\n # cap on defining an unavoidable term\n #\n # Dropped, each for its own reason:\n # - the arrow-point paragraph format: four of the seven voice-bearing\n # states are machine-parsed and their grammars require literal\n # `- [ ]` rows\n # - the go-deeper escape hatch: a state's own prompt is the request and\n # there is no channel to ask through\n # - the confirm-before-acting rule: every prompt is an instruction and\n # the actor's only output channel is the file\n styleBlock: |-\n This block opens with why density matters, not with rule one: the reader\n has a finite attention budget, and burying the line that mattered fails as\n hard as cutting it. This is motivation, not a rule — it commands nothing\n and never outranks never-trim.\n\n This file is a deliverable, not a chat reply. Its size follows the work —\n write what the task demands, and let padding, not length, be the target of\n any cut.\n\n Lead with the answer or the decision immediately. Skip the throat-clearing\n opener, and never circle back to restate the point once it has landed.\n\n Write in flat, commanding sentences — commit to the claim rather than\n hedging it.\n\n Favor everyday vocabulary over technical shorthand; where a term is\n unavoidable, define it in five words or fewer on first use.\n\n In markdown, bold carries the load: bold the claim, not the sentence around\n it. The test: reading only the bold text yields the full point and every\n risk, or the bolding is wrong.\n\n Above every rule in this block: never trim a risk, a number, a threshold,\n or a scoped condition to save space. A claim that only holds under some\n condition ships with that condition. It outranks every other rule here.\n\n Ship the artifact bare. Output only what was asked for — no lead-in, no\n \"here's the…\", no sign-off wrapped around it. A generated file consumed\n as-is turns a wrapper into a defect.\n\n Compressing is not dropping. A task with three load-bearing parts ships all\n three, each shorter — reporting two of three findings has failed the brief,\n not fulfilled it.\n\n One idea per block; break when the idea shifts. Blocks may run any length,\n but one unbroken paragraph is a defect even when short.\n\n Flag risk and uncertainty in one blunt line. This is never-trim's\n constructive twin: that rule forbids deleting a caveat, this one forbids\n dissolving it into hedged prose.\n\n Never narrate the work — do it. Skip lines like \"I will now list the\n concerns\"; a file has no reason to describe its own composition.\n styleFormatContract: |-\n This file is machine-read. Its format contract outranks every style rule\n above it: keep every `##`/`###` heading this state specifies, every\n `- [ ]` checkbox row, and every required marker line exactly as specified.\n\n Do not replace a checkbox list with prose paragraphs. Do not renumber or\n rename a required heading. Do not drop a marker line. A parser reads these\n literally, and a violation refuses the turn outright — this is not a\n polite request, it is the one thing standing between this output and a\n failed parse.\n\n The voice rules above still govern the prose that sits between these\n structural elements. They do not govern the elements themselves.\n\nentry:\n default: unified\n\nmachines:\n # ── DEDUP: the green-baseline entry gate (identity-free) ──────────────────\n # Runs the suite before any work starts — both `start-gate` and\n # `review-gate` reach this one check, and its script IS the shared\n # `&suiteCheck` anchor (defined once, right here, and aliased by\n # `fix-precheck` below); the `on` rules turn the result into \"halt at\n # blocked\" vs \"proceed to $onGreen\". Kept SEPARATE from `healthGate` (below)\n # even though both are a check+something-else shape: this gate runs BEFORE\n # planning, so it must NOT sweep any steering file `healthGate` does.\n entryGate:\n params: [onGreen, blockedMessage, blockedDescribe, checkLabel, blockedLabel, reviewBase]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: &suiteCheck |\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 \"<%~ it.stateDir %>\"\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 mkdir -p \"$(dirname \"$feedback\")\"\n <%~ it.vars.testCommand %> > <%~ it.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.stateDir %>/.check-output ]; then\n mv <%~ it.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.stateDir %>/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n # A repeat identical failure would otherwise leave FEEDBACK.md\n # byte-identical to its committed copy — no diff, so the check would\n # look GREEN. Stamp each red run with the current HEAD (which always\n # advances between checks) so a still-red re-run always re-registers\n # as an M/A edit and takes the red edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$feedback\"\n else\n rm -f <%~ it.stateDir %>/.check-output\n rm -f \"$feedback\"\n fi\n # `entry: true` on the shared local marks both `entryGate` instances\n # (start-gate/review-gate) as manual entries — only `review-gate.check`\n # actually needs the reachability root (it is otherwise unreachable\n # from `entries.default`), but the shared dedup makes marking one mark\n # both. Accepted as correct: both ARE the workflow's entry gates.\n entry: true\n # `reviewBase: $reviewBase` — only `review-gate` binds a real\n # template (`<%= it.vars.reviewBase %>`), fixing the whole process's\n # diff base to whatever `gtd --entry review-gate.check --var\n # reviewBase=<commitish>` supplies. `start-gate` binds the literal\n # empty string, which compiles away to \"no base\" here — it is reached\n # from `idle`, never manually entered with a base.\n reviewBase: $reviewBase\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 action: Retry check\n describe: $blockedDescribe\n\n # ── DEDUP: the check/escalate health pair (identity-free) ─────────────────\n # Runs the suite; a red run records the configured feedbackFile and routes to\n # the caller's OWN fixer (`$onRed` — a coder-identity action that lives\n # beside this machine, not inside it, since fixing IS a coder action and this\n # machine has no identity); a green run cleans it up and routes to\n # `$onGreen`. Escalation redirects here after the caller's fixer has retried\n # `max` times without success. Instantiated for the build tail's own tail\n # (`build.health.*`, caller's fixer is `build.fix`) and the per-package\n # build's own tail (`packages.item.health.*`, caller's fixer is\n # `packages.item.fix-suite`).\n healthGate:\n params: [onGreen, onRed, escalateDescribe, checkLabel, escalateLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # This script ALSO sweeps the spent raw review capture\n # (`reviewRawFile`) — a mechanic `entryGate.check` must NOT run, since\n # it executes before planning, while a fresh sketch legitimately still\n # exists.\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 \"<%~ it.stateDir %>\"\n # Hoist every 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\n # 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 mkdir -p \"$(dirname \"$feedback\")\"\n # Sweep the raw review capture — no path from `deciding`/`collecting`\n # reaches this gate any more (its only inbound edge is\n # `gtd --entry fix-precheck`'s red branch); this is a leftover\n # sweeper for a capture an earlier, abandoned process may have left\n # committed on that path. A no-op when it is already gone or never\n # existed.\n reviewRaw=\"<%~ it.vars.reviewRawFile %>\"\n rm -f \"$reviewRaw\"\n <%~ it.vars.testCommand %> > <%~ it.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.stateDir %>/.check-output ]; then\n mv <%~ it.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.stateDir %>/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n # A repeat identical failure would otherwise leave FEEDBACK.md\n # byte-identical to its committed copy — no diff, so the check would\n # look GREEN. Stamp each red run with the current HEAD (which always\n # advances between checks) so a still-red re-run always re-registers\n # as an M/A edit and takes the red edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$feedback\"\n else\n rm -f <%~ it.stateDir %>/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A <%= it.vars.feedbackFile %>\": $onRed\n \"M <%= it.vars.feedbackFile %>\": $onRed\n \"* **\": $onGreen\n \"C\": $onGreen\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 land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $escalateDescribe\n\n # ── DEDUP: the open-questions check/answer pair (identity-free) ───────────\n # Shared by design and architecture's own gate (`design.gate`/\n # `architecture.gate`): `check`'s script probes whichever of\n # requirementsFile/architectureFile currently exists (see the script's own\n # comment) with `gtd check qa --open-questions` and stamps/removes\n # `questionsFile` on the result; only when it's present does the process\n # rest at `answer`, a human gate. A question-free phase never rests here at\n # all — `check`'s own `on` rows skip straight to `$onNone`. Takes NO `file:`\n # PARAM for the probe script itself: machine `$param` substitution is\n # whole-value only, so a path cannot be spliced into a shared script body —\n # `file` is instead the PARAM `answer` renders as its own `file:` (the\n # human-facing gate), a different use entirely from the probe.\n questionGate:\n params: [file, message, onNone, onRevise, checkLabel, answerLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: |\n #!/usr/bin/env bash\n # gtd check turn — probe the current phase's steering file for\n # unanswered questions; an answered/empty result removes the marker\n # (questionsFile), an unanswered one stamps it fresh. Design and\n # architecture share this one probe: whichever of\n # requirementsFile/architectureFile exists ON DISK right now IS this\n # phase's steering file — unambiguous because architecture.author\n # deletes requirementsFile in the same turn it writes\n # architectureFile, so exactly one of the two exists here.\n set +e\n mkdir -p \"<%~ it.stateDir %>\"\n # Hoist every 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\n # 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 questions=\"<%~ it.vars.questionsFile %>\"\n mkdir -p \"$(dirname \"$questions\")\"\n requirements=\"<%~ it.vars.requirementsFile %>\"\n architecture=\"<%~ it.vars.architectureFile %>\"\n file=\"$requirements\"\n [ -f \"$file\" ] || file=\"$architecture\"\n gtd check qa \"$file\" --open-questions > /dev/null 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n printf 'open questions remain in %s\\n' \"$file\" > \"$questions\"\n # A repeat identical result would otherwise leave the marker\n # byte-identical to its committed copy — no diff, so the check\n # would look answered. Stamp each round with the current HEAD\n # (which always advances between checks) so a still-open re-run\n # always re-registers as an M/A edit and takes the answer edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$questions\"\n else\n rm -f \"$questions\"\n fi\n on:\n \"A <%= it.vars.questionsFile %>\": answer\n \"M <%= it.vars.questionsFile %>\": answer\n \"D <%= it.vars.questionsFile %>\": $onNone\n \"C\": $onNone\n answer:\n actor: human\n label: $answerLabel\n file: $file\n mode: qa\n answerGate: true\n message: $message\n on:\n \"* **\":\n to: $onRevise\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace\n `_your answer_` for your own) to send it back for the agent to\n fold your answers in. Delete a question to skip it, or delete\n the whole `## Open Questions` section to accept the plan as-is.\n\n # ── ENCAPSULATION: the triage phase's one conversation ─────────────────────\n # Groups the diff that started the process into ordered, classified\n # concerns and resolves the PRODUCT ones' open questions — one machine\n # identity across the whole back-and-forth, so the agent explores the\n # codebase once and the human's answer rationale survives every return lap.\n # `triage`'s prompt reads THREE laps off the requirements file's own\n # content, never off which edge landed here (the resting state is the same\n # either way): FIRST (the file is absent), RETURN (a ticked/answered\n # question waiting to be folded), and REVIEW LOOP-BACK (concerns already\n # present, nothing ticked-but-unfolded — `re-unwind` put them there, not an\n # answer). The loop-back lap also runs the suite itself and files any\n # breakage it finds as its own FIRST concern: `re-unwind` sits between\n # `build.review.deciding` and here with no `entryGate` in between, so a red\n # the revert causes is otherwise ambient. Left for\n # `packages.item.health.check` to trip over instead, it would spend one of\n # `packages.item.fix-suite`'s retry cap — process-pooled across every\n # package, the same pattern `specReview.review`'s own retry comment\n # documents — on breakage the package itself never caused.\n # ▸ planner identity — `model:` declared once here, stamped onto `triage`\n # (its only prompt state; `gate.answer` is a human turn and carries no\n # prompt).\n designPlan:\n model: <%= it.vars.plannerModel %>\n params: [onArchitecture]\n entry: triage\n states:\n triage:\n actor: agent\n label: Triaging the change\n file: <%= it.vars.requirementsFile %>\n mode: qa\n # An agent that deletes the assembled review input without folding it\n # in is the original \"captured then discarded\" bug, now against THIS\n # state's own file — the retired `build.addressing` state was the\n # guard's only user before this restructure; this is its new one.\n requireProgress: true\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.requirementsFile %>`; develop it. Do\n not create other files to hold notes or output.\n\n `<%= it.vars.todoFile %>` is the likely home of the sketch that\n started this process. Despite living beside this workflow's own\n state files, it is the HUMAN'S INPUT — the sketch to be folded into\n the concerns below like any other part of the start diff. It is\n never a state file to preserve, and never gtd bookkeeping to ignore.\n\n This process started at commit `<%= it.startCommit %>`. The change\n that started it was reverted out of the working tree by the\n `unwind` beat right after it landed, so `git diff <%= it.startCommit %>`\n (against the working tree) is now empty — its content survives only\n in history. Find the entry commit yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\n (the process's very first turn, landed immediately after\n `<%= it.startCommit %>`, before any later baseline-repair commits) —\n then `git show` that commit to see everything that started this\n process: a hand-edit to real code, a scratch note in some file, or\n both.\n\n On the FIRST lap, `<%= it.vars.requirementsFile %>` does not exist\n yet (or holds whatever the human's own change happened to leave\n there). Build it from that start diff:\n\n 1. Group the diff into an ORDERED list of CONCERNS — each one a\n coherent piece of work that can leave the test suite GREEN on\n its own, in the order you'd want them built. If a concern would\n red-light tests that a later concern is meant to fix, that is\n not a valid split: merge the two into one concern.\n 2. Classify each concern as PRODUCT (a user-facing/requirements\n decision) or TECHNICAL (an implementation decision).\n 3. For every PRODUCT concern with an open point you cannot settle\n yourself, add a question under a `## Open Questions` heading near\n the top: one `### <question>` sub-heading each, followed by a\n checkbox list of candidate answers — aim for TWO concrete options\n 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 Raise questions ONLY for product concerns here — a technical\n concern's open points wait for the next phase. Never tick a box\n yourself — the human ticks exactly one per question.\n 4. Fold EVERYTHING the entry commit added into the concerns above —\n a scratch note and a real code edit are both just a SKETCH of\n the intent, a spec to be finished, never work to be preserved;\n `unwind` already reverted both out of the tree, so there is\n nothing left to delete. Describe each piece under the concern it\n belongs to.\n\n On a RETURN lap the human has ticked one option per question (or\n replaced `_your answer_` with their own text) in\n `<%= it.vars.requirementsFile %>`. For each answered question: fold\n the chosen answer into its concern's prose, then MOVE the question\n down into a `## Answered Questions` section as `### <question>`\n followed by the resolved answer in plain PROSE (drop the\n checkboxes). Never re-raise a question the human deleted; if the\n human deleted the whole `## Open Questions` section, treat every\n product concern as accepted and surface no new questions unless\n something is genuinely still blocking. Omit `## Open Questions`\n entirely once there are none left.\n\n On a REVIEW LOOP-BACK lap, `<%= it.vars.requirementsFile %>`\n already holds concerns and none of them have a ticked-but-unfolded\n answer waiting — that shape means a completed REVIEW ROUND put\n this material here, not a question you asked. Develop those\n existing concerns further with whatever the round raised; never\n rediscover or regroup them from a cold read the way the FIRST lap\n does.\n\n The human's own edit from that review round has been reverted out\n of the working tree the same way the entry commit is — read it\n from history, never the working tree: run\n `git show <%= it.reviewBase %>` to see it. (On a FIRST lap that\n same hash IS the process's own diff base, `<%= it.startCommit %>`,\n so this branch does not apply: `<%= it.vars.requirementsFile %>`\n does not exist yet for a review round to have happened against.)\n\n Whatever this round touches, every decision already settled under\n `## Answered Questions` stays settled on this lap too — never\n re-open one. A genuinely open PRODUCT point in the round's own\n material may still raise a fresh question under\n `## Open Questions`: the product gate sits on this path exactly as\n it does on a first lap, and will ask it.\n\n Before grouping anything on this lap, run\n `<%= it.vars.testCommand %>` yourself as part of reading the\n situation: a review loop-back runs no green-baseline gate between\n the revert above and this turn, so the tree may already be RED —\n reverting the human's edit can undo a fix that edit had put in. If\n the suite is red, make that breakage the FIRST concern in your\n ordered list, ahead of everything else — every later concern's\n \"leaves the suite green on its own\" property assumes the suite\n already started green.\n\n Leave `<%= it.vars.requirementsFile %>` uncommitted and finish your\n turn.\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: <%= it.vars.requirementsFile %>\n onNone: $onArchitecture\n onRevise: triage\n checkLabel: Checking for open questions\n answerLabel: Awaiting your product answers\n message: |\n `<%= it.vars.requirementsFile %>` holds the concerns under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n # ── ENCAPSULATION: the technical phase's one conversation ──────────────────\n # A separate machine from `designPlan` — its own memory scope, since there\n # may be a handover between the user-facing and the technical phase: this\n # phase does NOT resume design's conversation, it reads\n # `<%= it.vars.requirementsFile %>` cold. Resolves the TECHNICAL open\n # questions, then mechanically decomposes the settled concerns into package\n # files (no merge/split judgement — that already happened in triage's\n # grouping). ▸ planner identity — `model:` declared once here, stamped onto\n # `author` and `decompose` (its own prompt states; `gate.answer` is a human\n # turn and carries no prompt).\n archPlan:\n model: <%= it.vars.plannerModel %>\n params: [onPackages]\n entry: author\n states:\n author:\n actor: agent\n label: Refining the technical plan\n file: <%= it.vars.architectureFile %>\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state files this\n turn touches are `<%= it.vars.architectureFile %>` (write it) and\n `<%= it.vars.requirementsFile %>` (delete it once its content is\n folded in). Do not create other files to hold notes or output.\n\n You do NOT resume the design conversation — this is a separate\n machine with its own memory, a COLD read every time. Read\n `<%= it.vars.requirementsFile %>` (the settled, ordered, classified\n concerns) in full. Treat every product decision recorded there —\n plain prose or a resolved `## Answered Questions` entry — as\n SETTLED; never re-open it.\n\n On the FIRST lap, develop `<%= it.vars.architectureFile %>` from\n those concerns: for each one, in the same order, work out the *how*\n — file/module structure, data models, library/tech-stack choices,\n error-handling strategy — building on the *what* already settled.\n For every open TECHNICAL point you cannot settle yourself, add a\n question under a `## Open Questions` heading exactly as the\n requirements phase does: one `### <question>` each, followed by a\n checkbox list — TWO concrete options plus a final free-text slot,\n 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\n option per question (or replaced `_your answer_` with their own\n text): fold each chosen answer into the plan prose, then MOVE the\n question into a `## Answered Questions` section as `### <question>`\n + the resolved answer in plain PROSE (drop the checkboxes). Never\n re-raise a deleted question; treat a deleted `## Open Questions`\n section as acceptance. Omit `## Open Questions` once there are none\n left.\n\n Once `<%= it.vars.architectureFile %>` is written, delete\n `<%= it.vars.requirementsFile %>` — its content has been folded in\n and it must not linger. Leave everything uncommitted and finish\n your turn.\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: <%= it.vars.architectureFile %>\n onNone: decompose\n onRevise: author\n checkLabel: Checking for open questions\n answerLabel: Awaiting your technical answers\n message: |\n `<%= it.vars.architectureFile %>` holds the technical plan under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n decompose:\n actor: agent\n label: Decomposing into packages\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are an autonomous coding agent. This workflow steers itself through\n its own state files — treat them as its private scratchpad, never as\n project code or documentation. The only state files this turn touches\n are the package files it writes under `<%= it.vars.packagesDir %>/` and\n `<%= it.vars.architectureFile %>`, which it deletes. Do not create other\n files to hold notes or output.\n\n If you wrote `<%= it.vars.architectureFile %>` earlier in this\n conversation, work from what you already settled; otherwise read it\n (the converged technical plan) yourself. It already lists an ORDERED\n set of concerns with every merge/split judgement made — this turn is\n a mechanical write-out, not a planning one.\n\n Write one package file per concern, in the settled order, under\n `<%= it.vars.packagesDir %>/` (e.g.\n `<%= it.vars.packagesDir %>/01-name.md`,\n `<%= it.vars.packagesDir %>/02-name.md`, ...). Each package file\n carries that concern's requirement (so it can be reviewed against\n its spec), its independent tasks, and each task's acceptance\n criteria as `- [ ]` checkboxes and relevant paths. Do NOT merge or\n split concerns here — that judgement already happened upstream;\n carry the settled grouping over verbatim. No package file may\n reference any other `.gtd/` file.\n\n Once the package files are written, delete\n `<%= it.vars.architectureFile %>`. Leave everything uncommitted and\n finish your turn.\n on:\n \"* <%= it.vars.packagesDir %>/**\": $onPackages\n\n # ── ENCAPSULATION: the shared review tail ──────────────────────────────────\n # ▸ planner identity — `model:` declared once here, stamped onto `reviewing`\n # and `collecting` (its only prompt states). `collecting` JUDGES whether a\n # round is actionable and, when it is, CLASSIFIES it straight into\n # `requirementsFile` as ordered, PRODUCT/TECHNICAL concerns — the same\n # artifact `design.triage` develops — never an instruction list, and never a\n # build. An actionable round still leaves this machine entirely (the root's\n # own `re-unwind`, then `design.triage` folds the concerns in cold), so\n # there is no coder-identity follow-through inside this machine any more.\n humanReview:\n model: <%= it.vars.plannerModel %>\n entry: reviewing\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: <%= it.vars.reviewFile %>\n mode: review\n # `gtd --entry review-gate.check` (a plain non-gtd branch, e.g. a\n # colleague's PR) does NOT enter here directly — it enters\n # `review-gate.check` first, which runs the green-baseline gate and,\n # once green, transitions here. The `Gtd-Review-Base:` trailer rides on that\n # entry commit (the process's oldest), so everything below still reuses\n # the same reviewing/await-review/feedback machinery over <commitish>..HEAD.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.reviewFile %>`; write it at exactly\n that path. Do not create other files to hold notes or output.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the changes in\n this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.reviewBase %> -->`\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 This prompt carries no diff — read the changes yourself. The range runs\n from `<%= it.reviewBase %>` to the working tree (committed turns plus\n anything still pending); on a feedback round that base is the previous\n review's boundary, so it covers only what changed since then, not items\n already signed off.\n\n Leave `<%= it.vars.reviewFile %>` uncommitted and finish your turn.\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 process rests here, gtd opens a \"review checkout window\": raw HEAD\n # is rewound to the review base (the most-recent `deciding` commit — so a\n # feedback round shows only `last-review..HEAD` — or the process start on\n # 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 `deciding`, which decides sign-off vs.\n # 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 review-signoff guard (`src/StepGuards.ts`)\n # catches them BEFORE the step commits and refuses: an unfinished\n # review (a box still `- [ ]` with no comment) and a deleted REVIEW.md.\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the process — 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 land`:\n\n - **Sign off** — tick EVERY box and leave no comment — no note in\n `<%= it.vars.reviewFile %>`, no code edit — to collapse the process into\n one commit (**build.squashing**).\n - **Request changes** — leave a comment: a note on a\n `<%= it.vars.reviewFile %>` line, or a direct code edit — to send a FULL\n development lap (**review.deciding** → **review.collecting** →\n re-triage). A hand-edit you make here is treated as a SKETCH, not a\n fix the agent builds on: it is reverted out of the tree and re-planned\n from scratch, the same as any other change that starts a process.\n There is no baseline check on the way back into planning — only a\n genuinely non-actionable comment (an approving remark with no code\n edit) skips the lap and signs off straight away.\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 \"* **\": deciding\n\n 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 process 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 -> deciding` commit; HEAD^ is the `reviewing`\n # commit that wrote the agent's original REVIEW.md (the sign-off gate\n # refuses an incomplete step, so `deciding` is entered exactly once per\n # round — nothing commits in between). It is FEEDBACK when the human left\n # a comment: a note in REVIEW.md (any edit beyond a \"[ ]\"->\"[x]\" tick) OR a\n # hand-edit to any file this round outside the declared stateDir or the\n # state's own reviewFile (which is var-configurable and may live outside\n # stateDir, e.g. a root-level REVIEW.md — issue #128). Otherwise it is a\n # clean sign-off. This pathspec's plumbing-directory exclusion (stateDir,\n # never a literal `.gtd/`) is the same declaration `src/StepGuards.ts`'s\n # `isCodePath`/`hasCodeChange` re-derive for their own code-vs-plumbing\n # test — keep the two together when either changes. Both exclusions use\n # git's `:(exclude,literal)` pathspec magic, matching stateDir/reviewFile\n # as literal paths rather than regex/glob patterns — a bare grep here\n # would let a metacharacter in either value change what gets excluded.\n # This turn only CAPTURES the raw material into REVIEW_RAW.md (the\n # `collecting` agent then judges whether it's actionable); 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 # 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 stateDir=\"<%~ it.stateDir %>\"\n reviewFile=\"<%~ it.vars.reviewFile %>\"\n reviewRawFile=\"<%~ it.vars.reviewRawFile %>\"\n head=$(git rev-parse HEAD)\n mkdir -p \"$(dirname \"$reviewRawFile\")\"\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 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 # Did the human hand-edit anything this round besides stateDir and the\n # reviewFile itself (var-configurable — may live outside stateDir, so it\n # is excluded by exact path, not just the stateDir prefix)?\n if git diff-tree --no-commit-id --name-only -r HEAD -- . \\\n \":(exclude,literal)$stateDir\" \":(exclude,literal)$reviewFile\" \\\n | grep -q . \\\n || [ \"$before\" != \"$after\" ]; then\n {\n echo \"This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.\"\n echo\n echo \"Commit: $head\"\n echo \"The human's notes are in $reviewFile at this commit. Any hand edits are\"\n echo \"in that commit's other paths. Run: git show $head\"\n } > \"$reviewRawFile\"\n fi\n rm -f \"$reviewFile\"\n on:\n \"A <%= it.vars.reviewRawFile %>\": collecting\n \"M <%= it.vars.reviewRawFile %>\": collecting\n \"D <%= it.vars.reviewFile %>\": $onSignoff\n\n # Judges actionability and, when actionable, classifies straight into\n # `requirementsFile` — an ordered, PRODUCT/TECHNICAL concern list, never\n # an instruction list for a builder (the actionable path re-plans from\n # history via `re-unwind` + `design.triage`; it never builds on the\n # human's own edit). The outcome is encoded by which paths ITS OWN diff\n # touches, never by a sentinel: writing `requirementsFile` (`A`/`M`) IS\n # the actionable case — row ORDER matters, since an actionable round's\n # diff is `A requirementsFile` + `D reviewRawFile` together, so the\n # requirements rows must be declared first or every round would\n # short-circuit to sign-off. CONSUMING `reviewRawFile` alone (deleting\n # it, the only change this turn) is the non-actionable short-circuit —\n # the same trick `deciding` already uses, one state earlier; it reuses\n # `$onSignoff` (already bound to `build.squashing`) rather than a new\n # param, because the short-circuit IS a sign-off. Declaring no `C` row\n # and no `* **` row is deliberate: a clean turn here is a fruitless\n # dispatch (a crash, a refusal, a context-limit cutoff), never a\n # verdict, so it falls through to the ordinary `prompt`-state default\n # instead — an empty ATTEMPT commit, remembered as a stall\n # (`Edge.ts`'s `stalledAt`) rather than silently misread as \"the human's\n # feedback was non-actionable\" — and a dirty tree matching neither row\n # is a REFUSAL (you classify, you do not build). `requirementsFile`\n # survives `re-unwind` (it lives under `stateDir`, excluded from its own\n # revert pathspec) for `design.triage` to fold in cold; a consumed\n # `reviewRawFile` with no requirements written is gone before\n # `build.squashing` ever runs, so nothing leaks into the finale.\n collecting:\n actor: agent\n label: Collecting your feedback\n file: <%= it.vars.requirementsFile %>\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent judging and classifying a round\n of review feedback. This workflow steers itself through its own\n state files — treat them as its private scratchpad, never as\n project code or documentation. The only state files this turn\n touches are `<%= it.vars.requirementsFile %>` and\n `<%= it.vars.reviewRawFile %>`, which it deletes — do NOT change any\n other file: you classify, you do not build.\n\n The raw review material is: <%~ it.read(it.vars.reviewRawFile) %>\n\n It names a commit. If you wrote today's review earlier in this\n conversation, work from what you already reviewed; otherwise read that\n commit's diff yourself before classifying anything.\n\n The round is ACTIONABLE if any of the following is true:\n\n 1. The human left a NOTE on `<%= it.vars.reviewFile %>` — a note is\n anything beyond a checkbox flip, regardless of whether the box\n itself is ticked (ticking means only \"I read this hunk\", never\n sign-off). A note is a MANDATORY concern below, whether or not\n its box is ticked.\n 2. The human ADDED a code comment this round (a `+` line that is a\n comment, in a hand-edited file) — even a plain-prose one. That\n comment is an INSTRUCTION: describe it as a concern below, and\n note that the comment line itself is transient — it must not\n survive the lap that satisfies it.\n 3. The human hand-edited any non-comment code this round. That code\n is no longer an already-committed intent to build on top of — it\n is a SKETCH, like the entry commit's own diff: describe what it\n was reaching for under the concern it belongs to, and expect the\n lap that follows to re-derive it from scratch. Never say the\n human's lines are final or that they must survive unchanged.\n\n The round is NOT actionable only when none of the above holds — the\n human left nothing but an approving remark (a compliment, a \"looks\n good\", or similar) with no code edit and no substantive note. Never\n invent actionability to justify a lap, and never dismiss a real note\n or edit as approval.\n\n If the round is ACTIONABLE: write `<%= it.vars.requirementsFile %>`\n with an ORDERED list of CONCERNS, each classified PRODUCT (a\n user-facing/requirements decision) or TECHNICAL (an implementation\n decision) — the same shape `design.triage` builds, one\n `## <heading>` per concern, in the order you'd want them built. Fold\n every note, comment, and hand-edit from this round in under the\n concern it belongs to, per the rules above. Raise NO open\n questions here — write no `## Open Questions` section;\n `design.triage` owns that, one phase later. Then delete\n `<%= it.vars.reviewRawFile %>` and finish your turn.\n\n If the round is NOT actionable: delete\n `<%= it.vars.reviewRawFile %>` and finish your turn, writing nothing\n to `<%= it.vars.requirementsFile %>` — consuming the capture with no\n other change IS the sign-off.\n on:\n \"A <%= it.vars.requirementsFile %>\": $onFeedback\n \"M <%= it.vars.requirementsFile %>\": $onFeedback\n \"D <%= it.vars.reviewRawFile %>\": $onSignoff\n\n # ── ENCAPSULATION: the per-package agentic review ──────────────────────────\n # A single state: a machine whose entire job is to be a separate MIND from\n # the implementer, distinct from the coder identity that builds/fixes the\n # package (`packages.item.*`). ▸ planner identity — `model:` declared once\n # here, stamped onto `review` (its only prompt state). Fixing the reviewer's\n # own findings is a coder action, not a reviewer one — it lives in\n # `packages.item.fix-spec` (`$onFix`), not inside this machine.\n specReview:\n model: <%= it.vars.plannerModel %>\n params: [onApproved, onFix]\n entry: review\n states:\n review:\n actor: agent\n label: Reviewing the package\n # Bounded per process: once entered `max` times, a transition into it is\n # redirected to `$onApproved` (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 # `build.fix` cap has (see the README's \"Why\" section, \"Bounded, not\n # runaway\").\n retry:\n max: 3\n otherwise: $onApproved\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are an autonomous coding agent reviewing a freshly-built work package\n against its own spec. This workflow steers itself through its own\n state files — treat them as its private scratchpad, never as project\n code or documentation. The only state file this turn touches is\n `<%= it.vars.specFeedbackFile %>`; write it only when — and only\n when — you find problems. Do not create other files to hold notes or\n output.\n\n The package spec is: <%~ it.read(it.vars.nextFile) %>\n\n Read that spec, then verify the package's implementation against it: are\n all its tasks done, acceptance criteria met, and the code sound and\n consistent with the codebase? This prompt carries no diff — read it\n yourself: the range runs from `<%= it.startCommit %>` to the working\n tree.\n\n Note the range is process-wide (from the start of the whole build, not\n just this package), so it can span earlier packages too.\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 on:\n \"A <%= it.vars.specFeedbackFile %>\": $onFix\n \"M <%= it.vars.specFeedbackFile %>\": $onFix\n \"D <%= it.vars.specFeedbackFile %>\": $onApproved\n \"C\": $onApproved\n\n # ── ENCAPSULATION: the per-package build queue ─────────────────────────────\n # Identity-free (a bookkeeping queue, not a builder) — the actual per-package\n # build identity lives in its own `item` child (packageItem, below).\n packageLoop:\n params: [onDrained]\n entry: picking\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 # Sweep the design/architecture phases' spent steering files:\n # requirementsFile is consumed by architecture.author, architectureFile\n # by architecture.decompose, and questionsFile by whichever gate ran\n # last — all three are gone by the time the package queue starts.\n # reviewRawFile is swept here too: the review loop-back\n # (re-unwind) excludes stateDir from its revert, so an actionable\n # feedback round's MARKED raw capture (see humanReview's own\n # `collecting` state) rides through design and architecture\n # untouched — this is the ONLY sweeper on that path\n # before the queue (and eventually the squash finale) would\n # otherwise pick it up as stray content. No-op if already removed.\n requirements=\"<%~ it.vars.requirementsFile %>\"\n architecture=\"<%~ it.vars.architectureFile %>\"\n questions=\"<%~ it.vars.questionsFile %>\"\n reviewRaw=\"<%~ it.vars.reviewRawFile %>\"\n rm -f \"$requirements\" \"$architecture\" \"$questions\" \"$reviewRaw\"\n next=$(ls <%~ it.vars.packagesDir %>/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n mkdir -p \"$(dirname \"$nextFile\")\"\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 %>\": item.building\n \"C\": $onDrained\n\n # The per-package build identity — its own coder machine (below), so\n # deleting a package file's own lifecycle stays inside `item` while the\n # queue itself (`packages`) stays a single state.\n item:\n machine: packageItem\n with:\n onNext: picking\n\n # ── ENCAPSULATION: the per-package build identity ──────────────────────────\n # ▸ coder identity — `model:` declared once here, stamped onto `building`,\n # `fix-suite`, and `fix-spec` (its own prompt states). `closing` ends that\n # package file's own lifecycle; keeping it here (not in the queue) keeps\n # `packages` itself to a single state. `packages.item.closing` -> `picking`\n # is the ONE upward-resolving target in the whole file, so it MUST be a\n # param (`$onNext`, bound by `packages` — see the machine's own reference).\n packageItem:\n model: <%= it.vars.coderModel %>\n params: [onNext]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn may write is `<%= it.vars.satisfiedFile %>` (see below); do NOT\n delete the package file (the spec-review gate reads it after you)\n and never touch `<%= it.vars.nextFile %>` — the `picking` state owns\n it.\n\n The package to implement is: <%~ it.read(it.vars.nextFile) %>\n\n Before implementing anything, check the package's acceptance criteria\n against the current tree — an earlier package's fix turn may already\n have satisfied them. If **every** criterion is already met, implement\n nothing: write `<%= it.vars.satisfiedFile %>` recording each criterion\n and the concrete evidence it is already satisfied (the commit, file,\n or symbol that proves it), change nothing else, and finish your turn.\n If any criterion is outstanding, implement the package normally and\n do not write that file.\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 \"A <%= it.vars.satisfiedFile %>\": &satisfied\n to: health.check\n action: Close as already satisfied\n describe: >-\n record per-criterion evidence in .gtd/SATISFIED.md when this\n package's work already landed (an earlier package's fix turn\n pulled it in) — the package still runs the checks and the spec\n review, then closes out.\n \"M <%= it.vars.satisfiedFile %>\": *satisfied\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.feedbackFile %>`; fix it per its\n contents, or delete it if the feedback turns out to be wrong. Do not\n create other files to hold notes or output.\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. If the\n only way to green the suite is to implement work that another\n package file describes, make the smallest change that gets there and\n do it — that later package will then legitimately report itself\n already satisfied when its turn comes.\n\n Leave everything uncommitted and finish your turn — do not commit.\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n fix-spec:\n actor: agent\n label: Fixing review feedback\n file: <%= it.vars.specFeedbackFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.specFeedbackFile %>`; address it, then\n delete it. Do not create other files to hold notes or output.\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 \"* **\": health.check\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 and\n # already-satisfied evidence, so `picking` selects the NEXT package (or\n # falls through to review when the queue is empty). Reached on approval\n # (spec review clean) and on the spec review retry cap (force-close,\n # 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 %> <%~ it.vars.satisfiedFile %>\n on:\n \"* **\": $onNext\n\n # Per-package health pair: packages.item.health.check/.escalate ->\n # packages.item.spec.review (green) or packages.item.fix-suite (red).\n health:\n machine: healthGate\n with:\n onGreen: spec\n onRed: fix-suite\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**packages.item.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # Per-package agentic review: packages.item.spec.review -> closing\n # (approved), or packages.item.fix-spec (findings — re-verify via the\n # full health check before re-reviewing).\n spec:\n machine: specReview\n with:\n onApproved: closing\n onFix: fix-spec\n\n # ── ENCAPSULATION: the process's build identity ─────────────────────────────\n # ▸ coder identity — `model:` declared once here, stamped onto `squashing`\n # and `fix` (its own prompt states). There is no monolithic \"build the whole\n # plan\" state any more — every package is built inside `packages.item`, and\n # the queue drains straight into `build.review`. There is no coder-identity\n # follow-through on review feedback here any more — an actionable round\n # leaves this machine entirely, via `$onFeedback` bound at the root's own\n # `build:` reference to the root-local `re-unwind` state (see the header\n # comment's \"Why humanReview is nested\" section for why the review tail\n # still stays nested here even so). `humanReview` is nested HERE (not at the\n # root) so `build.squashing` — the one coder turn a sign-off still resumes —\n # stays inside the builder's own scope.\n buildTail:\n model: <%= it.vars.coderModel %>\n params: [onDone, onFeedback]\n # Nothing resolves this machine BARE (every reference into it names a\n # specific descendant, e.g. `build.review` / `build.fix`) — but a machine\n # must still declare an `entry:` local per its own grammar, so this names\n # an arbitrary one of its own states.\n entry: fix\n states:\n squashing:\n actor: agent\n label: Squashing\n file: <%= it.vars.commitMsgFile %>\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are an autonomous coding agent. The process is approved and done. This\n workflow steers itself through its own state files — treat them as its\n private scratchpad, never as project code or documentation. The only\n state file this turn touches is `<%= it.vars.commitMsgFile %>`; write\n it. Do not create other files to hold notes or output.\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. This prompt carries no diff —\n if you built that range yourself earlier in this conversation, work from\n what you already know; otherwise read the range yourself. It runs from\n `<%= it.retainedBase %>` to the working tree, and is exactly what that\n commit will contain, the changes THIS process made (its trace/retry\n boundary onward). For a normal build\n process that is the whole feature; for a `gtd --entry review-gate.check`\n process it is only the fixes made during the review — NOT the changeset\n that was under review.\n\n If that range is non-empty, write `<%= it.vars.commitMsgFile %>` with ONE\n conventional-commits message for the changes (subject line\n `type(scope): subject`, imperative mood, <= 72 characters; a body\n explaining the why, trade-offs, and key decisions). Plain text, no\n markdown wrapper.\n\n If that range is empty (a clean review sign-off with no fixes), write\n `<%= it.vars.commitMsgFile %>` with exactly this one line and nothing\n else:\n\n chore: human review\n on:\n \"A <%= it.vars.commitMsgFile %>\": $onDone\n \"M <%= it.vars.commitMsgFile %>\": $onDone\n\n fix:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.feedbackFile %>`; fix it per its\n contents, or delete it if the feedback turns out to be wrong. Do not\n create other files to hold notes or output.\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 retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n # Shared health pair: build.health.check/.escalate -> build.review.reviewing\n # (green) or build.fix (red).\n health:\n machine: healthGate\n with:\n onGreen: review\n onRed: fix\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**build.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # The shared review tail, nested HERE (not at the root) so `squashing`\n # stays inside the builder's own scope on sign-off — see the header\n # comment. Sign-off -> squashing; feedback -> $onFeedback, bound at the\n # root's own `build:` reference to `re-unwind` (a root-local, not a\n # descendant of this machine — the review tail's own scope ends here on\n # the actionable path).\n review:\n machine: humanReview\n with:\n onSignoff: squashing\n onFeedback: $onFeedback\n\n # The root machine — the whole one-flow tree described in this file's\n # header comment. WIRING ONLY: no identity of its own, just the shared\n # entry/exit states and the top-level machine tree.\n unified:\n entry: idle\n states:\n # ── Shared entry ────────────────────────────────────────────────────────\n idle:\n actor: human\n label: Idle\n file: <%= it.vars.todoFile %>\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 process.\n\n To start one, make ANY change — a hand-edit to real code, a scratch\n note, anything at all. <%= it.vars.todoFile %> is a good default\n place to start sketching. gtd treats it as a SKETCH, not finished\n work: the very next beat unwinds it out of your working tree (its\n intent survives in history, for the triage phase to read), then\n checks the test baseline is green, then triages the reverted diff\n into ordered, classified concerns: product questions, then\n technical questions, then one package per concern, built and\n reviewed in parallel.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: unwind\n action: Start\n describe: >-\n make any change — gtd unwinds it out of your working tree (its\n intent survives in history) before checking the test baseline is\n green, then triages the reverted diff into ordered, classified\n concerns and resolves product questions, then technical ones,\n before building each concern's package (**unwind** ->\n **start-gate.check**).\n\n # ── Green-baseline gate: `gtd --entry fix-precheck` entry ─────────────────\n # `gtd --entry fix-precheck` enters HERE: run the suite, then a red run\n # drops straight into the shared `build.fix` loop (-> build.health.check ->\n # review + squash tail); a green run is a no-op back to `idle` (nothing\n # to fix). Aliases the `&suiteCheck` anchor defined on `entryGate.check`'s\n # own `script:` above — the same plain suite check `start-gate`/\n # `review-gate` run.\n fix-precheck:\n actor: check\n label: Checking the baseline\n entry: true\n script: *suiteCheck\n on:\n \"A <%= it.vars.feedbackFile %>\": build.fix\n \"M <%= it.vars.feedbackFile %>\": build.fix\n \"D <%= it.vars.feedbackFile %>\": idle\n \"C\": idle\n\n # ── Shared tail: the finale ─────────────────────────────────────────────\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\n # ── Unwind: revert the entry commit's diff before planning starts ──────\n # Kept a SEPARATE state rather than folded into `start-gate.check`'s own\n # script: the `blocked -> check` retry edge re-runs the gate, so a\n # revert living there would discard the human's own baseline repair and\n # would need an idempotence guard. With exactly one inbound edge (from\n # `idle`), `unwind` is structurally reached once per process and needs\n # no guard at all. Names the reverted commit via `it.currentCommit`\n # (they're the same commit here), NOT a bare `HEAD` — pinning the\n # render-time hash keeps the script correct even if a driver runs it\n # late.\n unwind:\n actor: check\n label: Unwinding your input\n script: |\n #!/usr/bin/env bash\n # gtd check turn — revert the entry commit's diff out of the working\n # tree. Its content survives in history for design.triage to read.\n set +e\n # Hoist the commit hash once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last\n # token on a line.\n commit=\"<%~ it.currentCommit %>\"\n git revert --no-commit \"$commit\"\n on:\n \"* **\": start-gate\n\n # ── Re-unwind: revert an actionable review round's hand-edit before ────\n # re-planning starts. A SECOND unwind-shaped state, not a second inbound\n # edge on `unwind` above: `unwind`'s own doc comment turns on it having\n # exactly one inbound edge (from `idle`), which is why it carries no\n # idempotence guard — adding a second caller there would break that\n # invariant. Names the reverted commit via `it.reviewBase`, never\n # `it.currentCommit` (by now that names the assembly turn's own commit)\n # and never a bare `HEAD`: `reviewBaseFor` walks the process trace\n # backwards for the last commit that ENTERED a `reviewBase: true` state\n # — `build.review.deciding` is the template's only one — which is\n # exactly the human's own `await-review -> deciding` commit, the same\n # hash the next review round's incremental base already uses.\n #\n # SCOPED, not wholesale: `deciding` already `rm -f`'d the review file,\n # so a plain `git revert --no-commit` would resurrect it (and conflict\n # on it). Instead this renders that ONE commit's diff with `stateDir`\n # and `reviewFile` excluded from the pathspec (the review file by exact\n # path, since it is var-configurable and may sit at the repo root —\n # issue #128) and reverse-applies it. The apply is EXPECTED to succeed —\n # nothing between that commit and now touches a code path — but it can\n # still FAIL: a clean-filter round-trip, a drifted worktree, or a binary\n # path can all make `git apply -R` a no-op on an otherwise-clean tree,\n # indistinguishable from a legitimate note-only round. `requireRevert`\n # (below) is what catches that: its step-capture guard\n # (`src/StepGuards.ts`) re-establishes the fact from the tree itself —\n # comparing the human's own commit's paths against the review base's\n # parent — and refuses the step while residue remains, rather than\n # trusting the script's swallowed exit code. A note-only round's patch\n # is EMPTY, and `git apply` on empty input FAILS (\"No valid patches in\n # input\"), hence the `-s` guard rather than `--allow-empty` (which would\n # impose git >= 2.35). The apply is worktree-only, no `--index`: the\n # emitted step script's own `git add -A && git commit` stages it, and\n # `changedPaths` answers by CONTENT, so an unstaged revert is still the\n # pending diff the pattern matches — one fewer index write in a\n # worktree gtd shares with the editor's SCM. The patch file lives under\n # `stateDir` (excluded from its own pathspec, the same precedent as the\n # check scripts' own `.check-output`) and is removed before the step\n # lands.\n #\n # Both `on` rows target the SAME place (`design`) because either shape\n # is a legitimate outcome here, never a stall: a hand-edited round's\n # patch is non-empty, so the revert leaves a dirty tree (the `\"* **\"`\n # row); a note-only round's patch is empty, so nothing to apply leaves\n # a CLEAN tree — and a clean tree at a `script` state with no `C` row\n # would otherwise be a silent no-op that never advances. Declaring `C`\n # here makes that the expected, terminating case instead.\n #\n # `file: <%= it.vars.reviewFile %>` + `requireRevert: true` exist ONLY\n # to give the require-revert guard the path it must exempt from residue\n # (the engine blesses no variable names, so the guard reads `Rest.hints.\n # file`, never `it.vars.reviewFile` itself) — the review file is already\n # gone two commits earlier, so this state never renders it. The accepted\n # presentational cost: `gtd next --json`/`gtd status --json` carry that\n # already-deleted path as this state's `file:` hint, and the visualizer\n # labels `re-unwind` with it too. Bounded — `Lsp.ts`'s `buildSteeringMap`\n # skips a mode-less state, so `re-unwind` contributes no path→mode entry\n # and raises no \"already mapped by an earlier state\" warning against\n # `await-review`/`deciding`, which declare the same rendered path with\n # `mode: review`.\n re-unwind:\n actor: check\n label: Re-unwinding your review edit\n file: <%= it.vars.reviewFile %>\n requireRevert: true\n script: |\n #!/usr/bin/env bash\n # gtd check turn — revert the human's own review edit out of the\n # working tree, scoped to real code: stateDir and reviewFile\n # (var-configurable, may live at the repo root — issue #128) are\n # excluded, because the review file is already gone by this point\n # and reverting it would resurrect it. The human's intent survives\n # in their own commit for the triage phase to read. The apply is\n # EXPECTED to succeed — nothing between that commit and now touches\n # a code path — but it can still fail on a clean-filter round-trip,\n # a drifted worktree, or a binary path: the `requireRevert`\n # step-capture guard (src/StepGuards.ts) catches that by\n # re-checking the tree itself before this step is allowed to land.\n # The guard's own residue scoping (`isCodePath`) re-derives the same\n # stateDir/reviewFile exemption this pathspec renders, from the same\n # declaration (never a literal `.gtd/`) — keep the two together\n # when either changes. Both exclusions carry `:(exclude,literal)`,\n # the same magic `deciding` uses: git's DEFAULT pathspec magic is\n # glob-ish (`*` even crosses `/`), and nothing validates a\n # metacharacter out of either value — `stateDirError` rejects only\n # blanks/root/absolute/`..`/non-canonical segments, and `reviewFile`\n # is an ordinary var. A bare `:(exclude)` therefore lets a `*`/`?`/\n # `[` in either value drop a REAL edited path out of the patch,\n # which the guard (a plain string comparison, not a pathspec) then\n # scores as residue and refuses forever.\n set +e\n commit=\"<%~ it.reviewBase %>\"\n stateDir=\"<%~ it.stateDir %>\"\n reviewFile=\"<%~ it.vars.reviewFile %>\"\n patch=\"$stateDir/.re-unwind.patch\"\n mkdir -p \"$stateDir\"\n git diff --binary \"$commit^\" \"$commit\" -- . \\\n \":(exclude,literal)$stateDir\" \":(exclude,literal)$reviewFile\" > \"$patch\"\n if [ -s \"$patch\" ]; then\n git apply -R \"$patch\" || echo \"re-unwind: could not revert $commit\" >&2\n fi\n rm -f \"$patch\"\n on:\n \"* **\": design # a code hand-edit was reverted\n \"C\": design # note-only round: nothing to revert\n\n # ── Green-baseline gate: the single start entry. ───────────────────────\n # Runs on the tree AFTER `unwind`, so this is now just the ordinary\n # green-baseline check `review-gate`/`fix-precheck` also run — no\n # baseline-aware second run, no detached checkout, no HEAD move\n # anywhere in gtd's emitted scripts any more.\n start-gate:\n machine: entryGate\n with:\n onGreen: design\n reviewBase: \"\"\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 land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**start-gate.check**). To repair the baseline as its own separate\n reviewed commit instead, abandon this start and run\n `gtd --entry fix-precheck` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── Green-baseline gate: `gtd --entry review-gate.check` entry -> build.review.reviewing. ─\n review-gate:\n machine: entryGate\n with:\n onGreen: build.review\n reviewBase: <%= it.vars.reviewBase %>\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 land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**review-gate.check**).\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── The triage phase (design.triage -> design.gate.check/.answer -> architecture). ──\n design:\n machine: designPlan\n with:\n onArchitecture: architecture\n\n # ── The technical phase (architecture.author -> architecture.gate.check/.answer ─\n # -> architecture.decompose -> packages). ──────────────────────────────\n architecture:\n machine: archPlan\n with:\n onPackages: packages\n\n # ── The per-package build queue (packages.* -> build.review). ─────────────\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n # ── The process's build identity (build.* -> build.review -> done). ───────\n build:\n machine: buildTail\n with:\n onDone: done\n onFeedback: re-unwind\n";
192157
192293
  //#endregion
192158
192294
  //#region src/workflows/templates.ts
192159
192295
  /**
@@ -192353,12 +192489,16 @@ const JSON_TYPE = {
192353
192489
  /** The whole `workflow:` value — see `PatternConfig.ts`'s module docstring for the authoritative schema. */
192354
192490
  const workflowJsonSchema = {
192355
192491
  type: "object",
192356
- description: "The whole machine definition: a tree of named machines rooted at entry.default (plus the workflow's own vars: defaults and modes: steering-file modes). Compiled and validated by gtd at load time; content strings starting with ./ or ../ are file references inlined from the config file's directory (a modes: command never is — it is a shell command).",
192492
+ description: "The whole machine definition: a tree of named machines rooted at entry.default (plus the workflow's own vars: defaults, modes: steering-file modes, and stateDir: plumbing-directory declaration). Compiled and validated by gtd at load time; content strings starting with ./ or ../ are file references inlined from the config file's directory (a modes: command never is — it is a shell command).",
192357
192493
  additionalProperties: false,
192358
192494
  required: ["entry", "machines"],
192359
192495
  properties: {
192360
192496
  vars: varsJsonSchema,
192361
192497
  modes: modesJsonSchema,
192498
+ stateDir: {
192499
+ type: "string",
192500
+ description: "An Eta template naming where this workflow keeps its own scratch/bookkeeping directory (the review window's revert pathspec, the step guards' code-vs-plumbing test). Defaults to \".gtd\" when absent. The rendered value must name a directory inside the repository: not the repo root, not an absolute path, not a path that escapes the repo via `..`. It must also be spelled canonically — no `.` and no empty path segments (e.g. `a/./state` or `a//state` are rejected, not rewritten) — though a leading `./` and a trailing `/` are accepted. Distinct from the ordinary `vars.stateDir` workflow var of the same name one level down — this is the definition-level declaration the engine reads; the var is the knob a user overrides, which the bundled template renders this declaration from."
192501
+ },
192362
192502
  entry: {
192363
192503
  type: "object",
192364
192504
  description: "The workflow's root machine, resolved through the same resolver an `on`/`retry.otherwise` target uses (see src/Machines.ts) — accepts either a bare state path or an instance/reference-key path. Extra manual entry points are declared per-state instead, via a state's own `entry: true` (see `stateJsonSchema`'s `entry` property) — not here.",
@@ -193556,10 +193696,13 @@ var Eta = class extends Eta$1 {
193556
193696
  * The stub `TemplateContext` an `on` pattern key renders against at the edge
193557
193697
  * (see `Edge.ts`'s `renderOnEdges`): only `vars` (and, optionally, `state`)
193558
193698
  * are real — every commit-ish/diff field is empty, `processCost` is `0`,
193559
- * `edges` is empty, and `read` throws, since a pattern names a path and never
193560
- * legitimately needs a working tree or git history to resolve. This is also
193561
- * `Lsp.ts`'s `buildFileModeMap` map-building context, factored out here to
193562
- * avoid building the same stub in two places.
193699
+ * `edges` is empty, `stateDir` is empty, and `read` throws, since a pattern
193700
+ * names a path and never legitimately needs a working tree or git history to
193701
+ * resolve. This is also `Lsp.ts`'s `buildFileModeMap` map-building context,
193702
+ * factored out here to avoid building the same stub in two places, AND the
193703
+ * context `src/Edge.ts` renders the `stateDir:` declaration itself against —
193704
+ * an empty `stateDir` there is what makes a declaration that references
193705
+ * `it.stateDir` render empty rather than recurse.
193563
193706
  */
193564
193707
  const varsOnlyContext = (vars, state = "") => ({
193565
193708
  startCommit: "",
@@ -193575,7 +193718,8 @@ const varsOnlyContext = (vars, state = "") => ({
193575
193718
  throw new Error(`no working tree to read from while rendering against a vars-only context (path: ${path})`);
193576
193719
  },
193577
193720
  vars,
193578
- edges: []
193721
+ edges: [],
193722
+ stateDir: ""
193579
193723
  });
193580
193724
  const eta = new Eta();
193581
193725
  eta.readFile = null;
@@ -193675,6 +193819,8 @@ const restorability = (git, headHash, headMessage, tipHash) => gen(function* ()
193675
193819
  * module — every builder below routes every interpolated value through this.
193676
193820
  */
193677
193821
  const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
193822
+ /** Turns a path list into a shell pathspec: every path quoted via `shellQuote`, space-joined. */
193823
+ const pathspec = (paths) => paths.map(shellQuote).join(" ");
193678
193824
  /**
193679
193825
  * `--allow-empty` mirrors `commitAllWithPrefix`/`commitAsIs`'s load-bearing
193680
193826
  * use in `src/Git.ts`: gtd's workflow commits may be empty on purpose. The
@@ -193741,7 +193887,7 @@ const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
193741
193887
  */
193742
193888
  const restoreStagedFrom = (source, paths) => {
193743
193889
  if (paths.length === 0) return "";
193744
- const pathArgs = paths.map(shellQuote).join(" ");
193890
+ const pathArgs = pathspec(paths);
193745
193891
  return [
193746
193892
  `if ! out=$(git restore --staged --source=${shellQuote(source)} -- ${pathArgs} 2>&1); then`,
193747
193893
  ` case "$out" in`,
@@ -194090,15 +194236,18 @@ const combinedScript = (required, optional) => {
194090
194236
  * renamed-state refusal
194091
194237
  * 4. `commitHistory()` → `ProcessRun`
194092
194238
  * 5. `vars` — the four-layer merge (workflow < rc < entry commit < env)
194093
- * 6. `on` — the resting state's `on` edges, rendered against `vars` (Eta
194239
+ * 6. `stateDir` — `def`'s declared plumbing directory (`stateDirOf`),
194240
+ * rendered against `vars` ONLY (Eta template; `vars` must precede this,
194241
+ * same ordering problem `on`'s render below has)
194242
+ * 7. `on` — the resting state's `on` edges, rendered against `vars` (Eta
194094
194243
  * templates; `vars` must precede this)
194095
- * 7. `stepDef` — `def` with `on` patched onto the resting state, the shape
194244
+ * 8. `stepDef` — `def` with `on` patched onto the resting state, the shape
194096
194245
  * `PatternMachine.step` must be fed
194097
- * 8. `reviewBase` — the pure `reviewBaseFor(def, run)`
194098
- * 9. `changes` — `changedPaths()`
194099
- * 10. `context` — the full `TemplateContext` (`on` and `reviewBase` both feed
194100
- * it — `it.edges` derives from `on`)
194101
- * 11. `memory`, then `hints` (`model`/`label`/`file` rendered against
194246
+ * 9. `reviewBase` — the pure `reviewBaseFor(def, run)`
194247
+ * 10. `changes` — `changedPaths()`
194248
+ * 11. `context` — the full `TemplateContext` (`on`, `stateDir` and
194249
+ * `reviewBase` all feed it — `it.edges` derives from `on`)
194250
+ * 12. `memory`, then `hints` (`model`/`label`/`file` rendered against
194102
194251
  * `context`; `mode` passed through verbatim)
194103
194252
  *
194104
194253
  * Two module rules, enforced on this file itself: a private helper with only
@@ -194507,6 +194656,41 @@ const renderOnEdgesOrFail = (onEdges, vars) => try_({
194507
194656
  catch: (e) => e instanceof Error ? e : new Error(String(e))
194508
194657
  });
194509
194658
  /**
194659
+ * Render `def`'s declared plumbing directory (`PatternMachine.stateDirOf`)
194660
+ * against `vars` ONLY (`varsOnlyContext`) — same restriction `renderOnEdges`
194661
+ * uses, which is what breaks the circularity of a context field whose own
194662
+ * value comes from a template rendered against that context: a declaration
194663
+ * that references `it.stateDir` itself sees the empty stub there and renders
194664
+ * empty rather than recursing. Stripped ONCE here
194665
+ * (`PatternMachine.canonicalStateDir`) — tolerance for a leading `./` and a
194666
+ * trailing `/`, NOT full canonicalization; it does not make every downstream
194667
+ * reader see a canonical string on its own. `renderStateDirOrFail` is what
194668
+ * makes that guarantee true, by refusing anything the strip didn't already
194669
+ * fix. Throws whatever Eta throws on a malformed template; the caller turns
194670
+ * that into a step refusal / command error, exactly like a content render
194671
+ * failure.
194672
+ */
194673
+ const renderStateDir = (def, vars) => canonicalStateDir(renderStateTemplate(stateDirOf(def), varsOnlyContext(vars)));
194674
+ /**
194675
+ * `renderStateDir`, surfacing a malformed template as a plain `Error` (see
194676
+ * `renderOnEdgesOrFail`), THEN running `PatternMachine.stateDirError` on the
194677
+ * rendered, stripped result and failing with its message when it fires. This
194678
+ * is the one ENFORCEMENT site for the value rule (see
194679
+ * `PatternMachine.stateDirError`'s doc comment for why load time can't own
194680
+ * it): a non-canonical spelling that survives `renderStateDir`'s tolerance
194681
+ * strip is a config error naming the canonical form, never something this
194682
+ * function or any downstream consumer repairs — every caller of
194683
+ * `currentRest`/`restAt` (`gtd next`/`status`/`land` alike) sees the same
194684
+ * failure before the value reaches any consumer.
194685
+ */
194686
+ const renderStateDirOrFail = (def, vars) => try_({
194687
+ try: () => renderStateDir(def, vars),
194688
+ catch: (e) => e instanceof Error ? e : new Error(String(e))
194689
+ }).pipe(flatMap$4((stateDir) => {
194690
+ const error = stateDirError(stateDir);
194691
+ return error === void 0 ? succeed$3(stateDir) : fail$2(new Error(error));
194692
+ }));
194693
+ /**
194510
194694
  * A shallow clone of `def` whose `state`'s `on` is replaced by
194511
194695
  * `renderedOnEdges` — used to feed `PatternMachine.step`, which matches only
194512
194696
  * `def.states[state].on` for the state it's invoked at. Only the RESTING
@@ -194535,9 +194719,11 @@ const withRenderedOn = (def, state, renderedOnEdges) => ({
194535
194719
  * breakdown including the squashing step) — `0`/absent for the pure emitters
194536
194720
  * (`gtd next`/`gtd status`), where no step is being performed. No diff is ever
194537
194721
  * computed here — `it.reviewBase`/`it.retainedBase` are bases a template tells
194538
- * the agent to `git diff` itself.
194722
+ * the agent to `git diff` itself. `stateDir` is ALREADY RENDERED and
194723
+ * normalized by the caller (`renderStateDir`) — this function only carries it
194724
+ * onto `it.stateDir`, same discipline as `edges`.
194539
194725
  */
194540
- const buildTemplateContext = (git, read, state, actor, run, vars, edges, currentCost, currentModel, reviewBase) => gen(function* () {
194726
+ const buildTemplateContext = (git, read, state, actor, run, vars, edges, currentCost, currentModel, reviewBase, stateDir) => gen(function* () {
194541
194727
  const currentCommit = yield* git.resolveRef("HEAD");
194542
194728
  const previousCommit = yield* git.resolveRef("HEAD~1").pipe(catchAll(() => succeed$3(run.startParentHash)));
194543
194729
  const stepEntry = currentCost > 0 || currentModel !== void 0 ? [{
@@ -194557,7 +194743,8 @@ const buildTemplateContext = (git, read, state, actor, run, vars, edges, current
194557
194743
  processCostByModel: costByModel(allCostEntries),
194558
194744
  read,
194559
194745
  vars,
194560
- edges: toTemplateEdges(edges)
194746
+ edges: toTemplateEdges(edges),
194747
+ stateDir
194561
194748
  };
194562
194749
  });
194563
194750
  /**
@@ -194605,11 +194792,12 @@ const restAt = (ref) => gen(function* () {
194605
194792
  const resolved = resolution.rest;
194606
194793
  const run = yield* computeProcessRun(git, def, windowHead);
194607
194794
  const vars = resolveVars(config.workflowVars, config.rcVars, run.entryVars, envVars.all);
194795
+ const stateDir = yield* renderStateDirOrFail(def, vars);
194608
194796
  const on = yield* renderOnEdgesOrFail(resolved.stateDef.on, vars);
194609
194797
  const stepDef = withRenderedOn(def, resolved.state, on);
194610
194798
  const reviewBase = reviewBaseFor(def, run);
194611
194799
  const changes = yield* pendingChanges(git, windowHead);
194612
- const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase);
194800
+ const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase, stateDir);
194613
194801
  const memory = memoryKeyFor(config.stateScopes, resolved, run);
194614
194802
  const memoryResumed = memoryResumedFor(def, config.stateScopes, resolved, run);
194615
194803
  const hints = yield* renderHints(resolved.stateDef, context);
@@ -194721,14 +194909,16 @@ const formatStepRefusal = (refusal) => refusal.reason === "out-of-turn" ? `gtd l
194721
194909
  * `TemplateContext` at all), and it is NOT interchangeable with `rest.context`:
194722
194910
  * that one is pinned to the resting state and carries `cost: 0`, so rendering
194723
194911
  * `it.processCost` against it silently omits the squashing step's own
194724
- * `--cost`. Exported for `program.ts`'s `planLanding`, which assembles the
194912
+ * `--cost`. `stateDir` is definition-level and vars-only, so it never varies
194913
+ * by target state — reused verbatim from `rest.context.stateDir` rather than
194914
+ * re-rendered. Exported for `program.ts`'s `planLanding`, which assembles the
194725
194915
  * same script by hand and must pick the same context.
194726
194916
  */
194727
194917
  const contextAt = (rest, targetState, invoker, cost, model) => gen(function* () {
194728
194918
  const git = yield* GitService;
194729
194919
  const files = yield* RepoFiles;
194730
194920
  const onEdges = targetState === rest.state ? rest.on : yield* renderOnEdgesOrFail(rest.def.states[targetState]?.on, rest.vars);
194731
- return yield* buildTemplateContext(git, templateRead(files), targetState, invoker, rest.run, rest.vars, onEdges, cost ?? 0, model, reviewBaseFor(rest.def, rest.run));
194921
+ return yield* buildTemplateContext(git, templateRead(files), targetState, invoker, rest.run, rest.vars, onEdges, cost ?? 0, model, reviewBaseFor(rest.def, rest.run), rest.context.stateDir);
194732
194922
  });
194733
194923
  /**
194734
194924
  * Render a `"commit"`/`"squash"` decision as the `EmitStep`s the external
@@ -206999,9 +207189,31 @@ const openInBrowser = (url) => {
206999
207189
  * state's `file:`, so they ask it in exactly one place. A deletion is a
207000
207190
  * legitimate step outcome at some states (a review sign-off's bare REVIEW.md
207001
207191
  * deletion) and a refusal at others (see `reviewSignoffGuard`), but either way
207002
- * there is no file left to format or validate.
207192
+ * there is no file left to format or validate. `steeringModeSteps` widens this
207193
+ * same "nothing to format" idea one step further, to a file that was never
207194
+ * written at all (not deleted this step, just absent) — that's a separate
207195
+ * `RepoFiles.working` check there, not a change to this function or to
207196
+ * `fileDeleted`'s semantics here.
207003
207197
  */
207004
207198
  const deletesFile = (changes, file) => changes.some((c) => c.path === file && c.status === "D");
207199
+ /**
207200
+ * True when `path` sits inside the declared plumbing directory `dir` (carries
207201
+ * no trailing slash) — an exact match on the bare directory path itself, or
207202
+ * prefixed by `dir + "/"` (the separator is appended here, not assumed to be
207203
+ * part of `dir`). A naive `path.startsWith(dir)` would also swallow a sibling
207204
+ * `<dir>-backup/` next to the declared directory.
207205
+ */
207206
+ const isPlumbingPath = (path, dir) => path === dir || path.startsWith(`${dir}/`);
207207
+ /**
207208
+ * True when `path` is a real code path — outside the DECLARED plumbing
207209
+ * directory (`stateDir`, never a literal `.gtd/`) and not the state's own
207210
+ * `file:`, excluded by EXACT path (never merely a prefix check, since `file:`
207211
+ * is var-configurable to live outside `stateDir` — issue #128). The one "did
207212
+ * a human touch something real" predicate shared by `enforceStepGuards`'s own
207213
+ * `hasCodeChange` and the require-revert guard's residue scoping, so the rule
207214
+ * is declared exactly once.
207215
+ */
207216
+ const isCodePath = (path, file, stateDir) => !isPlumbingPath(path, stateDir) && path !== file;
207005
207217
  /** Normalize every markdown checkbox to a single placeholder so a pure `[ ]`→`[x]` tick is invisible to a text comparison; any surviving difference is a human note. */
207006
207218
  const normalizeCheckboxes = (content) => content.replace(/\[[ xX]\]/g, "[_]");
207007
207219
  /** The `mode:` name of gtd's built-in REVIEW.md checkbox validator — the only mode the sign-off guard understands. */
@@ -207033,7 +207245,7 @@ const feedbackProgressGuard = {
207033
207245
  };
207034
207246
  /** The `mode:` name of gtd's built-in open-questions checkbox format — the only mode the answer-completeness guard acts on. */
207035
207247
  const QA_MODE = "qa";
207036
- /** The three step-capture guards, in EVALUATION order — message precedence when two would fire is observable, so this order is the contract (matches the old hand-copied call order, minus the now-removed steering-file guard). */
207248
+ /** The four step-capture guards, in EVALUATION order — message precedence when two would fire is observable, so this order is the contract (matches the old hand-copied call order, minus the now-removed steering-file guard). */
207037
207249
  const stepGuards = [
207038
207250
  reviewSignoffGuard,
207039
207251
  feedbackProgressGuard,
@@ -207046,6 +207258,21 @@ const stepGuards = [
207046
207258
  const list = unanswered.map((q) => ` - ${q.question}`).join("\n");
207047
207259
  return `${unanswered.length} open question(s) in ${ctx.file} not answered at "${ctx.rest.state}" — tick exactly one option per question (or delete a question you don't want to answer, or delete the whole "## Open Questions" section to accept the plan as-is):\n${list}`;
207048
207260
  })
207261
+ },
207262
+ {
207263
+ name: "require-revert",
207264
+ appliesTo: (rest) => isRequireRevertState(rest.def, rest.state),
207265
+ check: (ctx) => gen(function* () {
207266
+ const rb = ctx.template.reviewBase;
207267
+ if (rb === "" || rb === ctx.template.startCommit) return `"${ctx.rest.state}" has no identifiable review round to check (reviewBase is unset) — the revert cannot be established.`;
207268
+ const git = yield* GitService;
207269
+ const base = `${rb}~1`;
207270
+ const scoped = ((yield* git.commitHistory(base, rb))[0]?.touched ?? []).filter((path) => isCodePath(path, ctx.file, ctx.template.stateDir));
207271
+ if (scoped.length === 0) return void 0;
207272
+ const residue = (yield* git.changedPaths(base)).filter((c) => scoped.includes(c.path)).map((c) => c.path);
207273
+ if (residue.length === 0) return void 0;
207274
+ return `${residue.join(", ")} still differ from ${base} at "${ctx.rest.state}" — the revert did not take. Run \`git checkout ${base} -- ${pathspec(residue)}\`, then \`gtd land\` again.`;
207275
+ })
207049
207276
  }
207050
207277
  ];
207051
207278
  /**
@@ -207064,7 +207291,7 @@ const enforceStepGuards = (input) => gen(function* () {
207064
207291
  const applicable = stepGuards.filter((g) => g.appliesTo(input.rest));
207065
207292
  if (applicable.length === 0) return;
207066
207293
  const fileDeleted = deletesFile(input.changes, file);
207067
- const hasCodeChange = input.changes.some((c) => !c.path.startsWith(".gtd/") && c.path !== file);
207294
+ const hasCodeChange = input.changes.some((c) => isCodePath(c.path, file, input.context.stateDir));
207068
207295
  const base = {
207069
207296
  rest: input.rest,
207070
207297
  file,
@@ -207510,21 +207737,24 @@ const headPreconditions = (currentCommit) => ({ expectedHead: currentCommit });
207510
207737
  * Empty for a state declaring no `file:`+`mode:` pair; an unknown mode name
207511
207738
  * is a refusal, exactly as it was when gtd ran the commands itself.
207512
207739
  *
207513
- * Also empty when the step DELETES that file (`deletesFile`). A deletion is a
207514
- * legitimate outcome at some states — `build.review.deciding`'s sign-off diff
207515
- * is a bare REVIEW.md deletion — and there is nothing left to format or
207516
- * validate either way. Emitting the mode's `format:` anyway made the step
207517
- * UNLANDABLE for any formatter that treats a missing path as an error
207518
- * (`prettier --write` exits non-zero with "No files matching the pattern were
207519
- * found"): it is the first command in a `set -euo pipefail` script, so it
207520
- * aborted the whole thing — window close, commit and all — before anything
207521
- * could land, and a driver saw only a non-zero exit.
207740
+ * Also empty when the step DELETES that file (`deletesFile`), or when the
207741
+ * file is simply ABSENT from the working tree (never written at all — the
207742
+ * step's diff touched something else entirely). A deletion is a legitimate
207743
+ * outcome at some states — `build.review.deciding`'s sign-off diff is a bare
207744
+ * REVIEW.md deletion — and either way (deleted or never created) there is
207745
+ * nothing left to format or validate. Emitting the mode's `format:` anyway
207746
+ * made the step UNLANDABLE for any formatter that treats a missing path as an
207747
+ * error (`prettier --write` exits non-zero with "No files matching the
207748
+ * pattern were found"): it is the first command in a `set -euo pipefail`
207749
+ * script, so it aborted the whole thing — window close, commit and all —
207750
+ * before anything could land, and a driver saw only a non-zero exit.
207522
207751
  */
207523
207752
  const steeringModeSteps = (rest) => gen(function* () {
207524
207753
  const file = rest.hints.file;
207525
207754
  const mode = rest.stateDef.mode;
207526
207755
  if (file === void 0 || mode === void 0) return [];
207527
207756
  if (deletesFile(rest.changes, file)) return [];
207757
+ if ((yield* RepoFiles).working(file) === void 0) return [];
207528
207758
  const resolved = resolveSteeringMode(rest.def, mode);
207529
207759
  if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
207530
207760
  return (yield* renderSteeringCommands(resolved, file, rest.context)).map((command) => ({