@pmelab/gtd 9.7.0 → 9.9.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.
@@ -189739,6 +189739,17 @@ const STATE_FIELDS = {
189739
189739
  rest: "rendered",
189740
189740
  doc: "Opaque harness hint stamped onto every one of this machine's own `prompt` states (e.g. \"smart\"), passed through `gtd next --json`/`gtd status --json`. The ONLY place a model may be declared — a state carrying its own `model:` is a config error. Never interpreted by gtd. A machine declaring this with no `prompt` state is a config error."
189741
189741
  },
189742
+ system: {
189743
+ kind: "text",
189744
+ surface: "def",
189745
+ authored: "machine",
189746
+ nonEmpty: true,
189747
+ commit: "forbidden",
189748
+ fileRef: true,
189749
+ rest: "rendered",
189750
+ viz: "field",
189751
+ doc: "The agent harness system prompt stamped onto every one of this machine's own `prompt` states — emitted verbatim in `gtd next --json`/`--sh` for the driver to pass to its agent CLI, never interpreted by gtd. Machine-level only: a machine's `prompt` states share one resumed session, and a system prompt must be identical across every call of it. A `./` or `../` value is inlined from the declaring config file's directory. A machine declaring this with no `prompt` state is a config error."
189752
+ },
189742
189753
  /**
189743
189754
  * An OPAQUE, human-readable display NAME for the state — gtd never
189744
189755
  * interprets this string, it only passes it through verbatim (`gtd next
@@ -189766,21 +189777,25 @@ const STATE_FIELDS = {
189766
189777
  * Optional — THE steering file this state is about: the file a human/
189767
189778
  * editor should look at while the machine rests here. An Eta template
189768
189779
  * (rendered through the same `it.vars`-carrying context as content and
189769
- * `model`) that must render non-empty. Forbidden on a commit state (never
189770
- * at rest — see `validateDefinition`). Multiple states may share one
189771
- * `file:` (and, in the bundled default, do). The engine never reads a
189772
- * path out of this string itself — only the LSP (`src/Lsp.ts`) interprets
189773
- * it, to map rendered paths to `mode`.
189780
+ * `model`) that must render non-empty. Names a path RELATIVE to `.gtd/` —
189781
+ * the compiler prepends the directory (see `PatternConfig.ts`'s `stateFile`
189782
+ * compiler); a `..` segment, an absolute path, or an already-prefixed
189783
+ * `.gtd/...` declaration are all load-time errors, never silently
189784
+ * rewritten. Forbidden on a commit state (never at rest — see
189785
+ * `validateDefinition`). Multiple states may share one `file:` (and, in the
189786
+ * bundled default, do). The engine never reads a path out of this string
189787
+ * itself — only the LSP (`src/Lsp.ts`) interprets it, to map rendered paths
189788
+ * to `mode`.
189774
189789
  */
189775
189790
  file: {
189776
- kind: "text",
189791
+ kind: "stateFile",
189777
189792
  surface: "def",
189778
189793
  authored: "state",
189779
189794
  nonEmpty: true,
189780
189795
  commit: "forbidden",
189781
189796
  viz: "field",
189782
189797
  rest: "rendered",
189783
- doc: "The state's steering file: an Eta template naming the file a human/editor should look at while the machine rests here. Forbidden on a commit state."
189798
+ doc: "The state's steering file: an Eta template naming the file a human/editor should look at while the machine rests here, RELATIVE to \".gtd/\" — the compiler prepends that directory automatically. A \"..\" segment, an absolute path, or an already-prefixed \".gtd/\" are rejected, not rewritten. Forbidden on a commit state."
189784
189799
  },
189785
189800
  /**
189786
189801
  * Optional, requires `file:`. The associated file's FORMAT — the NAME of a
@@ -189937,6 +189952,8 @@ const STATE_FIELDS = {
189937
189952
  const STATE_FIELD_ENTRIES = Object.entries(STATE_FIELDS);
189938
189953
  /** The four content-kind field names, in declaration order — the "exactly one of" set `validateContentKind`/`compileContent` iterate. */
189939
189954
  const CONTENT_FIELDS = STATE_FIELD_ENTRIES.filter(([, spec]) => spec.kind === "content").map(([key]) => key);
189955
+ /** Every machine-authored field, in table order — the machine tier's `CONTENT_FIELDS`. */
189956
+ const MACHINE_FIELD_ENTRIES = STATE_FIELD_ENTRIES.filter(([, spec]) => spec.authored === "machine");
189940
189957
  /** True when a state is a commit (final, squash) state. */
189941
189958
  const isCommitState = (state) => state.commit !== void 0;
189942
189959
  /**
@@ -190001,64 +190018,16 @@ const validateFieldRules = (name, state, key, spec) => {
190001
190018
  /** 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). */
190002
190019
  const knownModes = (def) => Object.keys(def.modes ?? {});
190003
190020
  /**
190004
- * `def`'s declared plumbing directory — the raw Eta template source a
190005
- * workflow carried on `stateDir:`, or `DEFAULT_STATE_DIR` when it declared
190006
- * none (so a workflow declaring nothing, and every hand-built test fixture
190007
- * that skipped the compiler, behaves exactly as before this key existed).
190008
- * Rendering the template is an edge concern, exactly like `entryBaseTemplateOf`
190009
- * — this module only carries and defaults the raw string.
190010
- */
190011
- const stateDirOf = (def) => def.stateDir ?? ".gtd";
190012
- /**
190013
- * The leading-`./` + trailing-`/` strip shared by `stateDirError` and
190014
- * `src/Edge.ts`'s `renderStateDir` — the one tolerated rewrite of a
190015
- * `stateDir` declaration, covering the two conventional spellings a
190016
- * hand-written template or `.gtdrc` value commonly carries. Exported as one
190017
- * function rather than duplicated at the edge so the two call sites can never
190018
- * drift into tolerating different affixes. Anything beyond these two is a
190019
- * spelling `stateDirError` rejects rather than rewrites — the strip is
190020
- * tolerance, not full canonicalization; it does not touch an internal `.` or
190021
- * empty segment.
190022
- */
190023
- const canonicalStateDir = (value) => value.replace(/^\.\//, "").replace(/\/+$/, "");
190024
- /**
190025
- * The one place the engine interprets a `stateDir` VALUE (never the var
190026
- * NAME) — pure and total, so both the edge (on the rendered, normalized
190027
- * value, before any consumer sees it — see `src/Edge.ts`'s
190028
- * `renderStateDirOrFail`) and a test can call it directly. Returns the
190029
- * error message when `value` cannot name a usable plumbing directory,
190030
- * `undefined` when it can.
190031
- *
190032
- * Rejects, in this order: blank/whitespace-only; the repo root (`.`, `./`,
190033
- * `/`, or empty once a leading `./` and trailing `/` are stripped); an
190034
- * absolute path (a leading `/` after that stripping); any `..` segment (an
190035
- * escape); any other `.` or empty path segment (a non-canonical spelling —
190036
- * `a/./state`, `a//state`, `a/.` — rejected rather than silently
190037
- * canonicalized, since every downstream consumer compares `it.stateDir`
190038
- * rather than re-normalizing it). The `..` escape check runs BEFORE the
190039
- * segment-canonicality check so a value carrying BOTH is always reported as
190040
- * an escape, never as a spelling to rewrite — `a/.././state` (segments `a`,
190041
- * `..`, `.`, `state`) would, under the reverse order, suggest the canonical
190042
- * spelling `"a/../state"`, a rewrite that still escapes the repo. `..` is
190043
- * deliberately not in the canonicality predicate — it is an escape, not a
190044
- * spelling. Deliberately does its own stripping rather than requiring an
190045
- * already-normalized `value` — a caller may hand it either the raw
190046
- * declaration or the edge's normalized form and get the same verdict, which
190047
- * is what lets a unit test exercise `"./"` directly.
190048
- */
190049
- const stateDirError = (value) => {
190050
- 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)})`;
190051
- if (value.trim() === "") return invalid();
190052
- const stripped = canonicalStateDir(value);
190053
- if (stripped === "" || stripped === ".") return invalid();
190054
- if (stripped.startsWith("/")) return invalid();
190055
- const segments = stripped.split("/");
190056
- if (segments.includes("..")) return invalid();
190057
- if (segments.some((segment) => segment === "." || segment === "")) {
190058
- const canonical = segments.filter((segment) => segment !== "." && segment !== "").join("/");
190059
- return `"stateDir": ${JSON.stringify(stripped)} is not a canonical path — write it as ${JSON.stringify(canonical)}`;
190060
- }
190061
- };
190021
+ * The engine's own plumbing directory — gtd's scratch/bookkeeping directory
190022
+ * (the review window's revert pathspec, the step guards' code-vs-plumbing
190023
+ * test) and the prefix `PatternConfig.ts`'s `stateFile` compiler prepends onto
190024
+ * every state's `file:` declaration. A plain const, not a workflow-level
190025
+ * declaration: there is nothing left to relocate it to, and the three sites
190026
+ * that must agree on it — the compiler that prepends, the validator that
190027
+ * re-checks, and the runtime guard that tests the prefix — all import this
190028
+ * one value rather than re-deriving a prefix string of their own.
190029
+ */
190030
+ const STATE_DIR = ".gtd";
190062
190031
  /** Which content kind a state declares, or `undefined` if none (a validation error). */
190063
190032
  const contentKindOf = (state) => {
190064
190033
  if (state.script !== void 0) return "script";
@@ -190497,18 +190466,6 @@ const validateKnownMode = (def, name, state) => {
190497
190466
  return [`state "${name}": "mode" must name a mode this workflow knows (${known.length > 0 ? known.join(", ") : "none declared"}) (got "${state.mode}")`];
190498
190467
  };
190499
190468
  /**
190500
- * `stateDir`, when DECLARED, must be non-blank — the same
190501
- * blank/whitespace-only check `validateReviewBaseTemplate` runs for a string
190502
- * `reviewBase` template. Deliberately checks nothing else: `stateDirError`'s
190503
- * repo-root/absolute/escape rules apply to the RENDERED value, which this
190504
- * load-time pass can't see (the bundled declaration is itself a template,
190505
- * e.g. `<%= it.vars.stateDir %>`, whose var can arrive at runtime via a
190506
- * `GTD_STATEDIR` override no load-time check could ever observe — see
190507
- * `renderStateDirOrFail` in `src/Edge.ts` for the one site that does own
190508
- * that rule).
190509
- */
190510
- const validateStateDir = (def) => def.stateDir !== void 0 && def.stateDir.trim() === "" ? [`"stateDir" template must not be blank`] : [];
190511
- /**
190512
190469
  * When `reviewBase` is a STRING (the template form — see `StateDef.reviewBase`),
190513
190470
  * its source must be non-blank: a literal `reviewBase: ""` (or whitespace-only)
190514
190471
  * is an authoring error, distinct from the runtime concern of the RENDERED
@@ -190520,6 +190477,18 @@ const validateReviewBaseTemplate = (name, state) => {
190520
190477
  if (typeof state.reviewBase !== "string") return [];
190521
190478
  return state.reviewBase.trim() === "" ? [`state "${name}": "reviewBase" template must not be blank`] : [];
190522
190479
  };
190480
+ /**
190481
+ * A compiled `file:` must sit under `STATE_DIR` (`.gtd`) — `PatternConfig.ts`'s
190482
+ * `stateFile` compiler already guarantees this for anything it compiles by
190483
+ * prepending the directory itself, so this can only fire for a definition
190484
+ * built directly in TypeScript (a hand-authored test fixture) that skipped
190485
+ * the compiler entirely.
190486
+ */
190487
+ const validateFileUnderStateDir = (name, state) => {
190488
+ const file = state.file;
190489
+ if (file === void 0) return [];
190490
+ return file === ".gtd" || file.startsWith(`.gtd/`) ? [] : [`state "${name}": "file" must be under "${STATE_DIR}/" (got "${file}")`];
190491
+ };
190523
190492
  /** Every `on` row parses, and its target names a defined state. */
190524
190493
  const validateOnEdges = (name, state, names) => {
190525
190494
  const errors = [];
@@ -190586,7 +190555,8 @@ const BESPOKE = {
190586
190555
  on: (_def, name, state, names) => validateOnEdges(name, state, names),
190587
190556
  retry: (_def, name, state, names) => validateRetry(name, state, names),
190588
190557
  mode: (def, name, state) => validateKnownMode(def, name, state),
190589
- reviewBase: (_def, name, state) => validateReviewBaseTemplate(name, state)
190558
+ reviewBase: (_def, name, state) => validateReviewBaseTemplate(name, state),
190559
+ file: (_def, name, state) => validateFileUnderStateDir(name, state)
190590
190560
  };
190591
190561
  /**
190592
190562
  * All per-state rule checkers, run over one state: the two group-rule
@@ -190613,12 +190583,14 @@ const validateState = (def, name, names) => {
190613
190583
  * `entries.manual` itself (see `validateEntries`); every state declares
190614
190584
  * exactly one content kind; commit states carry no `actor` and no `on`;
190615
190585
  * non-commit states carry an `actor`; every `modes:` entry declares at least
190616
- * one non-blank `format`/`validate` command; a DECLARED `stateDir` template is
190617
- * non-blank (`validateStateDir` — the value-shape rule, `stateDirError`, is a
190618
- * runtime/edge concern, not checked here); every state is reachable from an
190619
- * entry root (`def.entries` — `default` plus every `entries.manual` state) by
190620
- * walking `on` targets and `retry.otherwise` redirects (checked only when
190621
- * `validateEntries` itself found no problem — see `validateReachability`).
190586
+ * one non-blank `format`/`validate` command; a compiled `file:` sits under
190587
+ * `STATE_DIR` (`validateFileUnderStateDir` — the compiler's `stateFile` kind
190588
+ * already guarantees this for anything it compiles, so this only fires for a
190589
+ * hand-built definition that skipped the compiler); every state is reachable
190590
+ * from an entry root (`def.entries` — `default` plus every `entries.manual`
190591
+ * state) by walking `on` targets and `retry.otherwise` redirects (checked
190592
+ * only when `validateEntries` itself found no problem — see
190593
+ * `validateReachability`).
190622
190594
  *
190623
190595
  * Every OTHER per-field rule (`on`/`retry` targets resolving, `retry.max`
190624
190596
  * being a non-negative integer, a text field's non-empty/commit-forbidden
@@ -190638,13 +190610,99 @@ const validateDefinition = (def) => {
190638
190610
  return [
190639
190611
  ...entriesErrors,
190640
190612
  ...validateModes(def),
190641
- ...validateStateDir(def),
190642
190613
  ...names.flatMap((name) => validateState(def, name, names)),
190643
190614
  ...entriesErrors.length === 0 ? validateReachability(def, names) : []
190644
190615
  ];
190645
190616
  };
190646
190617
  //#endregion
190647
190618
  //#region src/Machines.ts
190619
+ /**
190620
+ * Compile-time MACHINE FLATTENING — a raw `entry:`/`machines:` config in, a
190621
+ * flat `{ states, entries, tree }` out. This is the pure compile-time pass
190622
+ * that lets a workflow be AUTHORED as a tree of reusable, parameterized
190623
+ * "machines" (a gate/loop written once, instantiated several times with
190624
+ * different bindings — dedup — or a complex cluster grouped under one name
190625
+ * for source comprehension — encapsulation) while the rest of the compiler
190626
+ * (`src/PatternConfig.ts`) and the pure engine (`src/PatternMachine.ts`) only
190627
+ * ever see ordinary qualified states. No git, no filesystem, no Effect, no
190628
+ * runtime footprint — every export here is a plain function of its
190629
+ * arguments, and every finding is a pushed string on the shared `errors`
190630
+ * accumulator the caller threads through, never a throw.
190631
+ *
190632
+ * ## The raw shape
190633
+ *
190634
+ * ```yaml
190635
+ * entry:
190636
+ * default: <machine name> # which machine is the ROOT instance
190637
+ * machines:
190638
+ * <name>:
190639
+ * params: [<param>, ...]? # advisory only — documents which $params a caller may bind
190640
+ * entry: <local or ref key> # this machine's OWN default local, resolved recursively
190641
+ * states:
190642
+ * <local>: <StateDef> # an ordinary state
190643
+ * <local>: { machine: <name>, with: { ... } } # a REFERENCE — instantiates <name> as a child
190644
+ * ```
190645
+ *
190646
+ * ## Two passes over one `Instance` tree
190647
+ *
190648
+ * **Pass 1 (instantiate)** walks depth-first from `entry.default`'s machine
190649
+ * at the root (`path: ""`), turning every local `machine:` reference into a
190650
+ * child `Instance` at `parent.path + "." + local` — recording each local as
190651
+ * either a `state` or a `ref` (for later target resolution) along the way.
190652
+ * Guards push an error and skip only the offending subtree/local: a machine
190653
+ * reference cycle, an unknown `machine:` name, or a local name containing
190654
+ * `.` (which would be indistinguishable from a qualified reference path).
190655
+ * Any `machines:` entry never targeted by a reference (or the root) is
190656
+ * reported as unreferenced once both passes complete.
190657
+ *
190658
+ * **Pass 2 (emit)** clones each instance's own states into the flat
190659
+ * `states` map, keyed by qualified name (`qualify`), substituting `$param`
190660
+ * placeholders (whole-value only — a field whose ENTIRE value is `$name`,
190661
+ * never a substring, so bash `$var`/Eta `<%= %>` inside a `script:` survive
190662
+ * untouched) and rewriting every `on` target and `retry.otherwise` through
190663
+ * the resolver below.
190664
+ *
190665
+ * ## Bindings carry their scope
190666
+ *
190667
+ * A bound value cannot be resolved eagerly at the reference site (it is not
190668
+ * yet known whether it will be used as a TARGET, needing scope-aware
190669
+ * resolution, or spliced whole into a `describe:`/`prompt:` sentence, needing
190670
+ * none) and cannot be resolved lazily purely in the callee's own scope (it
190671
+ * was written in the CALLER's). So a `Binding` is a value plus the instance
190672
+ * path it was written in (`scope`) — see `Binding`/`Bindings`. Building a
190673
+ * child's bindings at a reference site substitutes each `with:` value
190674
+ * against the caller's OWN bindings first: if it names another binding, that
190675
+ * binding is passed down WHOLE, scope intact, which is what makes a param
190676
+ * threaded grandparent → parent → child resolve in the grandparent's
190677
+ * namespace rather than the parent's or child's.
190678
+ *
190679
+ * ## The resolver
190680
+ *
190681
+ * Resolving a target string written in instance `I` (`resolveCore`):
190682
+ *
190683
+ * 1. `$param` → look up `I.bindings[name]`; missing or non-string is an
190684
+ * "unbound param" finding. Otherwise resolve `binding.value` starting
190685
+ * over at `binding.scope` (recursing through however many bindings were
190686
+ * chained).
190687
+ * 2. Split on the first `.`. The leading segment must name a local of `I`:
190688
+ * a state with no remainder qualifies directly; a reference with no
190689
+ * remainder resolves THAT machine's own `entry:` recursively (so a chain
190690
+ * of references collapses to a real state); a reference with a remainder
190691
+ * recurses into the child using the remainder; anything else (not a
190692
+ * local, or a remainder against a state) is the sideways/upward refusal.
190693
+ *
190694
+ * The root machine's own `entry:` (which becomes `FlattenedWorkflow.entries.
190695
+ * default`) runs through the exact same resolver seeded at the ROOT instance
190696
+ * — accepting either a bare state path or an instance/reference-key path
190697
+ * uniformly — but surfaces a distinctly worded finding (`"entry.default"
190698
+ * names "y", which is not a state or machine reference`) since there is no
190699
+ * single owning state to blame.
190700
+ *
190701
+ * The `MachineNode` tree (`FlattenedWorkflow.tree`) is a plain projection of
190702
+ * the `Instance` tree Pass 1 built — which concrete states each instance
190703
+ * directly owns — kept around for a future visualizer (Package 05's
190704
+ * `Visualize.ts`) to render; this module only produces the data.
190705
+ */
190648
190706
  const isPlainObject$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
190649
190707
  const describeType$1 = (v) => {
190650
190708
  if (v === null) return "null";
@@ -190883,29 +190941,34 @@ const emitState = (stateRaw, instance, localName, instancesByPath, machinesRaw,
190883
190941
  return out;
190884
190942
  };
190885
190943
  /**
190886
- * Resolve an instance's own `model` value, substituting a whole-value
190944
+ * Resolve every one of an instance's own machine-authored field values
190945
+ * (`MACHINE_FIELD_ENTRIES` — `model`, `system`), substituting a whole-value
190887
190946
  * `$param` at the reference site. A whole-value `$param` that resolves to the
190888
190947
  * empty string compiles away to "field absent" (mirrors the same rule in
190889
- * `emitState`) — an instance that doesn't bind a model for this machine
190890
- * should not stamp `model: ""`.
190948
+ * `emitState`) — an instance that doesn't bind a value for this machine
190949
+ * should not stamp e.g. `model: ""`.
190891
190950
  */
190892
- const resolveInstanceModel = (rawMachine, instance, errors) => {
190893
- const rawModel = rawMachine["model"];
190894
- if (rawModel === void 0) return void 0;
190895
- const substituted = substituteScalar(rawModel, instance, `machines.${instance.machine}`, errors);
190896
- if (typeof rawModel === "string" && PARAM_REF.test(rawModel) && substituted === "") return;
190897
- return substituted;
190951
+ const resolveInstanceMachineFields = (rawMachine, instance, errors) => {
190952
+ const resolved = {};
190953
+ for (const [key] of MACHINE_FIELD_ENTRIES) {
190954
+ const raw = rawMachine[key];
190955
+ if (raw === void 0) continue;
190956
+ const substituted = substituteScalar(raw, instance, `machines.${instance.machine}`, errors);
190957
+ if (typeof raw === "string" && PARAM_REF.test(raw) && substituted === "") continue;
190958
+ resolved[key] = substituted;
190959
+ }
190960
+ return resolved;
190898
190961
  };
190899
190962
  /** Walk the instance tree, emitting every instance's own direct states into `states` (qualified). */
190900
190963
  const emitTree = (instance, machinesRaw, states, scopes, instancesByPath, errors) => {
190901
190964
  const rawMachine = machinesRaw[instance.machine];
190902
190965
  const statesRaw = rawMachine["states"];
190903
- const resolvedModel = resolveInstanceModel(rawMachine, instance, errors);
190966
+ const resolvedFields = resolveInstanceMachineFields(rawMachine, instance, errors);
190904
190967
  for (const [localName, local] of instance.locals) {
190905
190968
  if (local.kind !== "state") continue;
190906
190969
  const qualified = qualify(instance.path, localName);
190907
190970
  const emitted = emitState(statesRaw[localName], instance, localName, instancesByPath, machinesRaw, errors);
190908
- if (resolvedModel !== void 0 && typeof emitted["prompt"] === "string") emitted["model"] = resolvedModel;
190971
+ if (typeof emitted["prompt"] === "string") Object.assign(emitted, resolvedFields);
190909
190972
  states[qualified] = emitted;
190910
190973
  scopes[qualified] = instance.path;
190911
190974
  }
@@ -191643,13 +191706,13 @@ const isSeededValidateCommand = (mode, command) => command === seededValidateCom
191643
191706
  * <name>:
191644
191707
  * format: <shell command> # at least one of format/validate
191645
191708
  * validate: <shell command>
191646
- * stateDir: <string>? # optional — an Eta template naming where this workflow keeps its own plumbing; defaults to ".gtd" (see PatternMachine.stateDirOf)
191647
191709
  * entry:
191648
191710
  * default: <machine name> # which machine is the ROOT instance
191649
191711
  * machines:
191650
191712
  * <name>:
191651
191713
  * params: [<param>, ...]? # advisory only — documents which $params a caller may bind
191652
191714
  * model: <string>? # optional, opaque harness hint — stamped onto every one of THIS machine's own `prompt` states; a state may NOT declare its own
191715
+ * system: <string>? # optional, machine-level only — the agent harness system prompt stamped onto every one of THIS machine's own `prompt` states; a "./" or "../" value is inlined from the declaring config file's directory (package 02); a state may NOT declare its own
191653
191716
  * entry: <local or ref key> # this machine's OWN default local, resolved recursively
191654
191717
  * states:
191655
191718
  * <local>: # an ordinary state
@@ -191662,7 +191725,7 @@ const isSeededValidateCommand = (mode, command) => command === seededValidateCom
191662
191725
  * max: <number>
191663
191726
  * otherwise: <targetState>
191664
191727
  * label: <string> # optional, opaque display name — never on a commit state
191665
- * file: <string> # optional, an Eta template naming the state's steering file — never on a commit state
191728
+ * file: <string> # optional, an Eta template naming the state's steering file RELATIVE to ".gtd/" (the compiler prepends it automatically) — never on a commit state
191666
191729
  * mode: <modeName> # optional, requires "file" — a built-in (qa/review) or a `modes:` entry; never on a commit state
191667
191730
  * entry: true # optional — an EXTRA reachability root (WorkflowEntries.manual), enterable via `gtd --entry <this state's qualified name>`; distinct from the top-level `entry:` key above
191668
191731
  * <local>: { machine: <name>, with: { ... } } # a REFERENCE — instantiates <name> as a child, see src/Machines.ts
@@ -191705,20 +191768,15 @@ const isSeededValidateCommand = (mode, command) => command === seededValidateCom
191705
191768
  * `isSeededValidateCommand` lets the edge (`src/SteeringMode.ts`) tell gtd's
191706
191769
  * own seeding apart from a genuine user override.
191707
191770
  *
191708
- * ## `stateDir:` — where this workflow keeps its own plumbing
191709
- *
191710
- * A sibling `stateDir:` key INSIDE the `workflow:` value declares the raw Eta
191711
- * template source for gtd's own scratch/bookkeeping directory (see
191712
- * `PatternMachine.WorkflowDefinition.stateDir`/`stateDirOf`) — a
191713
- * DEFINITION-level declaration, not a var, because the value must reach
191714
- * `enforceStepGuards`'s `hasCodeChange` (`src/StepGuards.ts`), computed once
191715
- * per call before any one guard runs, rather than any guard reaching into
191716
- * `it.vars` itself (a blessed-config-key shape this repo forbids). Absent
191717
- * compiles to `undefined`, so `stateDirOf`'s `.gtd` default applies — a
191718
- * workflow declaring nothing behaves exactly as before this key existed. The
191719
- * bundled template renders this from its own ordinary `vars.stateDir` (the
191720
- * knob a user actually overrides) — this compiler never renders it itself,
191721
- * the same discipline as a state's own `file:`.
191771
+ * ## `file:` — resolved under `.gtd/` automatically
191772
+ *
191773
+ * A state's `file:` names a path RELATIVE to `PatternMachine.STATE_DIR`
191774
+ * (`.gtd`) — the `stateFile` field kind (`compileStateFile`, below) prepends
191775
+ * the directory after rejecting a `..` segment, an absolute path, or a
191776
+ * declared `.gtd/` prefix (each a load-time error, never silently rewritten).
191777
+ * There is no per-workflow relocation knob any more: `.gtd` is a fixed
191778
+ * literal every consumer (the review window's revert pathspec, the step
191779
+ * guards' code-vs-plumbing test) agrees on.
191722
191780
  *
191723
191781
  * ## File references
191724
191782
  *
@@ -191844,37 +191902,19 @@ const mergeModes = (base, override) => {
191844
191902
  };
191845
191903
  return merged;
191846
191904
  };
191847
- /**
191848
- * Compile the `stateDir:` key — the raw Eta template source for where this
191849
- * workflow keeps its own plumbing (see `PatternMachine.WorkflowDefinition.stateDir`).
191850
- * An absent key compiles to `undefined` — the definition then carries none,
191851
- * and `stateDirOf`'s `.gtd` default applies. A non-string value pushes one
191852
- * load error and drops the value, never guessed at. Never rendered here —
191853
- * the pure engine carries the string verbatim, the same discipline as a
191854
- * state's own `file:`.
191855
- */
191856
- const compileStateDir = (raw, errors) => {
191857
- if (raw === void 0) return void 0;
191858
- if (typeof raw !== "string") {
191859
- errors.push(`"stateDir" must be a string, got ${describeType(raw)}`);
191860
- return;
191861
- }
191862
- return raw;
191863
- };
191864
191905
  /** Every state property a flattened state may carry — `STATE_FIELDS`'s own key set, in table order. */
191865
191906
  const KNOWN_STATE_KEYS = new Set(Object.keys(STATE_FIELDS));
191866
191907
  const KNOWN_TOP_KEYS = /* @__PURE__ */ new Set([
191867
191908
  "entry",
191868
191909
  "machines",
191869
191910
  "vars",
191870
- "modes",
191871
- "stateDir"
191911
+ "modes"
191872
191912
  ]);
191873
191913
  const KNOWN_MACHINE_KEYS = /* @__PURE__ */ new Set([
191874
191914
  "params",
191875
191915
  "entry",
191876
191916
  "states",
191877
- "model"
191917
+ ...MACHINE_FIELD_ENTRIES.map(([key]) => key)
191878
191918
  ]);
191879
191919
  const KNOWN_REF_KEYS = /* @__PURE__ */ new Set(["machine", "with"]);
191880
191920
  /** A state-level key removed by the `entry:`/`machines:` rewrite (or, for `memory`, by the machine-scoped-memory restructure), naming its replacement so a stale config's error points somewhere useful instead of a bare "unknown key". The state-level `model` case is instead caught pre-flatten by `LEGACY_AUTHORED_STATE_KEY_HINTS`/`validateMachineStateKeys`, because by the time a state reaches `KNOWN_STATE_KEYS`/`compileState` its `model` may be a legitimately machine-stamped key. */
@@ -191970,32 +192010,38 @@ const validateMachineStateKeys = (machineName, machineRaw, errors) => {
191970
192010
  }
191971
192011
  };
191972
192012
  /**
191973
- * The machine-level `model:` field — the ONLY place a model may be authored
191974
- * (`validateMachineStateKeys`, above, rejects the state-level form): a
191975
- * non-empty string, else a load error naming the machine. Validates the value
191976
- * a machine stamps onto its own `prompt` states (`src/Machines.ts`'s
191977
- * `resolveInstanceModel`/`emitTree`); the per-state `compileModel` (below) is
191978
- * then only the type guard over that STAMPED value.
192013
+ * Every machine-authored field (`MACHINE_FIELD_ENTRIES`) — the ONLY place any
192014
+ * of them may be authored (`validateMachineStateKeys`, above, rejects the
192015
+ * state-level form): a non-empty string, else a load error naming the
192016
+ * machine. Validates the value a machine stamps onto its own `prompt` states
192017
+ * (`src/Machines.ts`'s `resolveInstanceMachineFields`/`emitTree`); the
192018
+ * per-state `compileText` (below) is then only the type guard
192019
+ * over that STAMPED value.
191979
192020
  */
191980
- const compileMachineModel = (machineName, machineRaw, errors) => {
191981
- if (machineRaw.model === void 0) return;
191982
- if (typeof machineRaw.model !== "string" || machineRaw.model === "") errors.push(`machines.${machineName}: "model" must be a non-empty string`);
192021
+ const validateMachineFieldValues = (machineName, machineRaw, errors) => {
192022
+ for (const [key, spec] of MACHINE_FIELD_ENTRIES) {
192023
+ const value = machineRaw[key];
192024
+ if (value === void 0) continue;
192025
+ if (spec.nonEmpty === true && (typeof value !== "string" || value === "")) errors.push(`machines.${machineName}: "${key}" must be a non-empty string`);
192026
+ }
191983
192027
  };
191984
- /** Does `machineRaw` declare at least one of its OWN (non-reference) states with content kind `prompt`? Mirrors exactly which states `src/Machines.ts` stamps a machine-level `model` onto — a reference local's states belong to the CHILD machine, never this one. */
192028
+ /** Does `machineRaw` declare at least one of its OWN (non-reference) states with content kind `prompt`? Mirrors exactly which states `src/Machines.ts` stamps a machine-authored field onto — a reference local's states belong to the CHILD machine, never this one. */
191985
192029
  const machineHasPromptState = (machineRaw) => {
191986
192030
  const statesRaw = machineRaw["states"];
191987
192031
  if (!isPlainObject$1(statesRaw)) return false;
191988
192032
  return Object.values(statesRaw).some((def) => isPlainObject$1(def) && !isRefRaw(def) && typeof def["prompt"] === "string");
191989
192033
  };
191990
192034
  /**
191991
- * A machine declaring `model:` with no `prompt`-content state anywhere among
191992
- * its own states is a load error, not a silent no-op — see this module's
191993
- * "BOUNDARY CORRECTION" note in package 02: the model would never land on any
191994
- * emitted state, so the declaration does nothing.
192035
+ * A machine declaring a machine-authored field with no `prompt`-content state
192036
+ * anywhere among its own states is a load error, not a silent no-op — see
192037
+ * this module's "BOUNDARY CORRECTION" note in package 02: the field would
192038
+ * never land on any emitted state, so the declaration does nothing.
191995
192039
  */
191996
- const validateMachineModelHasPromptState = (machineName, machineRaw, errors) => {
191997
- if (machineRaw.model === void 0) return;
191998
- if (!machineHasPromptState(machineRaw)) errors.push(`machine "${machineName}": declares "model" but has no "prompt" state — this would never take effect`);
192040
+ const validateMachineFieldsTakeEffect = (machineName, machineRaw, errors) => {
192041
+ for (const [key] of MACHINE_FIELD_ENTRIES) {
192042
+ if (machineRaw[key] === void 0) continue;
192043
+ if (!machineHasPromptState(machineRaw)) errors.push(`machine "${machineName}": declares "${key}" but has no "prompt" state — this would never take effect`);
192044
+ }
191999
192045
  };
192000
192046
  const validateMachinesShape = (raw, errors) => {
192001
192047
  if (raw === void 0) return;
@@ -192009,8 +192055,8 @@ const validateMachinesShape = (raw, errors) => {
192009
192055
  if (unknownMachineKeys.length > 0) errors.push(`machine "${machineName}": unknown key(s) ${unknownMachineKeys.join(", ")}`);
192010
192056
  validateMachineRefs(machineName, machineRaw, errors);
192011
192057
  validateMachineStateKeys(machineName, machineRaw, errors);
192012
- compileMachineModel(machineName, machineRaw, errors);
192013
- validateMachineModelHasPromptState(machineName, machineRaw, errors);
192058
+ validateMachineFieldValues(machineName, machineRaw, errors);
192059
+ validateMachineFieldsTakeEffect(machineName, machineRaw, errors);
192014
192060
  }
192015
192061
  };
192016
192062
  const formatErrors = (errors) => `workflow config:\n${errors.map((e) => ` - ${e}`).join("\n")}`;
@@ -192042,6 +192088,16 @@ const resolveContent = (value, configDir, where, errors, fileRefs = nodeFileRefR
192042
192088
  /** A local is a REFERENCE iff its raw value carries a `machine` key — the same predicate `src/Machines.ts` uses. */
192043
192089
  const isRefRaw = (v) => isPlainObject$1(v) && typeof v["machine"] === "string";
192044
192090
  /**
192091
+ * Machine-authored fields whose `./`/`../` value is a file reference
192092
+ * (`FieldSpec.fileRef`), derived from `MACHINE_FIELD_ENTRIES` rather than
192093
+ * hand-listed — the declarative escape hatch for a field whose behaviour
192094
+ * doesn't fit `nonEmpty`/`commit`/`requires`. Deliberately NOT every
192095
+ * machine-authored field: `model` is an opaque harness hint today, and
192096
+ * looping it here would turn `model: ./tiers/fast.txt` into a file read — a
192097
+ * load error for a value that used to work.
192098
+ */
192099
+ const MACHINE_FILE_REF_FIELDS = MACHINE_FIELD_ENTRIES.filter(([, spec]) => spec.fileRef === true).map(([key]) => key);
192100
+ /**
192045
192101
  * Inline every `./`/`../` content file reference in ONE raw `workflow:` value
192046
192102
  * against `configDir` — the directory of the `.gtdrc` that DECLARED it — and
192047
192103
  * return a copy with those references replaced by the referenced file's text.
@@ -192075,17 +192131,31 @@ const inlineStateFileRefs = (def, machineName, local, configDir, errors, fileRef
192075
192131
  }
192076
192132
  return next;
192077
192133
  };
192078
- /** Inline every state's content file references for one machine; a malformed machine/states shape passes through untouched. */
192134
+ /**
192135
+ * Inline a machine's own file-reference fields (`MACHINE_FILE_REF_FIELDS`,
192136
+ * e.g. `system: ./persona.md`) plus every state's content file references;
192137
+ * a malformed machine shape passes through untouched. The machine-level pass
192138
+ * runs FIRST and unconditionally — only the states loop is gated on a
192139
+ * well-formed `states:` — so a machine with a malformed `states:` still gets
192140
+ * its own `system:` reference resolved (and its missing-file error reported)
192141
+ * rather than skipping inlining entirely.
192142
+ */
192079
192143
  const inlineMachineFileRefs = (machineRaw, machineName, configDir, errors, fileRefs = nodeFileRefReader) => {
192080
192144
  if (!isPlainObject$1(machineRaw)) return machineRaw;
192145
+ const next = { ...machineRaw };
192146
+ for (const key of MACHINE_FILE_REF_FIELDS) {
192147
+ const value = machineRaw[key];
192148
+ if (typeof value !== "string" || !isFileReference(value)) continue;
192149
+ const resolved = resolveContent(value, configDir, `machine "${machineName}" (${key})`, errors, fileRefs);
192150
+ if (resolved !== void 0) next[key] = resolved;
192151
+ }
192081
192152
  const rawStates = machineRaw["states"];
192082
- if (!isPlainObject$1(rawStates)) return machineRaw;
192083
- const states = {};
192084
- for (const [local, def] of Object.entries(rawStates)) states[local] = isPlainObject$1(def) ? inlineStateFileRefs(def, machineName, local, configDir, errors, fileRefs) : def;
192085
- return {
192086
- ...machineRaw,
192087
- states
192088
- };
192153
+ if (isPlainObject$1(rawStates)) {
192154
+ const states = {};
192155
+ for (const [local, def] of Object.entries(rawStates)) states[local] = isPlainObject$1(def) ? inlineStateFileRefs(def, machineName, local, configDir, errors, fileRefs) : def;
192156
+ next["states"] = states;
192157
+ }
192158
+ return next;
192089
192159
  };
192090
192160
  const inlineWorkflowFileRefs = (rawWorkflow, configDir, errors, fileRefs = nodeFileRefReader) => {
192091
192161
  if (!isPlainObject$1(rawWorkflow)) return rawWorkflow;
@@ -192208,6 +192278,43 @@ const compileText = (raw, key, name, errors) => {
192208
192278
  return value;
192209
192279
  };
192210
192280
  /**
192281
+ * `file:` — a plain string (`compileText`'s own shape), then prepended under
192282
+ * `STATE_DIR` after three load-time rejections: a `..` segment (an escape), a
192283
+ * leading `/` (an absolute path), or an already-declared `STATE_DIR` prefix
192284
+ * (what would otherwise silently double up into `.gtd/.gtd/...`). None of
192285
+ * these is a rewrite — a leading `./` or a doubled `//` is neither accepted
192286
+ * nor normalized, since reintroducing canonical-spelling machinery for `file:`
192287
+ * buys nothing when removing it is the point. A blank value is left alone
192288
+ * (returned as `""`, not prepended) so the field's own generic `nonEmpty` rule
192289
+ * (`STATE_FIELDS.file`, walked by `validateFieldRules`) still catches it —
192290
+ * prepending onto blank would hide the blank behind a non-empty
192291
+ * `"${STATE_DIR}/"` string. `undefined` (absent, or the type mismatch
192292
+ * `compileText` already reported) short-circuits with nothing left to check.
192293
+ *
192294
+ * A templated `file:` (e.g. `file: <%= it.vars.x %>`) is checked against its
192295
+ * SOURCE, not its rendered form — a var supplying `../REVIEW.md` at runtime
192296
+ * passes every one of these checks and renders outside `.gtd/` at the edge.
192297
+ * Accepted, not guarded: see this module's own `file:` section for the
192298
+ * rationale.
192299
+ */
192300
+ const compileStateFile = (raw, key, name, ctx) => {
192301
+ const value = compileText(raw, key, name, ctx.errors);
192302
+ if (value === void 0 || value === "") return value;
192303
+ if (value.split("/").includes("..")) {
192304
+ ctx.errors.push(`state "${name}": "${key}" must not contain a ".." segment (got "${value}")`);
192305
+ return;
192306
+ }
192307
+ if (value.startsWith("/")) {
192308
+ ctx.errors.push(`state "${name}": "${key}" must not be an absolute path (a leading "/") (got "${value}")`);
192309
+ return;
192310
+ }
192311
+ if (value === ".gtd" || value.startsWith(`.gtd/`)) {
192312
+ ctx.errors.push(`state "${name}": "${key}" is resolved under "${STATE_DIR}/" automatically — drop the "${STATE_DIR}/" prefix (got "${value}")`);
192313
+ return;
192314
+ }
192315
+ return `${STATE_DIR}/${value}`;
192316
+ };
192317
+ /**
192211
192318
  * A boolean state flag (`reviewWindow`/`requireProgress`/`answerGate`/
192212
192319
  * `entry`): `true` only when the raw value is the literal `true`; a
192213
192320
  * non-boolean is a config error; `false` (or absent) compiles away to
@@ -192269,6 +192376,7 @@ const compileContentRef = (raw, key, name, ctx) => {
192269
192376
  const COMPILE = {
192270
192377
  actor: (raw, key, name, ctx) => compileText(raw, key, name, ctx.errors),
192271
192378
  text: (raw, key, name, ctx) => compileText(raw, key, name, ctx.errors),
192379
+ stateFile: compileStateFile,
192272
192380
  mode: (raw, key, name, ctx) => compileText(raw, key, name, ctx.errors),
192273
192381
  content: compileContentRef,
192274
192382
  flag: (raw, key, name, ctx) => compileBooleanFlag(raw, key, name, ctx.errors),
@@ -192379,7 +192487,6 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
192379
192487
  const vars = compileVarsMap(raw.vars, errors);
192380
192488
  const seeded = Object.fromEntries(builtInModeNames().map((name) => [name, { validate: seededValidateCommand(name) }]));
192381
192489
  const modes = mergeModes(mergeModes(seeded, compileModesMap(raw.modes, errors)), rcModes);
192382
- const stateDir = compileStateDir(raw.stateDir, errors);
192383
192490
  validateMachinesShape(raw.machines, errors);
192384
192491
  const flattened = flattenMachines(raw, errors);
192385
192492
  if (flattened.entries === void 0) throw new Error(formatErrors(errors));
@@ -192396,8 +192503,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
192396
192503
  default: flattened.entries.default,
192397
192504
  manual
192398
192505
  },
192399
- modes,
192400
- ...stateDir !== void 0 ? { stateDir } : {}
192506
+ modes
192401
192507
  };
192402
192508
  assertScopesCoverStates(Object.keys(states), flattened.scopes, errors);
192403
192509
  const definitionErrors = validateDefinition(definition);
@@ -192412,7 +192518,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
192412
192518
  };
192413
192519
  //#endregion
192414
192520
  //#region src/workflows/unified.yaml
192415
- 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 (no REVIEW.md note, no code edit)\n# -> 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# One dead end never commits: the review-doc guard (src/StepGuards.ts) REFUSES\n# a deleted REVIEW.md — restore it, or leave a note (or edit code) to request\n# changes. A tick only records that a hunk was read; it never gates a land.\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 sh\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 POSIX sh.\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 sh\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 POSIX sh.\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 sh\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 POSIX sh.\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 file pointer per hunk (checkboxes are\n for the human to tick, not you; every path is `./`-relative, with an\n optional `#line`). The pointer's OWN line carries nothing but the\n pointer — no note, no dash. Its explanation goes on the line(s)\n beneath it instead:\n\n - [ ] ./path/to/file.ts#42\n what this hunk does, with room to run to several lines\n\n Text after the pointer token on the pointer's own line is a\n validation error, not just a style nit — an older-format doc like\n `- [ ] ./path/to/file.ts#42 — note` is UNLANDABLE.\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\" — it never gates a land, whatever the\n # boxes say. The one dead end that CAN'T be told apart by a file-pattern\n # edge is a deleted REVIEW.md, so the review-doc guard\n # (`src/StepGuards.ts`) catches it BEFORE the step commits and refuses.\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** — leave no comment — no note in\n `<%= it.vars.reviewFile %>`, no code edit — to collapse the process into\n one commit (**build.squashing**), whatever the boxes say.\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 Deleting `<%= it.vars.reviewFile %>` is refused.\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 sh\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 (`await-review`'s single\n # `\"* **\": deciding` edge means EVERY human step there lands here, so\n # `deciding` is entered exactly once per round — nothing else commits in\n # 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 POSIX sh.\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 sh\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 POSIX sh.\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 # Package file names are gtd-authored (architecture.decompose writes\n # NN-name.md), never containing whitespace/newlines that would break\n # ls | head.\n # shellcheck disable=SC2012\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 sh\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 sh\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 sh\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";
192521
+ 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 `resolveInstanceMachineFields`), 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 `REQUIREMENTS.md` 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# `REQUIREMENTS.md` in full rather than resuming design's conversation),\n# develops the *how* for each settled concern, and — now that it knows each\n# concern's file footprint — may re-merge concerns whose footprints coincide\n# (never split), recording every merge under its own `## Merged Concerns`\n# heading and raising only TECHNICAL open questions — routed through the\n# same shared `questionGate` shape (`architecture.gate`) over\n# `ARCHITECTURE.md`. Once those are settled, `architecture.decompose` is a\n# purely mechanical write-out: one package file per concern, in the settled\n# — possibly coarsened — order, with no merge/split judgement of its own.\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 (no REVIEW.md note, no code edit)\n# -> 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 `.gtd/`) -> `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# One dead end never commits: the review-doc guard (src/StepGuards.ts) REFUSES\n# a deleted REVIEW.md — restore it, or leave a note (or edit code) to request\n# changes. A tick only records that a hunk was read; it never gates a land.\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#\nvars:\n testCommand: npm test\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\n # The bundled default PERSONAS — one per prompt-bearing machine below. Each\n # is a machine's `system:` value (see src/StateFields.ts's `system` entry):\n # emitted verbatim as that machine's agent-CLI system prompt, stamped onto\n # every one of its own `prompt` states the same way `model:` is. Like every\n # other var, each is overridable via a top-level `.gtdrc` `vars:` key (e.g.\n # `vars: { builderPersona: \"...\" }`) or a `GTD_<NAME>`-style environment\n # variable — nothing below is privileged.\n #\n # `--system-prompt` REPLACES a harness's entire default system prompt; it\n # does not append to it, and it drops the harness's own injected cwd/env/\n # memory-path/git-status block. Each persona is therefore written as a\n # complete, self-contained system prompt: it states the role, gives tool-use\n # conduct (act, don't ask permission — this runs unattended), and tells the\n # agent to orient itself with real commands (there is no injected status\n # block) and to go inspect the commit range or path its turn's own prompt\n # names, rather than expect a diff pasted into the conversation.\n designPersona: |-\n You are the product-facing planning voice in an autonomous build pipeline\n called gtd. Your job is to turn a raw, half-formed sketch — a hand-edit to\n real code, a scratch note, or both — into an ordered, classified list of\n concerns a build can execute, and to carry a running conversation with a\n human about the product decisions that list depends on. You are read by\n that human, so write plainly and decisively; you are not read by a parser\n except where a state's own instructions say otherwise.\n\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. Nobody is watching this turn\n interactively: there is no one to grant permission, confirm a destructive\n command, or approve a plan before you act. Investigate with real commands\n (`git log`, `git show`, `ls`, `cat`) rather than assuming what a file or a\n commit contains, then act.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. Nothing has told you the\n current branch, the recent commit history, or whether the tree is clean —\n there is no injected status block. Run `git status`, `git log`, `ls` and\n similar yourself before you do anything else; that is the normal first\n move of this turn, not an extra step.\n\n The message you receive each turn is an instruction that names commits,\n paths, or file names for you to go inspect yourself — `git show <hash>`,\n `git diff`, reading a file directly — never a rendered diff pasted into\n the conversation. Expect to go look. Across the several turns this\n conversation may span, remember what you already read and settled in an\n earlier turn rather than re-deriving it from a cold read every time —\n unless the turn's own instructions tell you this is a fresh, cold pass.\n architectPersona: |-\n You are the technical planning voice in an autonomous build pipeline\n called gtd. You take over once the product-facing concerns are settled,\n reading them cold — you do not carry over the product conversation, only\n its written, settled output. Your job is to work out the technical *how*\n for each settled concern — file and module structure, data models,\n library and tech-stack choices, error-handling strategy — raise the\n technical open questions a human still needs to decide, and once every\n question is answered, write one package spec file per concern with no\n further judgement call: the grouping and ordering were already decided\n upstream, and this final step is a mechanical write-out of a plan you\n already converged on.\n\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. This runs unattended: there is\n no one present to confirm a step or approve a draft before you commit to\n it. Investigate with real commands rather than assuming a file's contents\n or a decision's status, then act on what you find.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block telling you the branch, recent history, or tree state — run\n `git status`, `git log`, `ls` and the like yourself before doing anything\n else.\n\n The message you receive each turn names files and, where relevant, commit\n ranges for you to go read yourself — never a diff or a plan rendered\n inline. Go look before you decide anything. When a later turn in this\n same conversation asks you to mechanically write out package files, work\n from the plan you already settled rather than re-deriving it, unless\n instructed to read it fresh.\n reviewerPersona: |-\n You are the independent reviewing mind in an autonomous build pipeline\n called gtd — deliberately a separate identity from whichever agent wrote\n the code under review, so you bring no attachment to how it got built.\n Your job splits into two distinct turns: first, writing a structured\n review document that groups a diff into reviewable chunks for a human to\n read through and comment on; later, reading back a round of that human's\n feedback — a note, a hand-edit — and classifying whether it is genuinely\n actionable or just an approving remark, without fixing anything yourself\n either way. You judge and you classify; you never build.\n\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. This runs unattended, so act\n on what you find rather than pausing for confirmation. Investigate with\n real commands — `git log`, `git show`, `git diff` — rather than assuming\n what changed.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block: no cwd banner, no environment summary, no memory-file list,\n no git-status readout. Run `git status`, `git log`, `ls` yourself first.\n\n The message you receive each turn names a commit range or a file for you\n to inspect yourself — `git show`, `git diff` against the range it gives\n you — not a diff pasted into the conversation. Go read the actual history\n before you write or judge anything; the range is often wider than the\n single round in front of you, so check what it actually spans rather than\n assuming it matches your last turn.\n specReviewerPersona: |-\n You are the adversarial spec-conformance checker in an autonomous build\n pipeline called gtd, reviewing one freshly-built work package against the\n written spec it was built from. Your only job is to verify: are the\n package's tasks actually done, its acceptance criteria actually met, and\n the code sound and consistent with the rest of the codebase? You write\n concrete, actionable feedback only when something is genuinely wrong —\n and when nothing is, you write nothing at all, because a clean turn from\n you IS the approval. You never fix a problem you find; naming it\n precisely enough for someone else's fix turn to act on is the whole job.\n\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. Nobody is present to confirm a\n verdict interactively, so investigate with real commands and commit to\n what you find rather than hedging or asking for permission to look.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block — no cwd, no environment summary, no git status handed to\n you. Orient yourself with `git status`, `git log`, `ls` and the like\n before judging anything.\n\n The message you receive each turn names a spec file to read and a commit\n range for you to inspect yourself — `git show`, `git diff` — never a\n diff rendered inline, and the range is often wider than the one package\n you are checking, since it can span everything built so far in this\n process. Go look before you approve or object; do not assume the range\n matches only the most recent change.\n builderPersona: |-\n You are the TDD implementer in an autonomous build pipeline called gtd.\n Your job is to build exactly one package's declared scope end to end,\n tests first, and to come back and fix things when redirected: a failing\n check's output, or a reviewer's concrete feedback. Stay strictly inside\n the package in front of you — never wander into another package's files\n or tasks just because you can see them, and never refactor code the\n current task doesn't touch. When fixing, treat the feedback you're given\n as the diagnosis to act on, not a suggestion to second-guess without\n evidence — but if it is simply wrong, say so by discarding it rather than\n forcing a change onto code that doesn't need one.\n\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. This runs unattended: there is\n no one to approve a plan or confirm a command before you run it. Write\n the failing test, watch it fail, then make it pass — investigate the\n actual current state of the code and the suite with real commands rather\n than assuming either.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block — no cwd, no environment info, no memory-file paths, no git\n status. Run `git status`, `git log`, `ls` yourself before you start\n editing anything.\n\n The message you receive each turn names the package spec, the feedback\n file, or the check output for you to read yourself at its actual path —\n never a diff or a rendered summary handed to you inline. Go read the\n file, and where a commit range is named, go inspect it with `git show` or\n `git diff` rather than expecting the change pasted in front of you.\n finisherPersona: |-\n You are the closing identity in an autonomous build pipeline called gtd —\n the last coder this process hands work to before everything it touched is\n squashed into a single commit. Most turns you are handed, your job is to\n write the one conventional-commit message that summarizes the whole\n approved change: subject line, imperative mood, a body that explains why\n and what trade-offs were made, not just what changed. Occasionally you\n are handed a late-breaking failing check instead — fix it, focused and\n minimal, before the process can close. Either way, you are aware that\n everything you touch this turn is about to lose its own commit boundary\n and become part of one final message, so make that message earn its\n place as the historical record.\n\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. This runs unattended, so act\n on what you find rather than waiting for confirmation. Investigate the\n actual commit range and the actual failing output with real commands\n rather than assuming either.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block — no cwd, no environment summary, no git status. Run\n `git status`, `git log`, `ls` yourself before acting.\n\n The message you receive each turn names a commit range or a feedback file\n for you to go inspect yourself — `git show`, `git diff`, reading the file\n directly — never a diff rendered inline into the conversation. Go look\n before you write the commit message or the fix; if you already built the\n range earlier in this same conversation, work from what you already know\n rather than re-deriving it.\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` and `healthGate.check` below); the `on` rules turn the\n # result into \"halt at blocked\" vs \"proceed to $onGreen\".\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 sh\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p .gtd\n # Sweep the raw review capture — no path from `deciding`/`collecting`\n # reaches this check; this is a leftover sweeper for a capture an\n # earlier, abandoned process may have left committed here (see the\n # `D .gtd/REVIEW_RAW.md` rows below). A no-op when it is already gone\n # or never existed.\n rm -f .gtd/REVIEW_RAW.md\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output .gtd/FEEDBACK.md\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > .gtd/FEEDBACK.md\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)\" >> .gtd/FEEDBACK.md\n else\n rm -f .gtd/.check-output\n rm -f .gtd/FEEDBACK.md\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 .gtd/FEEDBACK.md\": blocked\n \"M .gtd/FEEDBACK.md\": blocked\n \"D .gtd/REVIEW_RAW.md\": $onGreen\n \"D .gtd/FEEDBACK.md\": $onGreen\n \"C\": $onGreen\n blocked:\n actor: human\n label: $blockedLabel\n # `file:` is the one field with an implicit prefix: the compiler\n # resolves it under `.gtd/` automatically, so `file: FEEDBACK.md` here\n # sits beside `on: \"A .gtd/FEEDBACK.md\"` above on purpose — every OTHER\n # path in this template (an `on` pattern, a script body, a prompt/\n # message) still spells `.gtd/` in full.\n file: FEEDBACK.md\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 `.gtd/FEEDBACK.md` 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 # See the shared suite check on `entryGate.check` above.\n script: *suiteCheck\n on:\n \"A .gtd/FEEDBACK.md\": $onRed\n \"M .gtd/FEEDBACK.md\": $onRed\n \"* **\": $onGreen\n \"C\": $onGreen\n escalate:\n actor: human\n label: $escalateLabel\n file: FEEDBACK.md\n message: |\n The agent could not get the check to pass after repeated attempts.\n `.gtd/FEEDBACK.md` 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 # REQUIREMENTS.md/ARCHITECTURE.md currently exists (see the script's own\n # comment) with `gtd check qa --open-questions` and stamps/removes\n # `QUESTIONS.md` 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 sh\n # gtd check turn — probe the current phase's steering file for\n # unanswered questions; an answered/empty result removes the marker\n # (.gtd/QUESTIONS.md), an unanswered one stamps it fresh. Design and\n # architecture share this one probe: whichever of\n # .gtd/REQUIREMENTS.md/.gtd/ARCHITECTURE.md exists ON DISK right now\n # IS this phase's steering file — unambiguous because\n # architecture.author deletes .gtd/REQUIREMENTS.md in the same turn\n # it writes .gtd/ARCHITECTURE.md, so exactly one of the two exists\n # here.\n set +e\n mkdir -p .gtd\n file=.gtd/REQUIREMENTS.md\n [ -f \"$file\" ] || file=.gtd/ARCHITECTURE.md\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\" > .gtd/QUESTIONS.md\n # See the shared suite check's cache-buster rationale on\n # `entryGate.check` above.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> .gtd/QUESTIONS.md\n else\n rm -f .gtd/QUESTIONS.md\n fi\n on:\n \"A .gtd/QUESTIONS.md\": answer\n \"M .gtd/QUESTIONS.md\": answer\n \"D .gtd/QUESTIONS.md\": $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 system: <%~ it.vars.designPersona %>\n params: [onArchitecture]\n entry: triage\n states:\n triage:\n actor: agent\n label: Triaging the change\n file: REQUIREMENTS.md\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 `.gtd/REQUIREMENTS.md`; develop it. Do\n not create other files to hold notes or output.\n\n `.gtd/TODO.md` 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, `.gtd/REQUIREMENTS.md` 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\n Two more tests sit beside that green floor, not instead of it.\n Vertical, not horizontal: slice by capability, never by layer. A\n concern that only prepares for a later one — types, scaffolding,\n renames — is folded into the first concern that uses it. Distinct\n acceptance: each concern needs an observable check that fails\n before it and passes after; if you can't name one, merge it into\n its neighbour. Prefer fewer, larger packages — the smallest\n independently valuable change, not the smallest change that\n compiles.\n\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 `.gtd/REQUIREMENTS.md`. 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, `.gtd/REQUIREMENTS.md`\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: `.gtd/REQUIREMENTS.md`\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 `.gtd/REQUIREMENTS.md` uncommitted and finish your\n turn.\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: REQUIREMENTS.md\n onNone: $onArchitecture\n onRevise: triage\n checkLabel: Checking for open questions\n answerLabel: Awaiting your product answers\n message: |\n `.gtd/REQUIREMENTS.md` 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 # `.gtd/REQUIREMENTS.md` cold. Resolves the TECHNICAL open\n # questions, then decomposes the settled concerns into package files.\n # Merge authority lives HERE, in `author` — it is the first state that\n # knows the *how*, so it alone may re-merge concerns whose file footprints\n # coincide (never split); `decompose` remains the mechanical write-out,\n # carrying `author`'s settled — possibly coarsened — grouping over\n # verbatim. ▸ 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 system: <%~ it.vars.architectPersona %>\n params: [onPackages]\n entry: author\n states:\n author:\n actor: agent\n label: Refining the technical plan\n file: ARCHITECTURE.md\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 `.gtd/ARCHITECTURE.md` (write it) and\n `.gtd/REQUIREMENTS.md` (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 `.gtd/REQUIREMENTS.md` (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 `.gtd/ARCHITECTURE.md` 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\n You now know the *how*, so you know each concern's file footprint —\n list each concern's primary paths. Merge concerns whose footprints\n center on the same files into one, unless the later one only\n consumes an interface the earlier one creates: that exception is\n what keeps a genuine build-on-top sequence from collapsing into one\n blob. This authority is to merge only, never to split — granting\n split authority here would work directly against the goal; the\n whole problem is over-granularity. Record every merge under a\n dedicated `## Merged Concerns` heading, carrying both merged requirements\n verbatim so the per-package spec review still covers each one\n independently. A merge raises no open question and stops for no\n human — do not route it to `architecture.gate` for a veto; the\n human still sees every merge when they review the plan, and the\n per-package spec review is the real safety net. Prefer fewer,\n larger packages — the smallest independently valuable change, not\n the smallest change that compiles.\n\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 `.gtd/ARCHITECTURE.md` is written, delete\n `.gtd/REQUIREMENTS.md` — 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: ARCHITECTURE.md\n onNone: decompose\n onRevise: author\n checkLabel: Checking for open questions\n answerLabel: Awaiting your technical answers\n message: |\n `.gtd/ARCHITECTURE.md` 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 `.gtd/packages/` and\n `.gtd/ARCHITECTURE.md`, which it deletes. Do not create other\n files to hold notes or output.\n\n If you wrote `.gtd/ARCHITECTURE.md` earlier in this\n conversation, work from what you already settled; otherwise read it\n (the converged technical plan) yourself. It already lists an\n ORDERED set of concerns with every merge/split judgement\n `architecture.author` made — this turn is a mechanical write-out,\n not a planning one. A `## Merged Concerns` heading there is a\n record of those merges, never a concern of its own: write no\n package file for it.\n\n Write one package file per concern, in the settled order, under\n `.gtd/packages/` (e.g.\n `.gtd/packages/01-name.md`,\n `.gtd/packages/02-name.md`, ...). Each package file\n carries that concern's requirement(s) (so it can be reviewed\n against its spec — both, independently, when the concern was\n merged), 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 in\n `architecture.author`; carry the settled grouping over verbatim.\n No package file may reference any other `.gtd/` file.\n\n Once the package files are written, delete\n `.gtd/ARCHITECTURE.md`. Leave everything uncommitted and\n finish your turn.\n on:\n \"* .gtd/packages/**\": $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 # `REQUIREMENTS.md` 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 system: <%~ it.vars.reviewerPersona %>\n entry: reviewing\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: REVIEW.md\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 `.gtd/REVIEW.md`; write it at exactly\n that path. Do not create other files to hold notes or output.\n\n Write `.gtd/REVIEW.md` 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 file pointer per hunk (checkboxes are\n for the human to tick, not you; every path is `./`-relative, with an\n optional `#line`). The pointer's OWN line carries nothing but the\n pointer — no note, no dash. Its explanation goes on the line(s)\n beneath it instead:\n\n - [ ] ./path/to/file.ts#42\n what this hunk does, with room to run to several lines\n\n Text after the pointer token on the pointer's own line is a\n validation error, not just a style nit — an older-format doc like\n `- [ ] ./path/to/file.ts#42 — note` is UNLANDABLE.\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 `.gtd/REVIEW.md` uncommitted and finish your turn.\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n label: Awaiting your review\n file: REVIEW.md\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\" — it never gates a land, whatever the\n # boxes say. The one dead end that CAN'T be told apart by a file-pattern\n # edge is a deleted REVIEW.md, so the review-doc guard\n # (`src/StepGuards.ts`) catches it BEFORE the step commits and refuses.\n reviewWindow: true\n message: |\n `.gtd/REVIEW.md` 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** — leave no comment — no note in\n `.gtd/REVIEW.md`, no code edit — to collapse the process into\n one commit (**build.squashing**), whatever the boxes say.\n - **Request changes** — leave a comment: a note on a\n `.gtd/REVIEW.md` 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 Deleting `.gtd/REVIEW.md` is refused.\n on:\n \"* **\": deciding\n\n deciding:\n actor: check\n label: Reviewing\n file: REVIEW.md\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 sh\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 (`await-review`'s single\n # `\"* **\": deciding` edge means EVERY human step there lands here, so\n # `deciding` is entered exactly once per round — nothing else commits in\n # 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 `.gtd/` (every gtd steering\n # file, including REVIEW.md itself, lives there). Otherwise it is a\n # clean sign-off. This turn only CAPTURES the raw material into\n # REVIEW_RAW.md (the `collecting` agent then judges whether it's\n # actionable); it does NOT interpret it. Either way, remove REVIEW.md;\n # what the resulting diff MEANS is decided by this state's own `on`\n # rules — the A/M REVIEW_RAW.md rows are declared BEFORE the D\n # REVIEW.md row so a feedback round (which also deletes REVIEW.md) is\n # captured as feedback, not mistaken for a sign-off.\n set +e\n mkdir -p .gtd\n head=$(git rev-parse HEAD)\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^:.gtd/REVIEW.md\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n after=$(git show \"HEAD:.gtd/REVIEW.md\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n # Did the human hand-edit anything this round besides .gtd/?\n if git diff-tree --no-commit-id --name-only -r HEAD -- . \":(exclude).gtd\" \\\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 .gtd/REVIEW.md at this commit. Any hand edits are\"\n echo \"in that commit's other paths. Run: git show $head\"\n } > .gtd/REVIEW_RAW.md\n fi\n rm -f .gtd/REVIEW.md\n on:\n \"A .gtd/REVIEW_RAW.md\": collecting\n \"M .gtd/REVIEW_RAW.md\": collecting\n \"D .gtd/REVIEW.md\": $onSignoff\n\n # Judges actionability and, when actionable, classifies straight into\n # `REQUIREMENTS.md` — 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 `REQUIREMENTS.md` (`A`/`M`) IS\n # the actionable case — row ORDER matters, since an actionable round's\n # diff is `A REQUIREMENTS.md` + `D REVIEW_RAW.md` together, so the\n # requirements rows must be declared first or every round would\n # short-circuit to sign-off. CONSUMING `REVIEW_RAW.md` 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). `REQUIREMENTS.md`\n # survives `re-unwind` (it lives under `.gtd/`, excluded from its own\n # revert pathspec) for `design.triage` to fold in cold; a consumed\n # `REVIEW_RAW.md` 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: REQUIREMENTS.md\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 `.gtd/REQUIREMENTS.md` and\n `.gtd/REVIEW_RAW.md`, 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(\".gtd/REVIEW_RAW.md\") %>\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 `.gtd/REVIEW.md` — 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 `.gtd/REQUIREMENTS.md`\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 `.gtd/REVIEW_RAW.md` and finish your turn.\n\n If the round is NOT actionable: delete\n `.gtd/REVIEW_RAW.md` and finish your turn, writing nothing\n to `.gtd/REQUIREMENTS.md` — consuming the capture with no\n other change IS the sign-off.\n on:\n \"A .gtd/REQUIREMENTS.md\": $onFeedback\n \"M .gtd/REQUIREMENTS.md\": $onFeedback\n \"D .gtd/REVIEW_RAW.md\": $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 system: <%~ it.vars.specReviewerPersona %>\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 `.gtd/SPEC_FEEDBACK.md`; 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(\".gtd/NEXT.md\") %>\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 `.gtd/SPEC_FEEDBACK.md` 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 .gtd/SPEC_FEEDBACK.md\": $onFix\n \"M .gtd/SPEC_FEEDBACK.md\": $onFix\n \"D .gtd/SPEC_FEEDBACK.md\": $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 sh\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 mkdir -p .gtd\n # Sweep the design/architecture phases' spent steering files:\n # REQUIREMENTS.md is consumed by architecture.author, ARCHITECTURE.md\n # by architecture.decompose, and QUESTIONS.md by whichever gate ran\n # last — all three are gone by the time the package queue starts.\n # REVIEW_RAW.md is swept here too: the review loop-back\n # (re-unwind) excludes .gtd 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 rm -f .gtd/REQUIREMENTS.md .gtd/ARCHITECTURE.md .gtd/QUESTIONS.md .gtd/REVIEW_RAW.md\n # Package file names are gtd-authored (architecture.decompose writes\n # NN-name.md), never containing whitespace/newlines that would break\n # ls | head.\n # shellcheck disable=SC2012\n next=$(ls .gtd/packages/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n printf '%s' \"$next\" > .gtd/NEXT.md\n else\n rm -f .gtd/NEXT.md\n fi\n on:\n \"D .gtd/NEXT.md\": $onDrained\n \"* .gtd/NEXT.md\": 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 system: <%~ it.vars.builderPersona %>\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 `.gtd/SATISFIED.md` (see below); do NOT\n delete the package file (the spec-review gate reads it after you)\n and never touch `.gtd/NEXT.md` — the `picking` state owns\n it.\n\n The package to implement is: <%~ it.read(\".gtd/NEXT.md\") %>\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 `.gtd/SATISFIED.md` 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 .gtd/SATISFIED.md\": &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 .gtd/SATISFIED.md\": *satisfied\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\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 `.gtd/FEEDBACK.md`; 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 `.gtd/FEEDBACK.md` (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 `.gtd/FEEDBACK.md` 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: SPEC_FEEDBACK.md\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 `.gtd/SPEC_FEEDBACK.md`; address it, then\n delete it. Do not create other files to hold notes or output.\n\n Read `.gtd/SPEC_FEEDBACK.md` (the reviewer's concerns about the\n current package) and the package spec (`<%= it.read(\".gtd/NEXT.md\") %>`),\n then fix the code to resolve every concern. Keep the change focused.\n\n Delete `.gtd/SPEC_FEEDBACK.md` 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 sh\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 .gtd/NEXT.md 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f .gtd/SPEC_FEEDBACK.md .gtd/NEXT.md .gtd/SATISFIED.md\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 system: <%~ it.vars.finisherPersona %>\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: COMMIT_MSG.md\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 `.gtd/COMMIT_MSG.md`; write\n it. Do not create other files to hold notes or output.\n\n Leave `.gtd/COMMIT_MSG.md` 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 `.gtd/COMMIT_MSG.md` 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 `.gtd/COMMIT_MSG.md` with exactly this one line and nothing\n else:\n\n chore: human review\n on:\n \"A .gtd/COMMIT_MSG.md\": $onDone\n \"M .gtd/COMMIT_MSG.md\": $onDone\n\n fix:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\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 `.gtd/FEEDBACK.md`; 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 `.gtd/FEEDBACK.md` (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 `.gtd/FEEDBACK.md` 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: TODO.md\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. .gtd/TODO.md 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 .gtd/FEEDBACK.md\": build.fix\n \"M .gtd/FEEDBACK.md\": build.fix\n \"D .gtd/REVIEW_RAW.md\": idle\n \"D .gtd/FEEDBACK.md\": idle\n \"C\": idle\n\n # ── Shared tail: the finale ─────────────────────────────────────────────\n done:\n commit: |\n <%~ it.read(\".gtd/COMMIT_MSG.md\") %>\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 sh\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 `.gtd/`\n # excluded from the pathspec (REVIEW.md lives there too) and\n # 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 # `.gtd/` (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: REVIEW.md` + `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 this literal 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 land --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: REVIEW.md\n requireRevert: true\n script: |\n #!/usr/bin/env sh\n # gtd check turn — revert the human's own review edit out of the\n # working tree, scoped to real code: everything under `.gtd/` is\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 # `.gtd/` exemption this pathspec renders — keep the two together\n # when either changes.\n set +e\n # Hoist the reverted commit once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token\n # on a line.\n commit=\"<%~ it.reviewBase %>\"\n patch=.gtd/.re-unwind.patch\n mkdir -p .gtd\n git diff --binary \"$commit^\" \"$commit\" -- . \":(exclude).gtd\" > \"$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 `.gtd/FEEDBACK.md` 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 `.gtd/FEEDBACK.md` 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";
192416
192522
  //#endregion
192417
192523
  //#region src/workflows/templates.ts
192418
192524
  /**
@@ -192602,6 +192708,7 @@ const modesJsonSchema = {
192602
192708
  const JSON_TYPE = {
192603
192709
  actor: { type: "string" },
192604
192710
  text: { type: "string" },
192711
+ stateFile: { type: "string" },
192605
192712
  mode: { type: "string" },
192606
192713
  content: { type: "string" },
192607
192714
  flag: { type: "boolean" },
@@ -192609,19 +192716,42 @@ const JSON_TYPE = {
192609
192716
  edges: { type: "object" },
192610
192717
  retry: { type: "object" }
192611
192718
  };
192719
+ /** One state's shape — `properties` is derived from `STATE_FIELD_ENTRIES` (every `authored: "state"` field, in table order); see `ConfigSchema.test.ts`. */
192720
+ const stateJsonSchema = {
192721
+ type: "object",
192722
+ description: "One workflow state. Declare exactly one content kind (script/prompt/message/commit). A commit state is final: no actor, no on.",
192723
+ additionalProperties: false,
192724
+ properties: Object.fromEntries(STATE_FIELD_ENTRIES.filter(([, spec]) => spec.authored === "state").map(([key, spec]) => [key, spec.jsonSchema ?? {
192725
+ ...JSON_TYPE[spec.kind],
192726
+ description: spec.doc
192727
+ }]))
192728
+ };
192729
+ /** A reference local: instantiates a declared machine as a child, optionally binding its `params:` — mirrors `Machines.ts`'s `isRef`. */
192730
+ const machineRefJsonSchema = {
192731
+ type: "object",
192732
+ description: "A reference: instantiates the named machine as a child, at this local's path (see src/Machines.ts). Expanded at load time into concrete, qualified states — the engine only ever sees the flattened result.",
192733
+ additionalProperties: false,
192734
+ required: ["machine"],
192735
+ properties: {
192736
+ machine: {
192737
+ type: "string",
192738
+ description: "Name of a declared entry in `machines:`."
192739
+ },
192740
+ with: {
192741
+ type: "object",
192742
+ description: "Bindings for the referenced machine's `params:`. A bound value naming another of the CALLER's own bindings (a whole-value `$name`) passes it down verbatim, scope intact."
192743
+ }
192744
+ }
192745
+ };
192612
192746
  /** The whole `workflow:` value — see `PatternConfig.ts`'s module docstring for the authoritative schema. */
192613
192747
  const workflowJsonSchema = {
192614
192748
  type: "object",
192615
- 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).",
192749
+ 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).",
192616
192750
  additionalProperties: false,
192617
192751
  required: ["entry", "machines"],
192618
192752
  properties: {
192619
192753
  vars: varsJsonSchema,
192620
192754
  modes: modesJsonSchema,
192621
- stateDir: {
192622
- type: "string",
192623
- 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."
192624
- },
192625
192755
  entry: {
192626
192756
  type: "object",
192627
192757
  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.",
@@ -192651,38 +192781,15 @@ const workflowJsonSchema = {
192651
192781
  type: "string",
192652
192782
  description: "This machine's own default local (a local state name, or a reference key), resolved recursively."
192653
192783
  },
192654
- model: {
192655
- type: "string",
192656
- description: "Opaque harness hint stamped onto every one of this machine's own `prompt` states (e.g. \"smart\"), passed through `gtd next --json`/`gtd status --json`. The ONLY place a model may be declared — a state carrying its own `model:` is a config error. Never interpreted by gtd. A machine declaring this with no `prompt` state is a config error."
192657
- },
192784
+ ...Object.fromEntries(MACHINE_FIELD_ENTRIES.map(([key, spec]) => [key, spec.jsonSchema ?? {
192785
+ ...JSON_TYPE[spec.kind],
192786
+ description: spec.doc
192787
+ }])),
192658
192788
  states: {
192659
192789
  type: "object",
192660
192790
  description: "This machine's local states, each either an ordinary state or a `{ machine, with }` reference.",
192661
192791
  minProperties: 1,
192662
- additionalProperties: { oneOf: [{
192663
- type: "object",
192664
- description: "One workflow state. Declare exactly one content kind (script/prompt/message/commit). A commit state is final: no actor, no on.",
192665
- additionalProperties: false,
192666
- properties: Object.fromEntries(STATE_FIELD_ENTRIES.filter(([, spec]) => spec.authored === "state").map(([key, spec]) => [key, spec.jsonSchema ?? {
192667
- ...JSON_TYPE[spec.kind],
192668
- description: spec.doc
192669
- }]))
192670
- }, {
192671
- type: "object",
192672
- description: "A reference: instantiates the named machine as a child, at this local's path (see src/Machines.ts). Expanded at load time into concrete, qualified states — the engine only ever sees the flattened result.",
192673
- additionalProperties: false,
192674
- required: ["machine"],
192675
- properties: {
192676
- machine: {
192677
- type: "string",
192678
- description: "Name of a declared entry in `machines:`."
192679
- },
192680
- with: {
192681
- type: "object",
192682
- description: "Bindings for the referenced machine's `params:`. A bound value naming another of the CALLER's own bindings (a whole-value `$name`) passes it down verbatim, scope intact."
192683
- }
192684
- }
192685
- }] }
192792
+ additionalProperties: { oneOf: [stateJsonSchema, machineRefJsonSchema] }
192686
192793
  }
192687
192794
  }
192688
192795
  }
@@ -193864,13 +193971,10 @@ var Eta = class extends Eta$1 {
193864
193971
  * The stub `TemplateContext` an `on` pattern key renders against at the edge
193865
193972
  * (see `Edge.ts`'s `renderOnEdges`): only `vars` (and, optionally, `state`)
193866
193973
  * are real — every commit-ish/diff field is empty, `processCost` is `0`,
193867
- * `edges` is empty, `stateDir` is empty, and `read` throws, since a pattern
193868
- * names a path and never legitimately needs a working tree or git history to
193869
- * resolve. This is also `Lsp.ts`'s `buildFileModeMap` map-building context,
193870
- * factored out here to avoid building the same stub in two places, AND the
193871
- * context `src/Edge.ts` renders the `stateDir:` declaration itself against —
193872
- * an empty `stateDir` there is what makes a declaration that references
193873
- * `it.stateDir` render empty rather than recurse.
193974
+ * `edges` is empty, and `read` throws, since a pattern names a path and never
193975
+ * legitimately needs a working tree or git history to resolve. This is also
193976
+ * `Lsp.ts`'s `buildFileModeMap` map-building context, factored out here to
193977
+ * avoid building the same stub in two places.
193874
193978
  */
193875
193979
  const varsOnlyContext = (vars, state = "") => ({
193876
193980
  startCommit: "",
@@ -193886,8 +193990,7 @@ const varsOnlyContext = (vars, state = "") => ({
193886
193990
  throw new Error(`no working tree to read from while rendering against a vars-only context (path: ${path})`);
193887
193991
  },
193888
193992
  vars,
193889
- edges: [],
193890
- stateDir: ""
193993
+ edges: []
193891
193994
  });
193892
193995
  const eta = new Eta();
193893
193996
  eta.readFile = null;
@@ -194447,18 +194550,15 @@ const combinedScript = (required, optional) => {
194447
194550
  * renamed-state refusal
194448
194551
  * 4. `commitHistory()` → `ProcessRun`
194449
194552
  * 5. `vars` — the four-layer merge (workflow < rc < entry commit < env)
194450
- * 6. `stateDir` — `def`'s declared plumbing directory (`stateDirOf`),
194451
- * rendered against `vars` ONLY (Eta template; `vars` must precede this,
194452
- * same ordering problem `on`'s render below has)
194453
- * 7. `on` — the resting state's `on` edges, rendered against `vars` (Eta
194553
+ * 6. `on` — the resting state's `on` edges, rendered against `vars` (Eta
194454
194554
  * templates; `vars` must precede this)
194455
- * 8. `stepDef` — `def` with `on` patched onto the resting state, the shape
194555
+ * 7. `stepDef` — `def` with `on` patched onto the resting state, the shape
194456
194556
  * `PatternMachine.step` must be fed
194457
- * 9. `reviewBase` — the pure `reviewBaseFor(def, run)`
194458
- * 10. `changes` — `changedPaths()`
194459
- * 11. `context` — the full `TemplateContext` (`on`, `stateDir` and
194460
- * `reviewBase` all feed it — `it.edges` derives from `on`)
194461
- * 12. `memory`, then `hints` (`model`/`label`/`file` rendered against
194557
+ * 8. `reviewBase` — the pure `reviewBaseFor(def, run)`
194558
+ * 9. `changes` — `changedPaths()`
194559
+ * 10. `context` — the full `TemplateContext` (`on` and `reviewBase` both feed
194560
+ * it — `it.edges` derives from `on`)
194561
+ * 11. `memory`, then `hints` (`model`/`label`/`file` rendered against
194462
194562
  * `context`; `mode` passed through verbatim)
194463
194563
  *
194464
194564
  * Two module rules, enforced on this file itself: a private helper with only
@@ -194867,41 +194967,6 @@ const renderOnEdgesOrFail = (onEdges, vars) => try_({
194867
194967
  catch: (e) => e instanceof Error ? e : new Error(String(e))
194868
194968
  });
194869
194969
  /**
194870
- * Render `def`'s declared plumbing directory (`PatternMachine.stateDirOf`)
194871
- * against `vars` ONLY (`varsOnlyContext`) — same restriction `renderOnEdges`
194872
- * uses, which is what breaks the circularity of a context field whose own
194873
- * value comes from a template rendered against that context: a declaration
194874
- * that references `it.stateDir` itself sees the empty stub there and renders
194875
- * empty rather than recursing. Stripped ONCE here
194876
- * (`PatternMachine.canonicalStateDir`) — tolerance for a leading `./` and a
194877
- * trailing `/`, NOT full canonicalization; it does not make every downstream
194878
- * reader see a canonical string on its own. `renderStateDirOrFail` is what
194879
- * makes that guarantee true, by refusing anything the strip didn't already
194880
- * fix. Throws whatever Eta throws on a malformed template; the caller turns
194881
- * that into a step refusal / command error, exactly like a content render
194882
- * failure.
194883
- */
194884
- const renderStateDir = (def, vars) => canonicalStateDir(renderStateTemplate(stateDirOf(def), varsOnlyContext(vars)));
194885
- /**
194886
- * `renderStateDir`, surfacing a malformed template as a plain `Error` (see
194887
- * `renderOnEdgesOrFail`), THEN running `PatternMachine.stateDirError` on the
194888
- * rendered, stripped result and failing with its message when it fires. This
194889
- * is the one ENFORCEMENT site for the value rule (see
194890
- * `PatternMachine.stateDirError`'s doc comment for why load time can't own
194891
- * it): a non-canonical spelling that survives `renderStateDir`'s tolerance
194892
- * strip is a config error naming the canonical form, never something this
194893
- * function or any downstream consumer repairs — every caller of
194894
- * `currentRest`/`restAt` (`gtd next`/`status`/`land` alike) sees the same
194895
- * failure before the value reaches any consumer.
194896
- */
194897
- const renderStateDirOrFail = (def, vars) => try_({
194898
- try: () => renderStateDir(def, vars),
194899
- catch: (e) => e instanceof Error ? e : new Error(String(e))
194900
- }).pipe(flatMap$4((stateDir) => {
194901
- const error = stateDirError(stateDir);
194902
- return error === void 0 ? succeed$3(stateDir) : fail$2(new Error(error));
194903
- }));
194904
- /**
194905
194970
  * A shallow clone of `def` whose `state`'s `on` is replaced by
194906
194971
  * `renderedOnEdges` — used to feed `PatternMachine.step`, which matches only
194907
194972
  * `def.states[state].on` for the state it's invoked at. Only the RESTING
@@ -194930,11 +194995,9 @@ const withRenderedOn = (def, state, renderedOnEdges) => ({
194930
194995
  * breakdown including the squashing step) — `0`/absent for the pure emitters
194931
194996
  * (`gtd next`/`gtd status`), where no step is being performed. No diff is ever
194932
194997
  * computed here — `it.reviewBase`/`it.retainedBase` are bases a template tells
194933
- * the agent to `git diff` itself. `stateDir` is ALREADY RENDERED and
194934
- * normalized by the caller (`renderStateDir`) — this function only carries it
194935
- * onto `it.stateDir`, same discipline as `edges`.
194998
+ * the agent to `git diff` itself.
194936
194999
  */
194937
- const buildTemplateContext = (git, read, state, actor, run, vars, edges, currentCost, currentModel, reviewBase, stateDir) => gen(function* () {
195000
+ const buildTemplateContext = (git, read, state, actor, run, vars, edges, currentCost, currentModel, reviewBase) => gen(function* () {
194938
195001
  const currentCommit = yield* git.resolveRef("HEAD");
194939
195002
  const previousCommit = yield* git.resolveRef("HEAD~1").pipe(catchAll(() => succeed$3(run.startParentHash)));
194940
195003
  const stepEntry = currentCost > 0 || currentModel !== void 0 ? [{
@@ -194954,8 +195017,7 @@ const buildTemplateContext = (git, read, state, actor, run, vars, edges, current
194954
195017
  processCostByModel: costByModel(allCostEntries),
194955
195018
  read,
194956
195019
  vars,
194957
- edges: toTemplateEdges(edges),
194958
- stateDir
195020
+ edges: toTemplateEdges(edges)
194959
195021
  };
194960
195022
  });
194961
195023
  /**
@@ -195004,12 +195066,11 @@ const restAt = (ref) => gen(function* () {
195004
195066
  yield* (yield* Narrator).narrate(`rest resolved: ${resolved.state} (awaits ${resolved.actor})`);
195005
195067
  const run = yield* computeProcessRun(git, def, windowHead);
195006
195068
  const vars = resolveVars(config.workflowVars, config.rcVars, run.entryVars, envVars.all);
195007
- const stateDir = yield* renderStateDirOrFail(def, vars);
195008
195069
  const on = yield* renderOnEdgesOrFail(resolved.stateDef.on, vars);
195009
195070
  const stepDef = withRenderedOn(def, resolved.state, on);
195010
195071
  const reviewBase = reviewBaseFor(def, run);
195011
195072
  const changes = yield* pendingChanges(git, windowHead);
195012
- const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase, stateDir);
195073
+ const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase);
195013
195074
  const memory = memoryKeyFor(config.stateScopes, resolved, run);
195014
195075
  const memoryResumed = memoryResumedFor(def, config.stateScopes, resolved, run);
195015
195076
  const hints = yield* renderHints(resolved.stateDef, context);
@@ -195121,16 +195182,14 @@ const formatStepRefusal = (refusal) => refusal.reason === "out-of-turn" ? `gtd l
195121
195182
  * `TemplateContext` at all), and it is NOT interchangeable with `rest.context`:
195122
195183
  * that one is pinned to the resting state and carries `cost: 0`, so rendering
195123
195184
  * `it.processCost` against it silently omits the squashing step's own
195124
- * `--cost`. `stateDir` is definition-level and vars-only, so it never varies
195125
- * by target state — reused verbatim from `rest.context.stateDir` rather than
195126
- * re-rendered. Exported for `program.ts`'s `planLanding`, which assembles the
195185
+ * `--cost`. Exported for `program.ts`'s `planLanding`, which assembles the
195127
195186
  * same script by hand and must pick the same context.
195128
195187
  */
195129
195188
  const contextAt = (rest, targetState, invoker, cost, model) => gen(function* () {
195130
195189
  const git = yield* GitService;
195131
195190
  const files = yield* RepoFiles;
195132
195191
  const onEdges = targetState === rest.state ? rest.on : yield* renderOnEdgesOrFail(rest.def.states[targetState]?.on, rest.vars);
195133
- return yield* buildTemplateContext(git, templateRead(files), targetState, invoker, rest.run, rest.vars, onEdges, cost ?? 0, model, reviewBaseFor(rest.def, rest.run), rest.context.stateDir);
195192
+ return yield* buildTemplateContext(git, templateRead(files), targetState, invoker, rest.run, rest.vars, onEdges, cost ?? 0, model, reviewBaseFor(rest.def, rest.run));
195134
195193
  });
195135
195194
  /**
195136
195195
  * Render a `"commit"`/`"squash"` decision as the `EmitStep`s the external
@@ -195600,16 +195659,14 @@ const decideOpenWindow = (def, target, run, head) => {
195600
195659
  * same target is a no-op, and `restoreStagedFrom` re-pinning an already-pinned
195601
195660
  * path is a no-op.
195602
195661
  *
195603
- * `".gtd"` stays a LITERAL, deliberately, rather than being derived from the
195604
- * workflow's own `stateDir`/`file:` declarations. The pin exists to keep gtd's
195605
- * PLUMBING out of the reviewer's editor, and `.gtd/` is the conventional
195606
- * directory it lives in — including the parts no state declares (a check
195607
- * script's temp output), which a `file:`-derived list would miss. A steering
195608
- * file a workflow repoints outside it (`vars: { reviewFile: REVIEW.md }`)
195609
- * simply stays untracked in the window, which is harmless: `changedPaths`
195610
- * classifies untracked paths by content (`src/Git.ts`), so an unedited one is
195611
- * not a pending change and never a phantom deletion. Making the list dynamic
195612
- * would also cost the in-memory tier its trust property — the recognizer
195662
+ * `".gtd"` stays a LITERAL, deliberately, rather than being derived from
195663
+ * states' own `file:` declarations (every one of which is compiled under
195664
+ * `.gtd/` anyway — see `PatternConfig.ts`'s `stateFile` compiler). The pin
195665
+ * exists to keep gtd's PLUMBING out of the reviewer's editor, and `.gtd/` is
195666
+ * the conventional directory it lives in — including the parts no state
195667
+ * declares (a check script's temp output), which a `file:`-derived list would
195668
+ * miss. Making the list dynamic would also cost the in-memory tier its trust
195669
+ * property — the recognizer
195613
195670
  * (`src/testing/EmittedScriptRecognizer.ts`'s `recognizeReviewWindowOpen`)
195614
195671
  * re-derives this exact string and compares, which only works while the paths
195615
195672
  * are fixed. Don't trade that for cosmetics.
@@ -207230,7 +207287,7 @@ const flattenTree = (node, parent, depth, out) => {
207230
207287
  * The `model` of one group — a machine instance's own `model:`, read off any
207231
207288
  * ONE of that group's own prompt-content states (by construction every prompt
207232
207289
  * state one machine instance owns carries the identical `def.model` — see
207233
- * `Machines.ts`'s `resolveInstanceModel`/`emitTree`). `undefined` when the
207290
+ * `Machines.ts`'s `resolveInstanceMachineFields`/`emitTree`). `undefined` when the
207234
207291
  * group owns no prompt state (e.g. a queue/gate machine of only
207235
207292
  * script/message/commit states) or its machine declares no `model:` — never
207236
207293
  * guessed.
@@ -207415,23 +207472,22 @@ const openInBrowser = (url) => {
207415
207472
  */
207416
207473
  const deletesFile = (changes, file) => changes.some((c) => c.path === file && c.status === "D");
207417
207474
  /**
207418
- * True when `path` sits inside the declared plumbing directory `dir` (carries
207419
- * no trailing slash) — an exact match on the bare directory path itself, or
207420
- * prefixed by `dir + "/"` (the separator is appended here, not assumed to be
207421
- * part of `dir`). A naive `path.startsWith(dir)` would also swallow a sibling
207422
- * `<dir>-backup/` next to the declared directory.
207475
+ * True when `path` sits inside gtd's plumbing directory (`STATE_DIR`, `.gtd`)
207476
+ * — an exact match on the bare directory path itself, or prefixed by
207477
+ * `STATE_DIR + "/"`. The exact-match-or-separator shape matters: a naive
207478
+ * `path.startsWith(STATE_DIR)` would also swallow a sibling `.gtd-backup/`.
207423
207479
  */
207424
- const isPlumbingPath = (path, dir) => path === dir || path.startsWith(`${dir}/`);
207480
+ const isPlumbingPath = (path) => path === ".gtd" || path.startsWith(`.gtd/`);
207425
207481
  /**
207426
- * True when `path` is a real code path — outside the DECLARED plumbing
207427
- * directory (`stateDir`, never a literal `.gtd/`) and not the state's own
207428
- * `file:`, excluded by EXACT path (never merely a prefix check, since `file:`
207429
- * is var-configurable to live outside `stateDir` — issue #128). The one "did
207430
- * a human touch something real" predicate shared by `enforceStepGuards`'s own
207482
+ * True when `path` is a real code path — outside gtd's plumbing directory.
207483
+ * Every state's `file:` is compiled under `STATE_DIR` too (`PatternConfig.ts`'s
207484
+ * `stateFile` compiler), so this plain negation already excludes a state's own
207485
+ * steering file with no separate exact-path exemption needed. The one "did a
207486
+ * human touch something real" predicate shared by `enforceStepGuards`'s own
207431
207487
  * `hasCodeChange` and the require-revert guard's residue scoping, so the rule
207432
207488
  * is declared exactly once.
207433
207489
  */
207434
- const isCodePath = (path, file, stateDir) => !isPlumbingPath(path, stateDir) && path !== file;
207490
+ const isCodePath = (path) => !isPlumbingPath(path);
207435
207491
  /** The `mode:` name of gtd's built-in REVIEW.md checkbox format — the only mode the review-doc guard understands. */
207436
207492
  const REVIEW_MODE = "review";
207437
207493
  const reviewDocGuard = {
@@ -207475,7 +207531,7 @@ const stepGuards = [
207475
207531
  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.`;
207476
207532
  const git = yield* GitService;
207477
207533
  const base = `${rb}~1`;
207478
- const scoped = ((yield* git.commitHistory(base, rb))[0]?.touched ?? []).filter((path) => isCodePath(path, ctx.file, ctx.template.stateDir));
207534
+ const scoped = ((yield* git.commitHistory(base, rb))[0]?.touched ?? []).filter((path) => isCodePath(path));
207479
207535
  if (scoped.length === 0) return void 0;
207480
207536
  const residue = (yield* git.changedPaths(base)).filter((c) => scoped.includes(c.path)).map((c) => c.path);
207481
207537
  if (residue.length === 0) return void 0;
@@ -207499,7 +207555,7 @@ const enforceStepGuards = (input) => gen(function* () {
207499
207555
  const applicable = stepGuards.filter((g) => g.appliesTo(input.rest));
207500
207556
  if (applicable.length === 0) return;
207501
207557
  const fileDeleted = deletesFile(input.changes, file);
207502
- const hasCodeChange = input.changes.some((c) => isCodePath(c.path, file, input.context.stateDir));
207558
+ const hasCodeChange = input.changes.some((c) => isCodePath(c.path));
207503
207559
  const base = {
207504
207560
  rest: input.rest,
207505
207561
  file,
@@ -207698,6 +207754,10 @@ const stallDiagnosis = (state, actor) => `stalled at "${state}": the last gtd(${
207698
207754
  * always present, `true`/`false`, never omitted. `model`/`memory`/`file`/
207699
207755
  * `mode`/`label`/`edges` are plain facts about the resting state and are
207700
207756
  * emitted at EVERY kind — omitted (never `null`) when their source is unset.
207757
+ * `system` is the same, EXCEPT it is also omitted when its rendered value is
207758
+ * the empty string: unlike `model`, an empty system prompt reaching a driver
207759
+ * as `--system-prompt ""` silently deletes the harness's own default instead
207760
+ * of failing loudly, so a render-to-empty is treated as unset.
207701
207761
  * `changes` and `next` are always present (the headline conclusion, never
207702
207762
  * omit-vs-null — a `null` `next` means no declared pattern matches).
207703
207763
  * `cost`/`costByModel` are emitted only when a cost has actually been
@@ -207716,6 +207776,7 @@ const beatFields = (input) => {
207716
207776
  resume: session.resume
207717
207777
  } } : {},
207718
207778
  ...rendered.model !== void 0 ? { model: rendered.model } : {},
207779
+ ...rendered.system !== void 0 && rendered.system !== "" ? { system: rendered.system } : {},
207719
207780
  ...dispatchable && validate !== void 0 ? { validate } : {},
207720
207781
  log,
207721
207782
  state: rendered.state,
@@ -207743,6 +207804,7 @@ const BEAT_SH_SHAPE = {
207743
207804
  resume: "bool"
207744
207805
  },
207745
207806
  model: "scalar",
207807
+ system: "scalar",
207746
207808
  validate: "scalar",
207747
207809
  log: "scalar",
207748
207810
  state: "scalar",
@@ -207988,8 +208050,9 @@ while :; do
207988
208050
  # macOS, and POSIX guarantees only 4 KB, ARG_MAX), and a diff crosses
207989
208051
  # that far sooner than you'd expect.
207990
208052
  agent_turn() { printf '%s' "$gtd_content" | claude -p "$1" "$gtd_session_id" \\
207991
- \${gtd_model:+--model "$gtd_model"} --dangerously-skip-permissions \\
207992
- >>"$gtd_log" 2>&1; }
208053
+ \${gtd_model:+--model "$gtd_model"} \\
208054
+ \${gtd_system:+--system-prompt "$gtd_system"} \\
208055
+ --dangerously-skip-permissions >>"$gtd_log" 2>&1; }
207993
208056
  if [ "\${gtd_session_resume:-}" = true ]
207994
208057
  then agent_turn --resume || agent_turn --session-id
207995
208058
  else agent_turn --session-id || agent_turn --resume
@@ -207998,8 +208061,16 @@ while :; do
207998
208061
  while [ -n "\${gtd_validate:-}" ] && ! fix="$(sh -c "$gtd_validate" 2>&1)"; do
207999
208062
  n=$((n + 1)) && [ "$n" -gt 3 ] && { printf '%s\\n' "$fix" >&2; exit 1; }
208000
208063
  # $fix IS the fix prompt, verbatim — piped for the same reason as
208001
- # $gtd_content above
208064
+ # $gtd_content above. Whether \`claude --resume\` re-applies the
208065
+ # original session's model/system prompt is a harness detail gtd
208066
+ # cannot verify from outside, so this passes the identical
208067
+ # $gtd_model/$gtd_system on both calls rather than assume it does —
208068
+ # otherwise this fix turn might silently fall back to Claude Code's
208069
+ # own defaults while the turn that produced the file ran under the
208070
+ # workflow's own model and persona.
208002
208071
  printf '%s' "$fix" | claude -p --resume "$gtd_session_id" \\
208072
+ \${gtd_model:+--model "$gtd_model"} \\
208073
+ \${gtd_system:+--system-prompt "$gtd_system"} \\
208003
208074
  --dangerously-skip-permissions >>"$gtd_log" 2>&1
208004
208075
  done ;;
208005
208076
  esac