@pmelab/gtd 9.11.0 → 9.13.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.
@@ -189660,7 +189660,7 @@ const STATE_FIELDS = {
189660
189660
  surface: "def",
189661
189661
  authored: "state",
189662
189662
  viz: "field",
189663
- doc: "Who acts at this state. Required on every non-commit state; forbidden on a commit state."
189663
+ doc: "Who acts at this state. Required on every state."
189664
189664
  },
189665
189665
  script: {
189666
189666
  kind: "content",
@@ -189680,12 +189680,6 @@ const STATE_FIELDS = {
189680
189680
  authored: "state",
189681
189681
  doc: "Content kind: a human-facing message (Eta template), emitted by `gtd next`."
189682
189682
  },
189683
- commit: {
189684
- kind: "content",
189685
- surface: "def",
189686
- authored: "state",
189687
- doc: "Content kind: entering this state ends the process by squashing it into one commit with this message (Eta template). Final — no actor, no on."
189688
- },
189689
189683
  on: {
189690
189684
  kind: "edges",
189691
189685
  surface: "def",
@@ -189707,7 +189701,6 @@ const STATE_FIELDS = {
189707
189701
  surface: "def",
189708
189702
  authored: "machine",
189709
189703
  nonEmpty: true,
189710
- commit: "forbidden",
189711
189704
  viz: "field",
189712
189705
  rest: "rendered",
189713
189706
  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."
@@ -189717,7 +189710,6 @@ const STATE_FIELDS = {
189717
189710
  surface: "def",
189718
189711
  authored: "machine",
189719
189712
  nonEmpty: true,
189720
- commit: "forbidden",
189721
189713
  fileRef: true,
189722
189714
  rest: "rendered",
189723
189715
  viz: "field",
@@ -189729,9 +189721,8 @@ const STATE_FIELDS = {
189729
189721
  surface: "def",
189730
189722
  authored: "state",
189731
189723
  nonEmpty: true,
189732
- commit: "forbidden",
189733
189724
  rest: "rendered",
189734
- doc: "Opaque display name passed through `gtd next --json`/`gtd status --json` so a driver/viewer can show something nicer than the raw state name (e.g. \"Running checks\"). Never interpreted by gtd. Forbidden on a commit state."
189725
+ doc: "Opaque display name passed through `gtd next --json`/`gtd status --json` so a driver/viewer can show something nicer than the raw state name (e.g. \"Running checks\"). Never interpreted by gtd."
189735
189726
  },
189736
189727
  /** Multiple states may (and, in the bundled default, do) share one `file:`. The engine never reads a path out of this string itself — only the LSP interprets it, to map rendered paths to `mode`. */
189737
189728
  file: {
@@ -189739,59 +189730,45 @@ const STATE_FIELDS = {
189739
189730
  surface: "def",
189740
189731
  authored: "state",
189741
189732
  nonEmpty: true,
189742
- commit: "forbidden",
189743
189733
  viz: "field",
189744
189734
  rest: "rendered",
189745
- 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."
189735
+ 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."
189746
189736
  },
189747
189737
  /** Opaque, like `model`: the engine never branches on it — the edge (`src/SteeringMode.ts`) resolves it to a format/validate pair. `validateDefinition` only enforces that the name resolves, so a typo can't silently disable the gate. */
189748
189738
  mode: {
189749
189739
  kind: "mode",
189750
189740
  surface: "def",
189751
189741
  authored: "state",
189752
- commit: "forbidden",
189753
189742
  requires: "file",
189754
189743
  viz: "field",
189755
189744
  rest: "verbatim",
189756
- doc: "The steering file's format: the name of a built-in mode (qa/review, validated in-process by gtd) or of a `modes:` entry. gtd formats and validates the file with that mode before capturing a turn out of this state, and the LSP dispatches live diagnostics on the built-in names. Requires a sibling `file:`. Forbidden on a commit state."
189745
+ doc: "The steering file's format: the name of a built-in mode (qa/review, validated in-process by gtd) or of a `modes:` entry. gtd formats and validates the file with that mode before capturing a turn out of this state, and the LSP dispatches live diagnostics on the built-in names. Requires a sibling `file:`."
189757
189746
  },
189758
- /** This module's pure functions never read this flag — the window is opened/closed entirely at the edge (`src/ReviewWindow.ts`). */
189759
- reviewWindow: {
189760
- kind: "flag",
189761
- surface: "def",
189762
- authored: "state",
189763
- commit: "forbidden",
189764
- viz: "flag",
189765
- doc: "When true, gtd opens a review checkout window while the machine rests here — HEAD/index are rewound to the review base so the whole base..HEAD diff surfaces as uncommitted changes in the editor. Forbidden on a commit state."
189766
- },
189767
- /** Like `reviewWindow`, the engine never reads this — it's history-derived edge data. `isReviewBaseState` never treats a string value as the `true`/window-anchor form. */
189747
+ /** The engine never reads this — it's history-derived edge data. `isReviewBaseState` never treats a string value as the `true`/window-anchor form. */
189768
189748
  reviewBase: {
189769
189749
  kind: "flagOrTemplate",
189770
189750
  surface: "def",
189771
189751
  authored: "state",
189772
- commit: "forbidden",
189773
189752
  viz: "flag",
189774
- doc: "true marks the state whose most-recent in-process commit anchors the review window's diff base; absent any, the base is the process start. A string is a different shape: an Eta template rendering a commitish that becomes the WHOLE PROCESS's fixed diff base when this state is entered manually via `gtd --entry <state> --base <commitish>` (see the `entry` property below). Forbidden on a commit state."
189753
+ doc: "true marks the state whose most-recent in-process commit anchors the review window's diff base; absent any, the base is the process start. A string is a different shape: an Eta template rendering a commitish that becomes the WHOLE PROCESS's fixed diff base when this state is entered manually via `gtd --entry <state> --base <commitish>` (see the `entry` property below)."
189775
189754
  },
189776
189755
  /** The pure engine never reads this — checked at the edge by the feedback-progress guard in `src/StepGuards.ts`. */
189777
189756
  requireProgress: {
189778
189757
  kind: "flag",
189779
189758
  surface: "def",
189780
189759
  authored: "state",
189781
- commit: "forbidden",
189782
189760
  requires: "file",
189783
189761
  viz: "flag",
189784
- doc: "When true, a step at this state is refused if its only pending change is deleting the state's own `file:` — a work-free turn that discards its input without addressing it. A `NOTHING ACTIONABLE` sentinel file is exempt (a legitimately non-actionable round makes no code change). Requires a `file:`. Forbidden on a commit state."
189762
+ doc: "When true, a step at this state is refused if its only pending change is deleting the state's own `file:` — a work-free turn that discards its input without addressing it. A `NOTHING ACTIONABLE` sentinel file is exempt (a legitimately non-actionable round makes no code change). Requires a `file:`."
189785
189763
  },
189786
189764
  /** The pure engine never reads this — checked at the edge by the answer-completeness guard in `src/StepGuards.ts`, and only when the state also declares `mode: qa`. */
189787
189765
  answerGate: {
189788
189766
  kind: "flag",
189789
189767
  surface: "def",
189790
189768
  authored: "state",
189791
- commit: "forbidden",
189792
189769
  requires: "file",
189793
189770
  viz: "flag",
189794
- doc: "When true, a step at this state is refused unless every open question in its qa-mode `file:` is answered — exactly one checkbox ticked per question. Requires a `file:` and `mode: qa`. Forbidden on a commit state."
189771
+ doc: "When true, a step at this state is refused unless every open question in its qa-mode `file:` is answered — exactly one checkbox ticked per question. Requires a `file:` and `mode: qa`."
189795
189772
  },
189796
189773
  /**
189797
189774
  * The pure engine never reads this — checked at the edge by the
@@ -189804,10 +189781,9 @@ const STATE_FIELDS = {
189804
189781
  kind: "flag",
189805
189782
  surface: "def",
189806
189783
  authored: "state",
189807
- commit: "forbidden",
189808
189784
  requires: "file",
189809
189785
  viz: "flag",
189810
- doc: "When true, a step at this state is refused unless the paths the human's own review-round commit touched have actually been reverted out of the working tree — re-established from the tree itself, never from the script's own exit code. Requires a `file:`. Forbidden on a commit state."
189786
+ doc: "When true, a step at this state is refused unless the paths the human's own review-round commit touched have actually been reverted out of the working tree — re-established from the tree itself, never from the script's own exit code. Requires a `file:`."
189811
189787
  },
189812
189788
  /** Authoring-only: never lands on the compiled `StateDef` — `compileWorkflowConfig` reads the raw flag directly to build `entries.manual` instead. */
189813
189789
  entry: {
@@ -189819,17 +189795,15 @@ const STATE_FIELDS = {
189819
189795
  }
189820
189796
  };
189821
189797
  const STATE_FIELD_ENTRIES = Object.entries(STATE_FIELDS);
189822
- /** The four content-kind field names, in declaration order — the "exactly one of" set `validateContentKind`/`compileContent` iterate. */
189798
+ /** The three content-kind field names, in declaration order — the "exactly one of" set `validateContentKind`/`compileContent` iterate. */
189823
189799
  const CONTENT_FIELDS = STATE_FIELD_ENTRIES.filter(([, spec]) => spec.kind === "content").map(([key]) => key);
189824
189800
  /** Every machine-authored field, in table order — the machine tier's `CONTENT_FIELDS`. */
189825
189801
  const MACHINE_FIELD_ENTRIES = STATE_FIELD_ENTRIES.filter(([, spec]) => spec.authored === "machine");
189826
- const isCommitState = (state) => state.commit !== void 0;
189827
189802
  /**
189828
189803
  * The generic per-field rules every derivation site walks the same way:
189829
- * non-empty, forbidden on a commit state, requires a sibling field — only
189830
- * evaluated when `key` is actually declared on `state`. A field's own bespoke
189831
- * checks (an `on` pattern parsing, a `mode` name resolving, …) are the
189832
- * caller's concern.
189804
+ * non-empty, requires a sibling field — only evaluated when `key` is
189805
+ * actually declared on `state`. A field's own bespoke checks (an `on`
189806
+ * pattern parsing, a `mode` name resolving, …) are the caller's concern.
189833
189807
  */
189834
189808
  const validateFieldRules = (name, state, key, spec) => {
189835
189809
  const raw = state;
@@ -189837,7 +189811,6 @@ const validateFieldRules = (name, state, key, spec) => {
189837
189811
  if (value === void 0) return [];
189838
189812
  const errors = [];
189839
189813
  if (spec.nonEmpty === true && value === "") errors.push(`state "${name}": "${key}" must be a non-empty string`);
189840
- if (spec.commit === "forbidden" && isCommitState(state)) errors.push(`state "${name}": a commit state cannot declare "${key}"`);
189841
189814
  if (spec.requires !== void 0 && raw[spec.requires] === void 0) errors.push(`state "${name}": "${key}" requires "${spec.requires}"`);
189842
189815
  return errors;
189843
189816
  };
@@ -189856,13 +189829,10 @@ const contentKindOf = (state) => {
189856
189829
  if (state.script !== void 0) return "script";
189857
189830
  if (state.prompt !== void 0) return "prompt";
189858
189831
  if (state.message !== void 0) return "message";
189859
- if (state.commit !== void 0) return "commit";
189860
189832
  };
189861
- /** The raw template source a state's own content kind carries — `script`/`prompt`/`message`, or `undefined` for a commit state (never at rest, no template a viewer could show). */
189833
+ /** The raw template source a state's own content kind carries — `script`/`prompt`/`message`. */
189862
189834
  const contentOf = (state) => state.script ?? state.prompt ?? state.message;
189863
- /** True when a rest at `state` should open the review checkout window. Safe for an unknown state name (returns `false`). */
189864
- const isReviewWindowState = (def, state) => def.states[state]?.reviewWindow === true;
189865
- /** True when `state` anchors the review window's diff base. Safe for an unknown state name (returns `false`). The string/template form of `reviewBase` is NOT a window anchor — this stays narrowed to `=== true` on purpose, never truthy. */
189835
+ /** True when `state` anchors the review's diff base. Safe for an unknown state name (returns `false`). The string/template form of `reviewBase` is NOT this anchor form — this stays narrowed to `=== true` on purpose, never truthy. */
189866
189836
  const isReviewBaseState = (def, state) => def.states[state]?.reviewBase === true;
189867
189837
  /** The named state's `reviewBase` when it is a STRING (an Eta template rendering a commitish that fixes the whole process's diff base) — `undefined` when `reviewBase` is `true`, absent, or `state` doesn't exist. Pure accessor only: rendering the template is an edge concern. */
189868
189838
  const entryBaseTemplateOf = (def, state) => {
@@ -189873,12 +189843,10 @@ const isRequireProgressState = (def, state) => def.states[state]?.requireProgres
189873
189843
  const isAnswerGateState = (def, state) => def.states[state]?.answerGate === true;
189874
189844
  const isRequireRevertState = (def, state) => def.states[state]?.requireRevert === true;
189875
189845
  /**
189876
- * Every declared state that is NOT a commit state, sorted. Intentionally ALL
189877
- * non-commit states, not just ones that declared `entry: true` — this drives
189878
- * the CLI's `--entry <state>` guard and its error message, a broader set on
189879
- * purpose.
189846
+ * Every declared state, sorted. This drives the CLI's `--entry <state>` guard
189847
+ * and its error message, a broader set than `entries.manual` on purpose.
189880
189848
  */
189881
- const enterableStates = (def) => Object.keys(def.states).filter((name) => !isCommitState(def.states[name])).sort();
189849
+ const enterableStates = (def) => Object.keys(def.states).sort();
189882
189850
  const TRANSITION_SEP = " → ";
189883
189851
  /**
189884
189852
  * `gtd(<actor>): <from> → <to>` — the subject a step commit carries.
@@ -189922,7 +189890,6 @@ const initialStateOf = (def) => def.entries.default;
189922
189890
  /**
189923
189891
  * Every actor declared by ANY state in the workflow — the closed-world
189924
189892
  * vocabulary a parsed subject's actor is checked against by `resolveState`.
189925
- * Commit states carry no `actor` and so contribute nothing.
189926
189893
  */
189927
189894
  const declaredActors = (def) => {
189928
189895
  const actors = /* @__PURE__ */ new Set();
@@ -189939,18 +189906,13 @@ const declaredActors = (def) => {
189939
189906
  * now awaited.
189940
189907
  *
189941
189908
  * An unrecognized subject — non-`gtd(...)`, malformed, naming an undefined
189942
- * state, an actor outside the vocabulary, or a commit state — resolves to the
189943
- * INITIAL state (old histories and every completed process's squash commit
189944
- * land here). Commit states are excluded explicitly rather than via an
189945
- * actor-mismatch trick, since a hand-authored subject could still name one
189946
- * even though `step` never writes one.
189909
+ * state, or an actor outside the vocabulary — resolves to the INITIAL state
189910
+ * (old histories land here).
189947
189911
  */
189948
189912
  const resolveState = (def, headSubject) => {
189949
189913
  const parsed = parseStateSubject(headSubject);
189950
189914
  if (parsed === void 0) return initialStateOf(def);
189951
- const state = def.states[parsed.state];
189952
- if (state === void 0) return initialStateOf(def);
189953
- if (isCommitState(state)) return initialStateOf(def);
189915
+ if (def.states[parsed.state] === void 0) return initialStateOf(def);
189954
189916
  if (!declaredActors(def).has(parsed.actor)) return initialStateOf(def);
189955
189917
  return parsed.state;
189956
189918
  };
@@ -190089,19 +190051,18 @@ const memoryScopeAt = (scopes, state, trace) => {
190089
190051
  * or the tree is dirty and no `on` pattern matches. A clean tree with no
190090
190052
  * matching pattern is a plain no-op at a `script`/`message` rest, but at a
190091
190053
  * `prompt` rest it's an ATTEMPT instead: the state itself becomes the raw
190092
- * target, falling through the same retry/commit-state tail as a real match,
190093
- * tagged `attempt: true` on the resulting `"commit"` — a fruitless `prompt`
190054
+ * target, falling through the same retry tail as a real match, tagged
190055
+ * `attempt: true` on the resulting `"commit"` — a fruitless `prompt`
190094
190056
  * dispatch still costs money and must be remembered across restarts, so it's
190095
190057
  * committed rather than treated as an inert no-op. The target (or attempt's
190096
- * self-target) is retry-redirected (`applyRetry`) before being classified: a
190097
- * commit-state target yields `"squash"`; anything else yields `"commit"`.
190098
- * Throws only on a structurally invalid call — an undefined `state`, or a
190099
- * commit-state `state` (stepping AT a commit state is a caller error).
190058
+ * self-target) is retry-redirected (`applyRetry`) before being classified —
190059
+ * always yielding `"commit"`. Throws only on a structurally invalid call: an
190060
+ * undefined `state`.
190100
190061
  */
190101
190062
  const step = (def, state, invoker, payload) => {
190102
190063
  const stateDef = def.states[state];
190103
190064
  if (stateDef === void 0) throw new Error(`step: unknown state "${state}"`);
190104
- if (stateDef.actor === void 0) throw new Error(`step: "${state}" is a commit state — a process never rests there`);
190065
+ if (stateDef.actor === void 0) throw new Error(`step: "${state}" declares no actor — a process never rests there`);
190105
190066
  if (invoker !== stateDef.actor) return {
190106
190067
  kind: "refusal",
190107
190068
  reason: "out-of-turn",
@@ -190128,12 +190089,7 @@ const step = (def, state, invoker, payload) => {
190128
190089
  const finalTarget = applyRetry(def, target, payload.processTrace);
190129
190090
  const targetDef = def.states[finalTarget];
190130
190091
  if (targetDef === void 0) throw new Error(`step: "${state}" transitions to undefined state "${finalTarget}"`);
190131
- if (targetDef.commit !== void 0) return {
190132
- kind: "squash",
190133
- state: finalTarget,
190134
- template: targetDef.commit
190135
- };
190136
- if (targetDef.actor === void 0) throw new Error(`step: "${finalTarget}" is not a commit state but declares no actor`);
190092
+ if (targetDef.actor === void 0) throw new Error(`step: "${finalTarget}" declares no actor`);
190137
190093
  return {
190138
190094
  kind: "commit",
190139
190095
  subject: stateSubject(invoker, finalTarget, state),
@@ -190166,7 +190122,6 @@ const validateEntries = (def, names) => {
190166
190122
  errors.push(`entries.${key} "${state}" is not a defined state`);
190167
190123
  return;
190168
190124
  }
190169
- if (isCommitState(def.states[state])) errors.push(`entries.${key} "${state}" must not be a commit state`);
190170
190125
  if (key !== "default" && state === def.entries.default) errors.push(`entries.${key} "${state}" must not be the same state as entries.default`);
190171
190126
  };
190172
190127
  checkEntry("default", def.entries.default);
@@ -190180,15 +190135,9 @@ const validateEntries = (def, names) => {
190180
190135
  };
190181
190136
  const validateContentKind = (name, state) => {
190182
190137
  const kindCount = CONTENT_FIELDS.filter((key) => state[key] !== void 0).length;
190183
- return kindCount === 1 ? [] : [`state "${name}": must declare exactly one of script/prompt/message/commit (found ${kindCount})`];
190184
- };
190185
- const validateActorShape = (name, state) => {
190186
- if (!isCommitState(state)) return state.actor === void 0 ? [`state "${name}" must declare an actor (only a commit state may omit one)`] : [];
190187
- const errors = [];
190188
- if (state.actor !== void 0) errors.push(`commit state "${name}" must not declare an actor`);
190189
- if (state.on !== void 0) errors.push(`commit state "${name}" must not declare "on"`);
190190
- return errors;
190138
+ return kindCount === 1 ? [] : [`state "${name}": must declare exactly one of script/prompt/message (found ${kindCount})`];
190191
190139
  };
190140
+ const validateActorShape = (name, state) => state.actor === void 0 ? [`state "${name}" must declare an actor`] : [];
190192
190141
  /**
190193
190142
  * A declared mode's `format`/`validate`, when present, may not be blank (a
190194
190143
  * whitespace-only shell command would run and "succeed", silently disabling
@@ -191409,7 +191358,8 @@ const KNOWN_TOP_KEYS = /* @__PURE__ */ new Set([
191409
191358
  "entry",
191410
191359
  "machines",
191411
191360
  "vars",
191412
- "modes"
191361
+ "modes",
191362
+ "summary"
191413
191363
  ]);
191414
191364
  const KNOWN_MACHINE_KEYS = /* @__PURE__ */ new Set([
191415
191365
  "params",
@@ -191423,7 +191373,8 @@ const LEGACY_STATE_KEY_HINTS = {
191423
191373
  initial: `"initial" no longer exists — declare this state's qualified path in the top-level "entry.default" instead`,
191424
191374
  reviewEntry: `"reviewEntry" no longer exists — declare "entry: true" on this state instead`,
191425
191375
  fixEntry: `"fixEntry" no longer exists — declare "entry: true" on this state instead`,
191426
- memory: `"memory" no longer exists — a machine's memory scope is derived from its position in the tree and starts fresh on every entry`
191376
+ memory: `"memory" no longer exists — a machine's memory scope is derived from its position in the tree and starts fresh on every entry`,
191377
+ commit: `"commit" no longer exists — the automatic squash finale was removed; a review sign-off lands an ordinary commit entering the workflow's initial state, and \`gtd summary\` prints a prompt for the process's own closing message instead`
191427
191378
  };
191428
191379
  const LEGACY_REF_KEY_HINTS = {
191429
191380
  as: `"as" no longer exists — a reference's local name (the key itself) IS the concrete name; there is nothing left to rename`,
@@ -191611,12 +191562,18 @@ const inlineMachineFileRefs = (machineRaw, machineName, configDir, errors, fileR
191611
191562
  */
191612
191563
  const inlineWorkflowFileRefs = (rawWorkflow, configDir, errors, fileRefs = nodeFileRefReader) => {
191613
191564
  if (!isPlainObject$1(rawWorkflow)) return rawWorkflow;
191565
+ const next = { ...rawWorkflow };
191566
+ const rawSummary = rawWorkflow["summary"];
191567
+ if (typeof rawSummary === "string" && isFileReference(rawSummary)) {
191568
+ const resolved = resolveContent(rawSummary, configDir, `"summary"`, errors, fileRefs);
191569
+ if (resolved !== void 0) next["summary"] = resolved;
191570
+ }
191614
191571
  const rawMachines = rawWorkflow["machines"];
191615
- if (!isPlainObject$1(rawMachines)) return rawWorkflow;
191572
+ if (!isPlainObject$1(rawMachines)) return next;
191616
191573
  const machines = {};
191617
191574
  for (const [machineName, machineRaw] of Object.entries(rawMachines)) machines[machineName] = inlineMachineFileRefs(machineRaw, machineName, configDir, errors, fileRefs);
191618
191575
  return {
191619
- ...rawWorkflow,
191576
+ ...next,
191620
191577
  machines
191621
191578
  };
191622
191579
  };
@@ -191845,6 +191802,27 @@ const assertScopesCoverStates = (stateNames, scopes, errors) => {
191845
191802
  for (const name of stateNames) if (!(name in scopes)) errors.push(`internal error: scopes map produced by the flattener is missing state "${name}"`);
191846
191803
  };
191847
191804
  /**
191805
+ * Compile the top-level `summary:` template — the `gtd summary` prompt. An
191806
+ * absent value is legal (`gtd summary` refuses at runtime instead); a
191807
+ * present-but-blank value (or a blank inlined file) is a load error, the same
191808
+ * rule a mode's `format:`/`validate:` follows. File-ref inlining mirrors
191809
+ * `compileContentRef`'s `ctx.inlineFileRefs` discipline.
191810
+ */
191811
+ const compileSummary = (raw, configDir, errors, inlineFileRefs, fileRefs) => {
191812
+ const value = raw["summary"];
191813
+ if (value === void 0) return void 0;
191814
+ if (typeof value !== "string") {
191815
+ errors.push(`"summary" must be a string`);
191816
+ return;
191817
+ }
191818
+ const resolved = inlineFileRefs ? resolveContent(value, configDir, `"summary"`, errors, fileRefs) : value;
191819
+ if (resolved !== void 0 && resolved.trim() === "") {
191820
+ errors.push(`"summary" must not be blank`);
191821
+ return;
191822
+ }
191823
+ return resolved;
191824
+ };
191825
+ /**
191848
191826
  * Compile the raw, decoded `workflow:` YAML value into a `WorkflowDefinition`
191849
191827
  * plus the workflow's own compiled `vars:` map and its machine tree.
191850
191828
  * `rcModes` (the already-compiled top-level `.gtdrc` `modes:` key) is layered
@@ -191874,13 +191852,15 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
191874
191852
  if (isPlainObject$1(s) && s["entry"] === true) manualSet.add(name);
191875
191853
  }
191876
191854
  const manual = Array.from(manualSet).sort();
191855
+ const summary = compileSummary(raw, configDir, errors, inlineFileRefs, fileRefs);
191877
191856
  const definition = {
191878
191857
  states,
191879
191858
  entries: {
191880
191859
  default: flattened.entries.default,
191881
191860
  manual
191882
191861
  },
191883
- modes
191862
+ modes,
191863
+ ...summary !== void 0 ? { summary } : {}
191884
191864
  };
191885
191865
  assertScopesCoverStates(Object.keys(states), flattened.scopes, errors);
191886
191866
  const definitionErrors = validateDefinition(definition);
@@ -191895,7 +191875,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
191895
191875
  };
191896
191876
  //#endregion
191897
191877
  //#region src/workflows/unified.yaml
191898
- var unified_default = "# gtd's BUILT-IN DEFAULT workflow — used when no `workflow:` key is\n# configured anywhere in the cwd→home config chain; `gtd init` seeds only\n# vars.testCommand + a modes: suggestion, never this file. Compiled through\n# the same `compileWorkflowConfig` a user's own `workflow:` goes through — no\n# privileged path. See skills/authoring/SKILL.md for how to read or edit it\n# (machines, patterns, templates) and docs/driver.md for how a driver runs it.\n#\n# One flow: any change to the tree starts the process. `idle` -> `unwind`\n# reverts that diff (intent survives only in history) -> a green-baseline\n# gate -> `design.triage` groups the reverted diff into ordered, classified\n# concerns and resolves the PRODUCT ones' open questions (`design.gate`) ->\n# `architecture.author` works out the *how* and resolves the TECHNICAL ones\n# (`architecture.gate`) -> `architecture.decompose` writes one package file\n# per concern -> the per-package queue (`packages.*`) builds and reviews each\n# -> the shared tail (`build.review.*`) decides sign-off (-> `build.squashing`\n# -> `done`, one commit) vs. actionable feedback (-> `re-unwind` -> back to\n# `design.triage`, a full re-plan lap that never builds on the human's\n# hand-edit). `--entry review-gate.check --var reviewBase=<commitish>` and\n# `--entry fix-precheck` are the two other entries into this same flow.\nvars:\n testCommand: npm test\n # plannerModel: the heavier tier for one-shot triage/design/review turns.\n # coderModel: the tier for build/fix turns. Both repointable via a\n # top-level `vars:` key or a `GTD_<NAME>` env var, like any other var.\n plannerModel: smart\n coderModel: base\n # Declared (not left undeclared) so `--var reviewBase=...` passes the CLI's\n # declared-name check; blank renders empty, which the review entry refuses.\n reviewBase: \"\"\n # gtd's own voice for generated files — adapted from the \"Spartan\" output\n # style (https://github.com/alexgreensh/attention-span, AGPL-3.0, version 0.6),\n # rewritten in gtd's own words for deliverables rather than chat replies;\n # no upstream text ships. A point-in-time derivation with no refresh\n # mechanism.\n styleBlock: |-\n This file is a deliverable, not a chat reply — size follows the work, and\n padding is the target of any cut. Lead with the answer immediately; never\n circle back to restate it once it has landed.\n\n Write flat, commanding sentences — commit to the claim, do not hedge it.\n Favor everyday vocabulary; where a term is unavoidable, define it in five\n words or fewer on first use.\n\n In markdown, bold carries the load: bold the claim, not the sentence around\n it — reading only the bold text must yield the full point and every risk.\n\n Never trim a risk, a number, a threshold, or a scoped condition to save\n space. This outranks every other rule here.\n\n Ship the artifact bare — no lead-in, no sign-off wrapped around it.\n Compressing is not dropping: a task with three load-bearing parts ships all\n three, each shorter, never two of three.\n\n One idea per block; break when the idea shifts. Flag risk in one blunt\n line rather than hedged prose. Never narrate the work — do it.\n styleFormatContract: |-\n This file is machine-read; its format contract outranks every style rule\n above it: keep every `##`/`###` heading, every `- [ ]` checkbox row, and\n every marker line exactly as specified — do not renumber or rename a\n heading or replace a checkbox list with prose. A parser reads these\n literally; a violation refuses the turn outright.\n\n Voice rules above govern the prose between these elements, never the\n elements themselves.\n\n # One persona per prompt-bearing machine below, stamped as its `system:`\n # (see src/StateFields.ts's `system`). `--system-prompt` REPLACES a\n # harness's default system prompt — including its injected cwd/env/\n # git-status block — so each persona is written self-contained, restating\n # that context explicitly rather than relying on it.\n\n # Shared conduct tail, appended after every persona's own role paragraph:\n # use tools without asking, orient yourself with git, and go inspect what\n # the turn's message names. Deliberately NOT named `*Persona`:\n # `templates.test.ts` derives its six-name persona set from\n # `it\\.vars\\.(\\w+Persona)`, so that suffix would silently grow the set.\n agentConduct: |-\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 grant permission, confirm a command, or approve a plan before\n you act. Investigate with real commands rather than assuming what a file,\n a commit, or a decision's status actually is, 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 — no cwd, no branch, no recent history, no tree state. Run\n `git status`, `git log`, `ls` and the like yourself before you do\n anything else; that is the normal first move of this turn, not an extra\n step.\n\n The message you receive each turn names a commit, a range, or a file for\n you to go inspect yourself — `git show`, `git diff`, reading the file\n directly — never a diff or a summary rendered into the conversation. Go\n look before you act. Across the several turns one conversation may span,\n work from what you already read and settled earlier rather than\n re-deriving it from a cold read every time, unless the turn's own\n instructions tell you this is a fresh, cold pass.\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 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 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 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 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, touching neither another package's files nor\n code outside the current task. Treat feedback you're given as the\n diagnosis to act on; discard it, with reason, only when it is simply\n wrong.\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 # The scratchpad-opener sentence shared by every prompt-content state (see\n # STATE_FIELDS's `prompt`) — this workflow's own state files are its private\n # working notes, never project code or documentation a human reads. Injected\n # as the leading line of all eleven prompt bodies that touch a `.gtd/` file\n # directly; each state's own \"the only state file this turn touches is X\"\n # sentence stays local, immediately after the tag. Three states splice a\n # role clause into this same opening sentence (`build.review.collecting`,\n # `packages.item.spec.review`, `build.squashing`) — those hoist the clause to\n # its own leading sentence ahead of the tag instead, keeping every word.\n stateFileRules: |-\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.\n # The open-questions warrant test, decide-it-yourself sink, and `## Open\n # Questions` checkbox shape shared by `design.triage` and\n # `architecture.author` — written phase-agnostic (no PRODUCT/TECHNICAL\n # bake-in) so both sites can inject it verbatim. Each site's own\n # phase-scope sentence (triage: product-only, technical waits; architecture:\n # every point here is TECHNICAL) stays local around the tag — the two\n # genuinely disagree on scope, so that sentence is never shared.\n # `questionBarReturn` is the return-lap half — split out so each site can\n # inject it under its own `## Return lap` heading instead of burying\n # return-lap behaviour inside the first lap's own section.\n questionBar: |-\n For every open point, a question is warranted only when all three hold:\n the answer is a genuine fork with divergent outcomes — user-visible for a\n product question, materially different implementations for a technical\n one; the sketch, the entry diff, and history do not already settle it\n (treat anything the entry diff states explicitly — a directive in\n `.gtd/TODO.md`, a committed choice in a hand-edit — as already settled,\n and on a review loop-back lap treat whatever the human's own review-round\n edit or note states explicitly the same way); and getting it wrong would\n survive all the way to the human review tail.\n\n Anything below that bar, decide yourself: record the decision under\n `## Answered Questions` — the same section a human-answered question\n lands in — as `### <the point phrased as a question>` followed by the\n decision and a one-line rationale in prose, no checkboxes.\n\n A question that clears the bar is written under a `## Open Questions`\n heading near the top and takes this shape: one `### <question>`\n sub-heading each, followed by a checkbox list of candidate answers — two\n concrete options plus a final free-text slot, all left unticked:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself — the human ticks exactly one per question.\n questionBarReturn: |-\n The human has ticked one option per question (or replaced\n `_your answer_` with their own text): fold each chosen answer into its\n concern's prose, then move the question into a `## Answered Questions`\n section as `### <question>` followed by the resolved answer in plain\n prose (drop the checkboxes). Never re-raise a deleted question; treat a\n deleted `## Open Questions` section as acceptance. Omit `## Open\n Questions` entirely once there are none left.\n # The body `packages.item.fix-suite` and `build.fix` share byte for byte —\n # both fix a red `.gtd/FEEDBACK.md`. `fix-suite` appends its own\n # cross-package paragraph right after this tag (before the shared\n # \"Leave everything uncommitted\" close, which stays local at both sites so\n # the appended paragraph lands in the same position it renders in today);\n # `build.fix` renders the tag alone.\n fixFeedbackPrompt: |-\n The only state file this 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\nentry:\n default: unified\n\nmachines:\n # Shared green-baseline gate — start-gate/review-gate/fix-precheck all\n # alias this script via &suiteCheck.\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 set +e\n mkdir -p .gtd\n # Sweep a raw review capture an earlier, abandoned process may have\n # left behind — no ordinary path from deciding/collecting reaches\n # this check.\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 # Stamp with HEAD so a repeat identical failure still re-registers\n # as an M/A edit instead of looking byte-identical (GREEN).\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 # Marks BOTH entryGate instances (start-gate/review-gate); only\n # review-gate strictly needs it, but the shared dedup makes marking\n # one mark both — both ARE entry gates.\n entry: true\n # Only review-gate binds a real reviewBase (via --var); start-gate\n # binds \"\" (no base), since it's reached from idle, never entered.\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: FEEDBACK.md\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $blockedDescribe\n\n # Shared check/escalate pair for build.health.*/packages.item.health.* —\n # the caller's own coder fix-state ($onRed), not this machine, handles red.\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 # Shared check/answer pair for design.gate/architecture.gate. No `file:`\n # PARAM for the probe script itself: machine `$param` substitution is\n # whole-value only, so a path can't be spliced into a shared script body —\n # `file` here is instead the `answer` param's own `file:` (the human gate).\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 # Exactly one of REQUIREMENTS.md/ARCHITECTURE.md exists on disk at\n # a time — architecture.author deletes the former in the same turn\n # it writes the latter — so this probe is unambiguous either way.\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 # The triage phase — one conversation across the whole back-and-forth (the\n # human's answer rationale survives every return lap). A review loop-back\n # lap runs the suite itself and files any breakage as its own FIRST\n # concern, rather than spending one of packages.item.fix-suite's\n # (per-episode) retries on breakage the package didn't cause.\n # ▸ planner identity.\n designPlan:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.designPersona %>\n\n\n <%~ it.vars.agentConduct %>\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 # Guards against discarding assembled review input without folding\n # it in (the original bug this guard was added for).\n requireProgress: true\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this 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 Whichever lap this is, leave `.gtd/REQUIREMENTS.md` uncommitted and\n finish your turn once it reflects this lap's own work below.\n\n ## First lap\n\n `.gtd/REQUIREMENTS.md` does not exist yet (or holds whatever the\n human's own change happened to leave there). Build it from that\n start diff:\n\n ### Group into concerns\n\n 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 ### Classify each concern\n\n Classify each concern as PRODUCT (a user-facing/requirements\n decision) or TECHNICAL (an implementation decision).\n\n <%~ it.vars.questionBar %>\n\n\n Raise questions only for product concerns here — a technical\n concern's open points wait for the next phase. When every concern\n in this lap is TECHNICAL, write no `## Open Questions` section at\n all.\n\n ### Fold in the sketch\n\n 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 ## Return lap\n\n <%~ it.vars.questionBarReturn %>\n\n ## Review loop-back\n\n `.gtd/REQUIREMENTS.md` already holds concerns and none of them\n have a ticked-but-unfolded answer waiting — that shape means a\n completed REVIEW round put this material here, not a question you\n asked. Develop those existing concerns further with whatever the\n round raised; never rediscover or regroup them from a cold read\n the way the first lap 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 the 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 the 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 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 # A separate machine from designPlan (its own memory scope — a COLD read,\n # not a resumed conversation). Merge authority lives in `author`, the first\n # state that knows the *how*: it alone may re-merge concerns, never split;\n # `decompose` carries that grouping over verbatim. ▸ planner identity.\n archPlan:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.architectPersona %>\n\n\n <%~ it.vars.agentConduct %>\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 <%~ it.vars.stateFileRules %>\n\n 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 decision recorded there — plain\n prose or a resolved `## Answered Questions` entry, PRODUCT or\n TECHNICAL alike — as settled; never re-open it.\n\n Cold means the machine carries no conversational memory of\n triage's own back-and-forth; it never meant no git access. This\n process started at commit `<%= it.startCommit %>`. Find the\n process's first turn yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\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,\n or both.\n\n Whichever lap this is, once `.gtd/ARCHITECTURE.md` is written,\n delete `.gtd/REQUIREMENTS.md` — its content has been folded in and\n it must not linger. Leave everything uncommitted and finish your\n turn.\n\n ## First lap\n\n Develop `.gtd/ARCHITECTURE.md` from those concerns: for each one,\n in the same order, work out the *how* — file/module structure,\n data models, library/tech-stack choices, error-handling strategy —\n 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 Every open point here is TECHNICAL — triage already resolved the\n product ones, one phase earlier.\n\n <%~ it.vars.questionBar %>\n\n ## Return lap\n\n <%~ it.vars.questionBarReturn %>\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 <%~ it.vars.stateFileRules %>\n\n 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 # The shared review tail. ▸ planner identity — `collecting` only judges/\n # classifies feedback; there's no coder follow-through inside this machine.\n #\n # Nested inside buildTail (build.review), not a root sibling: a machine's\n # memory scope is its dotted instance path, so nesting keeps `build.fix ->\n # build.health.check -> build.review.* -> build.squashing` (the `--entry\n # fix-precheck` path) inside one scope, so `build.squashing` drafts the\n # finale from the same session that made the fixes. The ordinary path\n # (packages -> build.review -> squashing) gets nothing from this either\n # way, since `build`'s own scope has no prior turn to resume there.\n humanReview:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.reviewerPersona %>\n\n\n <%~ it.vars.agentConduct %>\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` enters via review-gate.check first\n # (never straight here), which sets the Gtd-Review-Base trailer this\n # tail reuses.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this 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`). Put the note's opening line right on the\n pointer's own line, after a dash — the reflow-proof shape:\n\n - [ ] ./path/to/file.ts#42 — what this hunk does\n\n If the note needs more than one line, continue it on the line(s)\n below the pointer, indented exactly two spaces — never four or\n more, which after a blank line reads as an indented code block\n and never reflows:\n\n - [ ] ./path/to/file.ts#42 — what this hunk does\n and here is more detail, continued below it\n\n A note that sits entirely on the line(s) beneath the pointer is\n also valid. Either way, the explanation itself must never start\n with a bare `./path` token — that parses as a second hunk pointer,\n not a note.\n\n This prompt carries no diff — read the changes yourself. The range runs\n from `<%= it.reviewBase %>` to the working tree (committed turns plus\n anything still pending); on a feedback round that base is the previous\n review's boundary, so it covers only what changed since then, not items\n already signed off.\n\n Leave `.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 # Opens the review checkout window (src/ReviewWindow.ts) while\n # resting here. Deleting REVIEW.md is refused by the review-doc\n # guard (src/StepGuards.ts).\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 to the most-recent in-process\n # commit (the process start on the first review).\n reviewBase: true\n script: |\n #!/usr/bin/env sh\n # FEEDBACK iff the human left a REVIEW.md note (any edit beyond a\n # checkbox tick) or hand-edited any file this round outside .gtd/;\n # otherwise a clean sign-off. This turn only CAPTURES the raw\n # material into REVIEW_RAW.md — collecting judges actionability.\n # A/M REVIEW_RAW.md rows are declared before D REVIEW.md so a\n # feedback round (which also deletes REVIEW.md) isn't mistaken for\n # sign-off.\n set +e\n mkdir -p .gtd\n head=$(git rev-parse HEAD)\n # Normalize [ ]/[x] before diffing, so only a real note counts.\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 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 # Outcome is by which paths THIS diff touches: writing REQUIREMENTS.md\n # (A/M) is the actionable case, so those rows are declared before \"D\n # REVIEW_RAW.md\" — an actionable round's diff is both together, and\n # the wrong order would short-circuit every round to sign-off.\n # Consuming REVIEW_RAW.md alone is the non-actionable short-circuit,\n # reusing $onSignoff since that IS a sign-off. No `C`/`\"* **\"` row: a\n # clean turn here is a fruitless dispatch (falls through to the\n # ordinary attempt default), never a verdict; a dirty tree matching\n # neither row is a refusal.\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.\n\n <%~ it.vars.stateFileRules %>\n\n 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 where the definition above doesn't hold — it\n manufactures a lap nothing asked for — and never dismiss a real note\n or edit as approval, which drops feedback the human left.\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 # The per-package review — a separate MIND from the implementer. ▸ planner\n # identity; fixing findings is a coder action (packages.item.fix-spec).\n specReview:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.specReviewerPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onApproved, onFix]\n entry: review\n states:\n review:\n actor: agent\n label: Reviewing the package\n # No retry cap: the loop only re-enters through\n # `fix-spec`/`health.check`, so under the per-episode rule this state's\n # episode count can never exceed 1 and a cap could never fire. The\n # spec-review loop is genuinely unbounded — see docs/configuration.md's\n # `retry` documentation.\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.\n\n <%~ it.vars.stateFileRules %>\n\n 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 # The per-package build queue — identity-free; build identity lives in\n # 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 # Mechanics only — NEXT.md's presence/absence is interpreted by\n # the `on` rows below, never here.\n set +e\n mkdir -p .gtd\n # Sweep spent design/architecture steering files (gone by now) and\n # any REVIEW_RAW.md the review loop-back left behind — the only\n # sweeper on that path before it would leak into the squash finale.\n rm -f .gtd/REQUIREMENTS.md .gtd/ARCHITECTURE.md .gtd/QUESTIONS.md .gtd/REVIEW_RAW.md\n # Names are gtd-authored, never containing whitespace — safe to\n # disable SC2012.\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 # Its own coder machine (below) keeps `packages` itself a single state.\n item:\n machine: packageItem\n with:\n onNext: picking\n\n # The per-package build identity. ▸ coder identity. packages.item.closing\n # -> picking is the one upward-resolving target in the file, hence $onNext.\n packageItem:\n model: <%= it.vars.coderModel %>\n system: |-\n <%~ it.vars.builderPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onNext]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n 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 <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.fixFeedbackPrompt %>\n\n 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 <%~ it.vars.stateFileRules %>\n 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 # Removes the just-reviewed package file (path in NEXT.md) plus\n # leftover spec feedback/evidence, so picking selects the next.\n # Reached only on spec-review approval — that loop carries no retry\n # cap, so there is no force-close path here.\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 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 spec:\n machine: specReview\n with:\n onApproved: closing\n onFix: fix-spec\n\n # The process's build identity. ▸ coder identity — model/system stamped\n # onto squashing/fix.\n buildTail:\n model: <%= it.vars.coderModel %>\n system: |-\n <%~ it.vars.finisherPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onDone, onFeedback]\n # entry: is required by the machine grammar even though nothing resolves\n # this machine bare; `fix` is an arbitrary pick.\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 The process is approved and done.\n\n <%~ it.vars.stateFileRules %>\n\n 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 <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.fixFeedbackPrompt %>\n\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 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 # Nested here (not root) so `squashing` stays inside the builder's own\n # scope on sign-off — see humanReview above.\n review:\n machine: humanReview\n with:\n onSignoff: squashing\n onFeedback: $onFeedback\n\n # WIRING ONLY — the root ties together the machine tree above.\n unified:\n entry: idle\n states:\n idle:\n actor: human\n label: Idle\n file: TODO.md\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 # --entry fix-precheck: a red suite drops into build.fix (-> health ->\n # review + squash tail); a green suite is a no-op back to idle.\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 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 # Separate from start-gate.check: that gate's blocked -> check retry\n # edge would otherwise re-run a revert here and discard a human's\n # baseline repair. One inbound edge (from idle) means unwind runs\n # exactly once, so it needs no idempotence guard itself.\n unwind:\n actor: check\n label: Unwinding your input\n script: |\n #!/usr/bin/env sh\n set +e\n # Hoisted here, 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.\n # Uses it.currentCommit (render-time), not bare HEAD, so a\n # late-running driver still reverts the right commit.\n commit=\"<%~ it.currentCommit %>\"\n git revert --no-commit \"$commit\"\n on:\n \"* **\": start-gate\n\n # Reverts the human's own review-round hand-edit before re-planning,\n # scoped to real code (`.gtd/` excluded — REVIEW.md is already gone by\n # now). Names the commit via it.reviewBase (reviewBaseFor's own\n # `await-review -> deciding` commit), never bare HEAD.\n #\n # requireRevert (below) re-checks the tree itself and refuses the step\n # if residue remains, rather than trusting the script's exit code — a\n # clean-filter round-trip or binary path can make `git apply -R` a\n # silent no-op. Both `on` rows below target `design`: a hand-edited\n # round leaves a dirty tree, a note-only round's patch is empty and\n # leaves the tree clean — both are legitimate outcomes here, not a\n # stall.\n #\n # `file: REVIEW.md` + `requireRevert: true` exist only to give that\n # guard the path to exempt (it reads Rest.hints.file, never this\n # literal) — REVIEW.md is already gone two commits earlier, so this\n # state never renders it.\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 # Scoped revert of the human's review-round edit — .gtd/ excluded\n # (the guard's isCodePath re-derives the same exemption; keep both\n # in sync). Expected to succeed; requireRevert catches a silent\n # apply failure.\n set +e\n # Hoisted here, 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.\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 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 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 design:\n machine: designPlan\n with:\n onArchitecture: architecture\n\n architecture:\n machine: archPlan\n with:\n onPackages: packages\n\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n build:\n machine: buildTail\n with:\n onDone: done\n onFeedback: re-unwind\n";
191878
+ var unified_default = "# gtd's BUILT-IN DEFAULT workflow — used when no `workflow:` key is\n# configured anywhere in the cwd→home config chain; `gtd init` seeds only\n# vars.testCommand + a modes: suggestion, never this file. Compiled through\n# the same `compileWorkflowConfig` a user's own `workflow:` goes through — no\n# privileged path. See skills/authoring/SKILL.md for how to read or edit it\n# (machines, patterns, templates) and docs/driver.md for how a driver runs it.\n#\n# One flow: any change to the tree starts the process. `idle` -> `unwind`\n# reverts that diff (intent survives only in history) -> a green-baseline\n# gate -> `design.triage` groups the reverted diff into ordered, classified\n# concerns and resolves the PRODUCT ones' open questions (`design.gate`) ->\n# `architecture.author` works out the *how* and resolves the TECHNICAL ones\n# (`architecture.gate`) -> `architecture.decompose` writes one package file\n# per concern -> the per-package queue (`packages.*`) builds and reviews each\n# -> the shared tail (`build.review.*`) decides sign-off (-> `idle`, landing\n# an ordinary commit that closes the process — every per-turn commit stays on\n# the branch; run `gtd summary` afterward for a closing-message prompt) vs.\n# actionable feedback (-> `re-unwind` -> back to `design.triage`, a full\n# re-plan lap that never builds on the human's hand-edit). `--entry\n# review-gate.check --var reviewBase=<commitish>` and `--entry fix-precheck`\n# are the two other entries into this same flow.\nvars:\n testCommand: npm test\n # plannerModel: the heavier tier for one-shot triage/design/review turns.\n # coderModel: the tier for build/fix turns. Both repointable via a\n # top-level `vars:` key or a `GTD_<NAME>` env var, like any other var.\n plannerModel: smart\n coderModel: base\n # Declared (not left undeclared) so `--var reviewBase=...` passes the CLI's\n # declared-name check; blank renders empty, which the review entry refuses.\n reviewBase: \"\"\n # gtd's own voice for generated files — adapted from the \"Spartan\" output\n # style (https://github.com/alexgreensh/attention-span, AGPL-3.0, version 0.6),\n # rewritten in gtd's own words for deliverables rather than chat replies;\n # no upstream text ships. A point-in-time derivation with no refresh\n # mechanism.\n styleBlock: |-\n This file is a deliverable, not a chat reply — size follows the work, and\n padding is the target of any cut. Lead with the answer immediately; never\n circle back to restate it once it has landed.\n\n Write flat, commanding sentences — commit to the claim, do not hedge it.\n Favor everyday vocabulary; where a term is unavoidable, define it in five\n words or fewer on first use.\n\n In markdown, bold carries the load: bold the claim, not the sentence around\n it — reading only the bold text must yield the full point and every risk.\n\n Never trim a risk, a number, a threshold, or a scoped condition to save\n space. This outranks every other rule here.\n\n Ship the artifact bare — no lead-in, no sign-off wrapped around it.\n Compressing is not dropping: a task with three load-bearing parts ships all\n three, each shorter, never two of three.\n\n One idea per block; break when the idea shifts. Flag risk in one blunt\n line rather than hedged prose. Never narrate the work — do it.\n styleFormatContract: |-\n This file is machine-read; its format contract outranks every style rule\n above it: keep every `##`/`###` heading, every `- [ ]` checkbox row, and\n every marker line exactly as specified — do not renumber or rename a\n heading or replace a checkbox list with prose. A parser reads these\n literally; a violation refuses the turn outright.\n\n Voice rules above govern the prose between these elements, never the\n elements themselves.\n\n # One persona per prompt-bearing machine below, stamped as its `system:`\n # (see src/StateFields.ts's `system`). `--system-prompt` REPLACES a\n # harness's default system prompt — including its injected cwd/env/\n # git-status block — so each persona is written self-contained, restating\n # that context explicitly rather than relying on it.\n\n # Shared conduct tail, appended after every persona's own role paragraph:\n # use tools without asking, orient yourself with git, and go inspect what\n # the turn's message names. Deliberately NOT named `*Persona`:\n # `templates.test.ts` derives its six-name persona set from\n # `it\\.vars\\.(\\w+Persona)`, so that suffix would silently grow the set.\n agentConduct: |-\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 grant permission, confirm a command, or approve a plan before\n you act. Investigate with real commands rather than assuming what a file,\n a commit, or a decision's status actually is, 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 — no cwd, no branch, no recent history, no tree state. Run\n `git status`, `git log`, `ls` and the like yourself before you do\n anything else; that is the normal first move of this turn, not an extra\n step.\n\n The message you receive each turn names a commit, a range, or a file for\n you to go inspect yourself — `git show`, `git diff`, reading the file\n directly — never a diff or a summary rendered into the conversation. Go\n look before you act. Across the several turns one conversation may span,\n work from what you already read and settled earlier rather than\n re-deriving it from a cold read every time, unless the turn's own\n instructions tell you this is a fresh, cold pass.\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 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 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 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 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, touching neither another package's files nor\n code outside the current task. Treat feedback you're given as the\n diagnosis to act on; discard it, with reason, only when it is simply\n wrong.\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 it closes. You are\n handed a late-breaking failing check after sign-off; fix it, focused and\n minimal, before the process can close. Every turn you land stays its own\n commit on the branch — nothing here gets squashed away.\n\n # The scratchpad-opener sentence shared by every prompt-content state (see\n # STATE_FIELDS's `prompt`) — this workflow's own state files are its private\n # working notes, never project code or documentation a human reads. Injected\n # as the leading line of every prompt body that touches a `.gtd/` file\n # directly; each state's own \"the only state file this turn touches is X\"\n # sentence stays local, immediately after the tag. Some states splice a\n # role clause into this same opening sentence (`build.review.collecting`,\n # `packages.item.spec.review`) — those hoist the clause to its own leading\n # sentence ahead of the tag instead, keeping every word.\n stateFileRules: |-\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.\n # The open-questions warrant test, decide-it-yourself sink, and `## Open\n # Questions` checkbox shape shared by `design.triage` and\n # `architecture.author` — written phase-agnostic (no PRODUCT/TECHNICAL\n # bake-in) so both sites can inject it verbatim. Each site's own\n # phase-scope sentence (triage: product-only, technical waits; architecture:\n # every point here is TECHNICAL) stays local around the tag — the two\n # genuinely disagree on scope, so that sentence is never shared.\n # `questionBarReturn` is the return-lap half — split out so each site can\n # inject it under its own `## Return lap` heading instead of burying\n # return-lap behaviour inside the first lap's own section.\n questionBar: |-\n For every open point, a question is warranted only when all three hold:\n the answer is a genuine fork with divergent outcomes — user-visible for a\n product question, materially different implementations for a technical\n one; the sketch, the entry diff, and history do not already settle it\n (treat anything the entry diff states explicitly — a directive in\n `.gtd/TODO.md`, a committed choice in a hand-edit — as already settled,\n and on a review loop-back lap treat whatever the human's own review-round\n edit or note states explicitly the same way); and getting it wrong would\n survive all the way to the human review tail.\n\n Anything below that bar, decide yourself: record the decision under\n `## Answered Questions` — the same section a human-answered question\n lands in — as `### <the point phrased as a question>` followed by the\n decision and a one-line rationale in prose, no checkboxes.\n\n A question that clears the bar is written under a `## Open Questions`\n heading near the top and takes this shape: one `### <question>`\n sub-heading each, followed by a checkbox list of candidate answers — two\n concrete options plus a final free-text slot, all left unticked:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself — the human ticks exactly one per question.\n questionBarReturn: |-\n The human has ticked one option per question (or replaced\n `_your answer_` with their own text): fold each chosen answer into its\n concern's prose, then move the question into a `## Answered Questions`\n section as `### <question>` followed by the resolved answer in plain\n prose (drop the checkboxes). Never re-raise a deleted question; treat a\n deleted `## Open Questions` section as acceptance. Omit `## Open\n Questions` entirely once there are none left.\n # The body `packages.item.fix-suite` and `build.fix` share byte for byte —\n # both fix a red `.gtd/FEEDBACK.md`. `fix-suite` appends its own\n # cross-package paragraph right after this tag (before the shared\n # \"Leave everything uncommitted\" close, which stays local at both sites so\n # the appended paragraph lands in the same position it renders in today);\n # `build.fix` renders the tag alone.\n fixFeedbackPrompt: |-\n The only state file this 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# `gtd summary`'s prompt — printed cold, no session identity, no diff inlined.\n# Names the hashes/range for the agent to inspect itself; renders the same\n# it.processCost/it.processCostByModel the old squash finale used to.\nsummary: |\n <%~ it.vars.styleBlock %>\n\n\n Write the closing message for the process HEAD closes or currently sits\n inside — the text a driver may use for a squash, an amend, or a pull\n request body. You are starting cold: read every decision back out of the\n commits named below rather than assuming any prior context.\n\n Cover the initial motivation, the important decisions, and the trade-offs\n made, and the high-level architectural changes — never which files\n changed; that is what `git diff --stat` is for, not this message.\n\n The process's entry commit is `<%= it.entryCommit %>`. <% if\n (it.humanCommits.length > 0) { %>The human contributed at these commits,\n oldest to newest:\n <% it.humanCommits.forEach(function (c) { %>\n - `<%= c.hash %>` (entering `<%= c.state %>`)\n <% }) %><% } else { %>The human left no comment or edit this process —\n every commit is machine-authored.<% } %>\n\n Inspect the range yourself: `git log <%= it.processBase %>..<%= it.processTip %>`\n and `git diff <%= it.processBase %> <%= it.processTip %>`. That range is\n exactly what a squash or PR body should describe.\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n\n Print the closing message and stop — this command writes nothing itself.\n\nentry:\n default: unified\n\nmachines:\n # Shared green-baseline gate — start-gate/review-gate/fix-precheck all\n # alias this script via &suiteCheck.\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 set +e\n mkdir -p .gtd\n # Sweep a raw review capture an earlier, abandoned process may have\n # left behind — no ordinary path from deciding/collecting reaches\n # this check.\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 # Stamp with HEAD so a repeat identical failure still re-registers\n # as an M/A edit instead of looking byte-identical (GREEN).\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 # Marks BOTH entryGate instances (start-gate/review-gate); only\n # review-gate strictly needs it, but the shared dedup makes marking\n # one mark both — both ARE entry gates.\n entry: true\n # Only review-gate binds a real reviewBase (via --var); start-gate\n # binds \"\" (no base), since it's reached from idle, never entered.\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: FEEDBACK.md\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $blockedDescribe\n\n # Shared check/escalate pair for build.health.*/packages.item.health.* —\n # the caller's own coder fix-state ($onRed), not this machine, handles red.\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 # Shared check/answer pair for design.gate/architecture.gate. No `file:`\n # PARAM for the probe script itself: machine `$param` substitution is\n # whole-value only, so a path can't be spliced into a shared script body —\n # `file` here is instead the `answer` param's own `file:` (the human gate).\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 # Exactly one of REQUIREMENTS.md/ARCHITECTURE.md exists on disk at\n # a time — architecture.author deletes the former in the same turn\n # it writes the latter — so this probe is unambiguous either way.\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 # The triage phase — one conversation across the whole back-and-forth (the\n # human's answer rationale survives every return lap). A review loop-back\n # lap runs the suite itself and files any breakage as its own FIRST\n # concern, rather than spending one of packages.item.fix-suite's\n # (per-episode) retries on breakage the package didn't cause.\n # ▸ planner identity.\n designPlan:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.designPersona %>\n\n\n <%~ it.vars.agentConduct %>\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 # Guards against discarding assembled review input without folding\n # it in (the original bug this guard was added for).\n requireProgress: true\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this 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 Whichever lap this is, leave `.gtd/REQUIREMENTS.md` uncommitted and\n finish your turn once it reflects this lap's own work below.\n\n ## First lap\n\n `.gtd/REQUIREMENTS.md` does not exist yet (or holds whatever the\n human's own change happened to leave there). Build it from that\n start diff:\n\n ### Group into concerns\n\n 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 ### Classify each concern\n\n Classify each concern as PRODUCT (a user-facing/requirements\n decision) or TECHNICAL (an implementation decision).\n\n <%~ it.vars.questionBar %>\n\n\n Raise questions only for product concerns here — a technical\n concern's open points wait for the next phase. When every concern\n in this lap is TECHNICAL, write no `## Open Questions` section at\n all.\n\n ### Fold in the sketch\n\n 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 ## Return lap\n\n <%~ it.vars.questionBarReturn %>\n\n ## Review loop-back\n\n `.gtd/REQUIREMENTS.md` already holds concerns and none of them\n have a ticked-but-unfolded answer waiting — that shape means a\n completed REVIEW round put this material here, not a question you\n asked. Develop those existing concerns further with whatever the\n round raised; never rediscover or regroup them from a cold read\n the way the first lap 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 the 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 the 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 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 # A separate machine from designPlan (its own memory scope — a COLD read,\n # not a resumed conversation). Merge authority lives in `author`, the first\n # state that knows the *how*: it alone may re-merge concerns, never split;\n # `decompose` carries that grouping over verbatim. ▸ planner identity.\n archPlan:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.architectPersona %>\n\n\n <%~ it.vars.agentConduct %>\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 <%~ it.vars.stateFileRules %>\n\n 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 decision recorded there — plain\n prose or a resolved `## Answered Questions` entry, PRODUCT or\n TECHNICAL alike — as settled; never re-open it.\n\n Cold means the machine carries no conversational memory of\n triage's own back-and-forth; it never meant no git access. This\n process started at commit `<%= it.startCommit %>`. Find the\n process's first turn yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\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,\n or both.\n\n Whichever lap this is, once `.gtd/ARCHITECTURE.md` is written,\n delete `.gtd/REQUIREMENTS.md` — its content has been folded in and\n it must not linger. Leave everything uncommitted and finish your\n turn.\n\n ## First lap\n\n Develop `.gtd/ARCHITECTURE.md` from those concerns: for each one,\n in the same order, work out the *how* — file/module structure,\n data models, library/tech-stack choices, error-handling strategy —\n 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 Every open point here is TECHNICAL — triage already resolved the\n product ones, one phase earlier.\n\n <%~ it.vars.questionBar %>\n\n ## Return lap\n\n <%~ it.vars.questionBarReturn %>\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 <%~ it.vars.stateFileRules %>\n\n 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 # The shared review tail. ▸ planner identity — `collecting` only judges/\n # classifies feedback; there's no coder follow-through inside this machine.\n #\n # Nested inside buildTail (build.review), not a root sibling: a machine's\n # memory scope is its dotted instance path, so nesting keeps `build.fix ->\n # build.health.check -> build.review.*` (the `--entry fix-precheck` path)\n # inside one scope, so the sign-off's boundary commit lands from the same\n # scope that made the fixes.\n humanReview:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.reviewerPersona %>\n\n\n <%~ it.vars.agentConduct %>\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` enters via review-gate.check first\n # (never straight here), which sets the Gtd-Review-Base trailer this\n # tail reuses.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this 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`). Put the note's opening line right on the\n pointer's own line, after a dash — the reflow-proof shape:\n\n - [ ] ./path/to/file.ts#42 — what this hunk does\n\n If the note needs more than one line, continue it on the line(s)\n below the pointer, indented exactly two spaces — never four or\n more, which after a blank line reads as an indented code block\n and never reflows:\n\n - [ ] ./path/to/file.ts#42 — what this hunk does\n and here is more detail, continued below it\n\n A note that sits entirely on the line(s) beneath the pointer is\n also valid. Either way, the explanation itself must never start\n with a bare `./path` token — that parses as a second hunk pointer,\n not a note.\n\n This prompt carries no diff — read the changes yourself. The range runs\n from `<%= it.reviewBase %>` to the working tree (committed turns plus\n anything still pending); on a feedback round that base is the previous\n review's boundary, so it covers only what changed since then, not items\n already signed off.\n\n Leave `.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 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 Review the diff yourself, with whatever tool you like — gtd checks\n nothing out and touches no ref; `gtd base` prints this same hash any\n time you need it again. The range runs from the review base to the\n working tree:\n\n git diff <%= it.reviewBase %>\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 close the process,\n whatever the boxes say. Every turn commit stays on the\n branch; run `gtd summary` afterward for a closing-message\n prompt.\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 to the most-recent in-process\n # commit (the process start on the first review).\n reviewBase: true\n script: |\n #!/usr/bin/env sh\n # FEEDBACK iff the human left a REVIEW.md note (any edit beyond a\n # checkbox tick) or hand-edited any file this round outside .gtd/;\n # otherwise a clean sign-off. This turn only CAPTURES the raw\n # material into REVIEW_RAW.md — collecting judges actionability.\n # A/M REVIEW_RAW.md rows are declared before D REVIEW.md so a\n # feedback round (which also deletes REVIEW.md) isn't mistaken for\n # sign-off.\n set +e\n mkdir -p .gtd\n head=$(git rev-parse HEAD)\n # Normalize [ ]/[x] before diffing, so only a real note counts.\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 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 # Outcome is by which paths THIS diff touches: writing REQUIREMENTS.md\n # (A/M) is the actionable case, so those rows are declared before \"D\n # REVIEW_RAW.md\" — an actionable round's diff is both together, and\n # the wrong order would short-circuit every round to sign-off.\n # Consuming REVIEW_RAW.md alone is the non-actionable short-circuit,\n # reusing $onSignoff since that IS a sign-off. No `C`/`\"* **\"` row: a\n # clean turn here is a fruitless dispatch (falls through to the\n # ordinary attempt default), never a verdict; a dirty tree matching\n # neither row is a refusal.\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.\n\n <%~ it.vars.stateFileRules %>\n\n 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 where the definition above doesn't hold — it\n manufactures a lap nothing asked for — and never dismiss a real note\n or edit as approval, which drops feedback the human left.\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 # The per-package review — a separate MIND from the implementer. ▸ planner\n # identity; fixing findings is a coder action (packages.item.fix-spec).\n specReview:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.specReviewerPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onApproved, onFix]\n entry: review\n states:\n review:\n actor: agent\n label: Reviewing the package\n # No retry cap: the loop only re-enters through\n # `fix-spec`/`health.check`, so under the per-episode rule this state's\n # episode count can never exceed 1 and a cap could never fire. The\n # spec-review loop is genuinely unbounded — see docs/configuration.md's\n # `retry` documentation.\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.\n\n <%~ it.vars.stateFileRules %>\n\n 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 # The per-package build queue — identity-free; build identity lives in\n # 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 # Mechanics only — NEXT.md's presence/absence is interpreted by\n # the `on` rows below, never here.\n set +e\n mkdir -p .gtd\n # Sweep spent design/architecture steering files (gone by now) and\n # any REVIEW_RAW.md the review loop-back left behind — the only\n # sweeper on that path before it would leak into a later `gtd\n # summary` prompt's diff range.\n rm -f .gtd/REQUIREMENTS.md .gtd/ARCHITECTURE.md .gtd/QUESTIONS.md .gtd/REVIEW_RAW.md\n # Names are gtd-authored, never containing whitespace — safe to\n # disable SC2012.\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 # Its own coder machine (below) keeps `packages` itself a single state.\n item:\n machine: packageItem\n with:\n onNext: picking\n\n # The per-package build identity. ▸ coder identity. packages.item.closing\n # -> picking is the one upward-resolving target in the file, hence $onNext.\n packageItem:\n model: <%= it.vars.coderModel %>\n system: |-\n <%~ it.vars.builderPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onNext]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n 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 <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.fixFeedbackPrompt %>\n\n 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 <%~ it.vars.stateFileRules %>\n 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 # Removes the just-reviewed package file (path in NEXT.md) plus\n # leftover spec feedback/evidence, so picking selects the next.\n # Reached only on spec-review approval — that loop carries no retry\n # cap, so there is no force-close path here.\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 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 spec:\n machine: specReview\n with:\n onApproved: closing\n onFix: fix-spec\n\n # The process's build identity. ▸ coder identity — model/system stamped\n # onto fix.\n buildTail:\n model: <%= it.vars.coderModel %>\n system: |-\n <%~ it.vars.finisherPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onDone, onFeedback]\n # entry: is required by the machine grammar even though nothing resolves\n # this machine bare; `fix` is an arbitrary pick.\n entry: fix\n states:\n fix:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.fixFeedbackPrompt %>\n\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 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 # Nested here (not root) so the sign-off's boundary commit stays inside\n # the builder's own scope — see humanReview above.\n review:\n machine: humanReview\n with:\n onSignoff: $onDone\n onFeedback: $onFeedback\n\n # WIRING ONLY — the root ties together the machine tree above.\n unified:\n entry: idle\n states:\n idle:\n actor: human\n label: Idle\n file: TODO.md\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 # --entry fix-precheck: a red suite drops into build.fix (-> health ->\n # review tail); a green suite is a no-op back to idle.\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 # Separate from start-gate.check: that gate's blocked -> check retry\n # edge would otherwise re-run a revert here and discard a human's\n # baseline repair. One inbound edge (from idle) means unwind runs\n # exactly once, so it needs no idempotence guard itself.\n unwind:\n actor: check\n label: Unwinding your input\n script: |\n #!/usr/bin/env sh\n set +e\n # Hoisted here, 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.\n # Uses it.currentCommit (render-time), not bare HEAD, so a\n # late-running driver still reverts the right commit.\n commit=\"<%~ it.currentCommit %>\"\n git revert --no-commit \"$commit\"\n on:\n \"* **\": start-gate\n\n # Reverts the human's own review-round hand-edit before re-planning,\n # scoped to real code (`.gtd/` excluded — REVIEW.md is already gone by\n # now). Names the commit via it.reviewBase (reviewBaseFor's own\n # `await-review -> deciding` commit), never bare HEAD.\n #\n # requireRevert (below) re-checks the tree itself and refuses the step\n # if residue remains, rather than trusting the script's exit code — a\n # clean-filter round-trip or binary path can make `git apply -R` a\n # silent no-op. Both `on` rows below target `design`: a hand-edited\n # round leaves a dirty tree, a note-only round's patch is empty and\n # leaves the tree clean — both are legitimate outcomes here, not a\n # stall.\n #\n # `file: REVIEW.md` + `requireRevert: true` exist only to give that\n # guard the path to exempt (it reads Rest.hints.file, never this\n # literal) — REVIEW.md is already gone two commits earlier, so this\n # state never renders it.\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 # Scoped revert of the human's review-round edit — .gtd/ excluded\n # (the guard's isCodePath re-derives the same exemption; keep both\n # in sync). Expected to succeed; requireRevert catches a silent\n # apply failure.\n set +e\n # Hoisted here, 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.\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 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 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 design:\n machine: designPlan\n with:\n onArchitecture: architecture\n\n architecture:\n machine: archPlan\n with:\n onPackages: packages\n\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n build:\n machine: buildTail\n with:\n onDone: idle\n onFeedback: re-unwind\n";
191899
191879
  //#endregion
191900
191880
  //#region src/workflows/templates.ts
191901
191881
  const SCHEMA_URL = "https://cdn.jsdelivr.net/npm/@pmelab/gtd/schema.json";
@@ -191980,7 +191960,7 @@ const JSON_TYPE = {
191980
191960
  /** One state's shape — `properties` is derived from `STATE_FIELD_ENTRIES` (every `authored: "state"` field, in table order); see `ConfigSchema.test.ts`. */
191981
191961
  const stateJsonSchema = {
191982
191962
  type: "object",
191983
- description: "One workflow state. Declare exactly one content kind (script/prompt/message/commit). A commit state is final: no actor, no on.",
191963
+ description: "One workflow state. Declare exactly one content kind (script/prompt/message).",
191984
191964
  additionalProperties: false,
191985
191965
  properties: Object.fromEntries(STATE_FIELD_ENTRIES.filter(([, spec]) => spec.authored === "state").map(([key, spec]) => [key, spec.jsonSchema ?? {
191986
191966
  ...JSON_TYPE[spec.kind],
@@ -192007,12 +191987,16 @@ const machineRefJsonSchema = {
192007
191987
  /** The whole `workflow:` value; `PatternConfig.ts`'s compiler is the authoritative schema, this is the editor's first net. */
192008
191988
  const workflowJsonSchema = {
192009
191989
  type: "object",
192010
- 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).",
191990
+ 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 summary: template). 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).",
192011
191991
  additionalProperties: false,
192012
191992
  required: ["entry", "machines"],
192013
191993
  properties: {
192014
191994
  vars: varsJsonSchema,
192015
191995
  modes: modesJsonSchema,
191996
+ summary: {
191997
+ type: "string",
191998
+ description: "Eta template rendered by `gtd summary`: instructions plus commit hashes (it.entryCommit, it.humanCommits, it.processBase, it.processTip) for an agent to write the process's own closing message. A ./ or ../ value is inlined from the config file's directory at load time. Absent is legal (gtd summary refuses); present-but-blank is a load error."
191999
+ },
192016
192000
  entry: {
192017
192001
  type: "object",
192018
192002
  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.",
@@ -192513,8 +192497,7 @@ const makeGitImpl = (executor, root) => {
192513
192497
  updateRef: (ref, hash) => exec("git", "update-ref", ref, hash).pipe(asVoid),
192514
192498
  deleteRef: (ref) => exec("git", "update-ref", "-d", ref).pipe(asVoid),
192515
192499
  mixedResetTo: (ref) => exec("git", "reset", "--mixed", ref).pipe(asVoid),
192516
- hardResetTo: (ref) => exec("git", "reset", "--hard", ref).pipe(asVoid),
192517
- restoreStagedFrom: (source, paths) => paths.length === 0 ? _void : exec("git", "restore", "--staged", `--source=${source}`, "--", ...paths).pipe(catchIf((e) => !isIndexLockError(e), () => _void))
192500
+ hardResetTo: (ref) => exec("git", "reset", "--hard", ref).pipe(asVoid)
192518
192501
  };
192519
192502
  };
192520
192503
  const makeLiveEffect = gen(function* () {
@@ -193123,7 +193106,7 @@ const varsOnlyContext = (vars, state = "") => ({
193123
193106
  state,
193124
193107
  actor: "",
193125
193108
  reviewBase: "",
193126
- retainedBase: "",
193109
+ processBase: "",
193127
193110
  processCost: 0,
193128
193111
  processCostByModel: [],
193129
193112
  read: (path) => {
@@ -193141,26 +193124,19 @@ const renderStateTemplate = (template, context) => eta.renderString(template, co
193141
193124
  const renderModeCommand = (template, context) => eta.renderString(template, context);
193142
193125
  //#endregion
193143
193126
  //#region src/RetainedHistory.ts
193144
- /** Per-worktree (`refs/worktree/gtd/*`, like `ReviewWindow.ts`'s refs) so linked worktrees sharing one `.git` don't clobber each other's retained history. */
193127
+ /** Per-worktree (`refs/worktree/gtd/*`) so linked worktrees sharing one `.git` don't clobber each other's retained history. */
193145
193128
  const HISTORY_REF = "refs/worktree/gtd/history";
193146
- const HISTORY_TRAILER_PREFIX = "Gtd-History: ";
193147
- const HISTORY_TRAILER_RE = /^Gtd-History:[ \t]*(\S+)[ \t]*$/m;
193148
- /** Appends a `Gtd-History: <hash>` trailer recording the pre-squash tip, for `gtd restore` to read back later. */
193149
- const withHistoryTrailer = (subject, hash) => `${subject}\n\n${HISTORY_TRAILER_PREFIX}${hash}`;
193150
- const parseHistoryTrailer = (message) => HISTORY_TRAILER_RE.exec(message)?.[1];
193151
193129
  const readRetainedHistory = (git) => git.readRefOption(HISTORY_REF);
193152
193130
  /**
193153
- * Whether resetting to `tipHash` is safe: either HEAD is the squash commit
193154
- * that still carries the matching `Gtd-History:` trailer (resetting discards
193155
- * only that one commit), or HEAD is an ancestor of `tipHash` (nothing on top
193156
- * would be lost). Anything else is refused.
193131
+ * Whether resetting to `tipHash` is safe: HEAD must be an ancestor of
193132
+ * `tipHash` (nothing on top of the retained tip would be lost). Anything else
193133
+ * is refused.
193157
193134
  */
193158
- const restorability = (git, headHash, headMessage, tipHash) => gen(function* () {
193159
- if (parseHistoryTrailer(headMessage) === tipHash) return { ok: true };
193135
+ const restorability = (git, headHash, tipHash) => gen(function* () {
193160
193136
  if (yield* git.isAncestor(headHash, tipHash)) return { ok: true };
193161
193137
  return {
193162
193138
  ok: false,
193163
- reason: "HEAD has advanced past the squash — restoring would discard commits built on top of it"
193139
+ reason: "HEAD has advanced past the retained tip — restoring would discard commits built on top of it"
193164
193140
  };
193165
193141
  });
193166
193142
  //#endregion
@@ -193187,39 +193163,11 @@ const commitAllowEmpty = (message) => {
193187
193163
  };
193188
193164
  /** `git add -A` then `commitAllowEmpty`, joined with `&&` so a failing `git add` never reaches the commit. */
193189
193165
  const commitAll = (message) => `git add -A &&\n${commitAllowEmpty(message)}`;
193190
- const commitAsIs = (message) => commitAllowEmpty(message);
193191
- const softResetTo = (ref) => `git reset --soft ${shellQuote(ref)}`;
193192
193166
  const mixedResetTo = (ref) => `git reset --mixed ${shellQuote(ref)}`;
193193
193167
  const hardResetTo = (ref) => `git reset --hard ${shellQuote(ref)}`;
193194
- /**
193195
- * `git add -A` then `git reset --hard HEAD` — discards every pending change,
193196
- * tracked or untracked. Joined with `&&`: a failed stage must not reach the
193197
- * hard reset, or untracked survivors remain — the exact outcome this builder
193198
- * exists to avoid.
193199
- */
193200
- const discardPending = () => `git add -A && git reset --hard HEAD`;
193201
193168
  const updateRef = (ref, hash) => `git update-ref ${shellQuote(ref)} ${shellQuote(hash)}`;
193202
193169
  /** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
193203
193170
  const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
193204
- /**
193205
- * `git restore --staged --source=<source> -- <paths…>`, tolerant of a missing
193206
- * ref or no path matching at `source` (mirrors `Git.ts`'s `restoreStagedFrom`)
193207
- * but NOT of `index.lock` contention — swallowing that here would silently
193208
- * skip the `.gtd/` index pin and leak plumbing into the review diff. Empty
193209
- * `paths` emits nothing, mirroring the Effect implementation's own skip.
193210
- */
193211
- const restoreStagedFrom = (source, paths) => {
193212
- if (paths.length === 0) return "";
193213
- const pathArgs = pathspec(paths);
193214
- return [
193215
- `if ! out=$(git restore --staged --source=${shellQuote(source)} -- ${pathArgs} 2>&1); then`,
193216
- ` case "$out" in`,
193217
- ` *"index.lock"*|*"Another git process seems to be running"*) printf '%s\\n' "$out" >&2; exit 1 ;;`,
193218
- ` *) : ;;`,
193219
- ` esac`,
193220
- `fi`
193221
- ].join("\n");
193222
- };
193223
193171
  //#endregion
193224
193172
  //#region src/OutcomeScript.ts
193225
193173
  /** The plain-text twin of `printfLine`: substitutes `args` into `fmt`'s `%s` placeholders, in order. */
@@ -193330,7 +193278,7 @@ const OUTCOME_PREAMBLE = [
193330
193278
  ].join("\n");
193331
193279
  /** `gtd_report_transition <from> <to>` — a landed self-loop/target-change transition, with its changed-file rows. */
193332
193280
  const transitionOutcome = (from, to) => `gtd_report_transition ${shellQuote(from)} ${shellQuote(to)}`;
193333
- /** `gtd_report_commit <subject>` — a bare capture or a squash's final commit, with its changed-file rows. */
193281
+ /** `gtd_report_commit <subject>` — a bare capture (a self-loop commit), with its changed-file rows. */
193334
193282
  const commitOutcome = (subject) => `gtd_report_commit ${shellQuote(subject)}`;
193335
193283
  /** `gtd_report_note <text>` — one already-rendered plain line, no marker. */
193336
193284
  const noteOutcome = (text) => `gtd_report_note ${shellQuote(text)}`;
@@ -193368,12 +193316,6 @@ const headAssertion = (expectedHead) => {
193368
193316
  * same `set -eu` safety `headAssertion` documents.
193369
193317
  */
193370
193318
  const fileExistsGuard = (file) => `[ -f ${shellQuote(file)} ] || exit 0`;
193371
- /** Same shape as `headAssertion`, for the saved-head ref a review window pins; `--verify --quiet` reads a missing ref as an empty string instead of erroring the substitution. */
193372
- const reviewWindowAssertion = (ref, expectedHash) => {
193373
- const refQ = shellQuote(ref);
193374
- const hashQ = shellQuote(expectedHash);
193375
- return `[ "$(git rev-parse --verify --quiet ${refQ} 2>/dev/null)" = ${hashQ} ] || { printf 'gtd: review window ref %s changed since this script was generated (expected %s) — re-run gtd\\n' ${refQ} ${hashQ} >&2; exit 1; }`;
193376
- };
193377
193319
  /**
193378
193320
  * Wraps `command` so a non-zero exit prints `prompt` plus the command's
193379
193321
  * captured output before propagating that exit code. The `{ … }` group lets
@@ -193443,7 +193385,6 @@ const assembleScript = (preconditions, steps) => {
193443
193385
  if (steps.length === 0) return "";
193444
193386
  const sections = ["set -eu"];
193445
193387
  if (preconditions.expectedHead !== void 0) sections.push(headAssertion(preconditions.expectedHead));
193446
- if (preconditions.reviewWindow) sections.push(reviewWindowAssertion(preconditions.reviewWindow.ref, preconditions.reviewWindow.expectedHash));
193447
193388
  if (steps.some((step) => step.kind === "gitWrite")) sections.push(RETRY_HELPER);
193448
193389
  if (steps.some((step) => step.kind === "outcome")) sections.push(OUTCOME_PREAMBLE);
193449
193390
  sections.push(...steps.map(renderStep));
@@ -193478,14 +193419,7 @@ const combinedScript = (required, optional) => {
193478
193419
  };
193479
193420
  //#endregion
193480
193421
  //#region src/Edge.ts
193481
- const EMPTY_TREE$2 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
193482
- /**
193483
- * The review checkout window's saved-head ref — duplicated from
193484
- * `src/ReviewWindow.ts`'s own `REVIEW_HEAD_REF` rather than imported, to avoid
193485
- * a circular module dependency (`ReviewWindow.ts` imports FROM this module).
193486
- * Read (never written) here; the window itself is opened/closed elsewhere.
193487
- */
193488
- const REVIEW_HEAD_REF$1 = "refs/worktree/gtd/review-head";
193422
+ const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
193489
193423
  const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
193490
193424
  const COST_TRAILER_PREFIX = "Gtd-Cost: ";
193491
193425
  const COST_TRAILER_RE = /^Gtd-Cost:[ \t]*([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+?))?[ \t]*$/gm;
@@ -193547,8 +193481,8 @@ const normalizeStatus = (raw) => raw === "A" ? "A" : raw === "D" ? "D" : "M";
193547
193481
  * process left resting at a state a workflow upgrade removed would otherwise
193548
193482
  * look like a fresh idle repo instead of pointing at `gtd abandon`. Every
193549
193483
  * other case `resolveState` folds into "rest at the initial state" (an
193550
- * unparseable subject, an actor mismatch, a commit-state target) is left as
193551
- * is — those are legitimately "no active process," not a renamed-out state.
193484
+ * unparseable subject, an actor mismatch) is left as is — those are
193485
+ * legitimately "no active process," not a renamed-out state.
193552
193486
  */
193553
193487
  const resolveRestFrom = (def, headSubject) => {
193554
193488
  const parsedHead = parseStateSubject(headSubject);
@@ -193560,7 +193494,7 @@ const resolveRestFrom = (def, headSubject) => {
193560
193494
  const stateDef = def.states[state];
193561
193495
  if (stateDef.actor === void 0) return {
193562
193496
  ok: false,
193563
- error: /* @__PURE__ */ new Error(`gtd: resolved at commit state "${state}" — a process never rests there`)
193497
+ error: /* @__PURE__ */ new Error(`gtd: resolved at state "${state}" declaring no actor`)
193564
193498
  };
193565
193499
  return {
193566
193500
  ok: true,
@@ -193572,7 +193506,7 @@ const resolveRestFrom = (def, headSubject) => {
193572
193506
  }
193573
193507
  };
193574
193508
  };
193575
- const pendingChanges = (git, base) => git.changedPaths(base).pipe(map$4((entries) => entries.map((e) => ({
193509
+ const pendingChanges = (git) => git.changedPaths().pipe(map$4((entries) => entries.map((e) => ({
193576
193510
  status: normalizeStatus(e.status),
193577
193511
  path: e.path
193578
193512
  }))));
@@ -193580,16 +193514,14 @@ const pendingChanges = (git, base) => git.changedPaths(base).pipe(map$4((entries
193580
193514
  * `ProcessRun.headTurn` from an already-fetched `commitHistory` array
193581
193515
  * (oldest→newest) — HEAD is its last entry. `undefined` for an empty
193582
193516
  * history, an unparseable/foreign subject, or a subject naming a state the
193583
- * ACTIVE definition doesn't declare or that is a commit state (never a real
193584
- * rest).
193517
+ * ACTIVE definition doesn't declare (never a real rest).
193585
193518
  */
193586
193519
  const headTurnFrom = (def, history) => {
193587
193520
  if (history.length === 0) return void 0;
193588
193521
  const head = history[history.length - 1];
193589
193522
  const parsed = parseStateSubject(subjectOf(head.message));
193590
193523
  if (parsed === void 0) return void 0;
193591
- const stateDef = def.states[parsed.state];
193592
- if (stateDef === void 0 || isCommitState(stateDef)) return void 0;
193524
+ if (def.states[parsed.state] === void 0) return void 0;
193593
193525
  return {
193594
193526
  state: parsed.state,
193595
193527
  actor: parsed.actor,
@@ -193614,6 +193546,33 @@ const parseEntryCommitOverrides = (processCommits) => {
193614
193546
  };
193615
193547
  };
193616
193548
  /**
193549
+ * The boundary-exclusive backward walk `computeProcessRun` needs, split out
193550
+ * so the surrounding Effect.gen body stays under the complexity gate. Returns
193551
+ * the trace/retry boundary's history index (`i`, `-1` when the whole history
193552
+ * belongs to the process) and, when `includeClosingBoundary` folded HEAD's own
193553
+ * closing commit into the process, its hash.
193554
+ */
193555
+ const walkProcessBoundary = (history, initialState, includeClosingBoundary) => {
193556
+ let i = history.length - 1;
193557
+ let closingHash;
193558
+ if (includeClosingBoundary && i >= 0) {
193559
+ const parsedLast = parseStateSubject(subjectOf(history[i].message));
193560
+ if (parsedLast !== void 0 && parsedLast.state === initialState) {
193561
+ closingHash = history[i].hash;
193562
+ i--;
193563
+ }
193564
+ }
193565
+ while (i >= 0) {
193566
+ const parsed = parseStateSubject(subjectOf(history[i].message));
193567
+ if (parsed === void 0 || parsed.state === initialState) break;
193568
+ i--;
193569
+ }
193570
+ return {
193571
+ boundaryIndex: i,
193572
+ closingHash
193573
+ };
193574
+ };
193575
+ /**
193617
193576
  * Walk first-parent history backward from HEAD while each commit's subject
193618
193577
  * parses as `gtd(actor): state` and that state isn't the workflow's initial
193619
193578
  * state; stop — excluding that boundary commit, which belongs to the
@@ -193627,24 +193586,30 @@ const parseEntryCommitOverrides = (processCommits) => {
193627
193586
  * `restAt`'s window-aware branch passes the saved-head hash here while a
193628
193587
  * review window is open, so the trace still covers commits a real `git log`
193629
193588
  * would miss with HEAD rewound to the review base.
193589
+ *
193590
+ * `includeClosingBoundary` (default `false`) is `summaryRun`'s one-flag
193591
+ * difference from `currentRun`'s ordinary walk: when set AND the walk's very
193592
+ * last history entry (HEAD itself) is a commit entering the initial state, that
193593
+ * boundary commit is folded INTO the trace instead of excluded, and its hash
193594
+ * is recorded as `ProcessRun.closingHash`. A process still in flight (HEAD is
193595
+ * not itself such a boundary) resolves identically to the flag being unset.
193630
193596
  */
193631
- const computeProcessRun = (git, def, head) => gen(function* () {
193597
+ const computeProcessRun = (git, def, includeClosingBoundary = false) => gen(function* () {
193632
193598
  const initialState = initialStateOf(def);
193633
- const history = yield* git.commitHistory(void 0, head);
193634
- let i = history.length - 1;
193635
- while (i >= 0) {
193636
- const parsed = parseStateSubject(subjectOf(history[i].message));
193637
- if (parsed === void 0 || parsed.state === initialState) break;
193638
- i--;
193639
- }
193599
+ const history = yield* git.commitHistory();
193600
+ const { boundaryIndex: i, closingHash } = walkProcessBoundary(history, initialState, includeClosingBoundary);
193640
193601
  const startIdx = i + 1;
193641
193602
  const processCommits = history.slice(startIdx);
193642
- const trace = processCommits.map((h) => ({
193643
- state: parseStateSubject(subjectOf(h.message)).state,
193644
- hash: h.hash
193645
- }));
193603
+ const trace = processCommits.map((h) => {
193604
+ const parsed = parseStateSubject(subjectOf(h.message));
193605
+ return {
193606
+ state: parsed.state,
193607
+ hash: h.hash,
193608
+ actor: parsed.actor
193609
+ };
193610
+ });
193646
193611
  const costEntries = parseCostTrailers(processCommits.map((h) => h.message));
193647
- const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$2;
193612
+ const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$1;
193648
193613
  const startHash = startIdx < history.length ? history[startIdx].hash : history[history.length - 1].hash;
193649
193614
  const { reviewBase: reviewBaseOverride, vars: entryVars } = parseEntryCommitOverrides(processCommits);
193650
193615
  return {
@@ -193654,26 +193619,31 @@ const computeProcessRun = (git, def, head) => gen(function* () {
193654
193619
  trace,
193655
193620
  costEntries,
193656
193621
  entryVars,
193657
- headTurn: headTurnFrom(def, history)
193622
+ headTurn: headTurnFrom(def, history),
193623
+ closingHash
193658
193624
  };
193659
193625
  });
193660
193626
  /**
193661
- * The run alone, WITHOUT resolving a rest — `gtd abandon`'s escape hatch (it
193627
+ * The run alone, WITHOUT resolving a rest — `gtd abandon`'s escape hatch: it
193662
193628
  * must still work when HEAD names a state `currentRest` would refuse on,
193663
- * since abandon IS the recovery command for that case) and the review
193664
- * window's re-arm, which degrades gracefully (stays closed) rather than
193665
- * erroring on the same kind of foreign/refused rest.
193666
- *
193667
- * Window-aware on exactly the same terms as `restAt`: while a review checkout
193668
- * window is open, real HEAD is rewound to the review base, so walking the
193669
- * trace from literal HEAD would see a process that has not started. Both
193670
- * callers need the REAL head, so the saved head is the walk's origin.
193629
+ * since abandon IS the recovery command for that case.
193671
193630
  */
193672
193631
  const currentRun = gen(function* () {
193673
193632
  const git = yield* GitService;
193674
193633
  const config = yield* (yield* ConfigService).load;
193675
- const windowHead = getOrUndefined(yield* git.readRefOption(REVIEW_HEAD_REF$1));
193676
- return yield* computeProcessRun(git, config.workflow, windowHead);
193634
+ return yield* computeProcessRun(git, config.workflow);
193635
+ });
193636
+ /**
193637
+ * The process HEAD closes or sits inside — `gtd summary`'s run resolution.
193638
+ * Identical to `currentRun` for a process still in flight; for a finished
193639
+ * process (HEAD is itself a commit entering the initial state), the closing
193640
+ * commit is folded back into the trace and its hash recorded as
193641
+ * `closingHash` — see `computeProcessRun`'s `includeClosingBoundary` flag.
193642
+ */
193643
+ const summaryRun = gen(function* () {
193644
+ const git = yield* GitService;
193645
+ const config = yield* (yield* ConfigService).load;
193646
+ return yield* computeProcessRun(git, config.workflow, true);
193677
193647
  });
193678
193648
  /**
193679
193649
  * PURE: the most-recent in-process turn commit that entered a `reviewBase`
@@ -193818,11 +193788,11 @@ const withRenderedOn = (def, state, renderedOnEdges) => ({
193818
193788
  * Build the `PatternTemplates.TemplateContext` for rendering `state`'s content
193819
193789
  * at the resolved rest. `edges` must already be rendered by the caller
193820
193790
  * (`renderOnEdges`). `currentCost`/`currentModel` are the in-flight step's own
193821
- * `--cost`/`--model`, folded into the process's committed cost entries so a
193822
- * `commit:` squash template sees the whole-process total including the
193823
- * squashing step (`0`/absent for the pure emitters, where no step runs). No
193824
- * diff is computed here — `it.reviewBase`/`it.retainedBase` are bases a
193825
- * template tells the agent to `git diff` itself.
193791
+ * `--cost`/`--model`, folded into the process's committed cost entries so
193792
+ * `it.processCost`/`it.processCostByModel` total the whole process (`0`/absent
193793
+ * for the pure emitters, where no step runs). No diff is computed here —
193794
+ * `it.reviewBase`/`it.processBase` are bases a template tells the agent to
193795
+ * `git diff` itself.
193826
193796
  */
193827
193797
  const buildTemplateContext = (git, read, state, actor, run, vars, edges, currentCost, currentModel, reviewBase) => gen(function* () {
193828
193798
  const currentCommit = yield* git.resolveRef("HEAD");
@@ -193839,7 +193809,7 @@ const buildTemplateContext = (git, read, state, actor, run, vars, edges, current
193839
193809
  state,
193840
193810
  actor,
193841
193811
  reviewBase,
193842
- retainedBase: run.startParentHash,
193812
+ processBase: run.startParentHash,
193843
193813
  processCost: totalCostOf(allCostEntries),
193844
193814
  processCostByModel: costByModel(allCostEntries),
193845
193815
  read,
@@ -193848,6 +193818,22 @@ const buildTemplateContext = (git, read, state, actor, run, vars, edges, current
193848
193818
  };
193849
193819
  });
193850
193820
  /**
193821
+ * The `TemplateContext` `gtd summary` renders `def.summary` against — no
193822
+ * resting state, no `on` edges (a summary isn't rendered AT a state), `cost`/
193823
+ * `model` both absent (the command writes nothing, so there is no in-flight
193824
+ * step to fold in).
193825
+ */
193826
+ const summaryTemplateContext = (run) => gen(function* () {
193827
+ const git = yield* GitService;
193828
+ const config = yield* (yield* ConfigService).load;
193829
+ const files = yield* RepoFiles;
193830
+ const envVars = yield* EnvVars;
193831
+ const def = config.workflow;
193832
+ const vars = resolveVars(config.workflowVars, config.rcVars, run.entryVars, envVars.all);
193833
+ const reviewBase = reviewBaseFor(def, run);
193834
+ return yield* buildTemplateContext(git, templateRead(files), "", "", run, vars, void 0, 0, void 0, reviewBase);
193835
+ });
193836
+ /**
193851
193837
  * True when this process would retain NOTHING: nothing pending and no net
193852
193838
  * change between its trace/retry boundary and HEAD. Used by `planStep`'s
193853
193839
  * "returned to the initial state retaining nothing" mixed-reset branch.
@@ -193857,37 +193843,24 @@ const retainsNothing = (git, run, changes) => gen(function* () {
193857
193843
  return (yield* git.changedPathsSince(run.startParentHash)).length === 0;
193858
193844
  });
193859
193845
  const omitUndefined = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
193860
- /**
193861
- * Resolve `Rest` at an arbitrary ref (`gtd visualize` reading the review
193862
- * window's saved head via `REVIEW_HEAD_REF`, or `undefined` for HEAD itself).
193863
- *
193864
- * Window-aware ONLY at `ref === undefined`: when the review checkout window's
193865
- * saved-head ref resolves, its hash is used as the effective head for
193866
- * `lastCommitSubject` and `computeProcessRun`'s trace walk — real git HEAD
193867
- * has been rewound to the review base while the window is open, so reading
193868
- * literal `HEAD` there would resolve state/trace/memory against the wrong
193869
- * commit.
193870
- */
193846
+ /** Resolve `Rest` at an arbitrary ref, or `undefined` for HEAD itself. */
193871
193847
  const restAt = (ref) => gen(function* () {
193872
193848
  const git = yield* GitService;
193873
193849
  const config = yield* (yield* ConfigService).load;
193874
193850
  const files = yield* RepoFiles;
193875
193851
  const envVars = yield* EnvVars;
193876
193852
  const def = config.workflow;
193877
- let windowHead;
193878
- if (ref === void 0) windowHead = getOrUndefined(yield* git.readRefOption(REVIEW_HEAD_REF$1));
193879
- const effectiveRef = windowHead ?? ref;
193880
- const headSubject = yield* git.lastCommitSubject(effectiveRef);
193853
+ const headSubject = yield* git.lastCommitSubject(ref);
193881
193854
  const resolution = resolveRestFrom(def, headSubject);
193882
193855
  if (!resolution.ok) return yield* fail$2(resolution.error);
193883
193856
  const resolved = resolution.rest;
193884
193857
  yield* (yield* Narrator).narrate(`rest resolved: ${resolved.state} (awaits ${resolved.actor})`);
193885
- const run = yield* computeProcessRun(git, def, windowHead);
193858
+ const run = yield* computeProcessRun(git, def);
193886
193859
  const vars = resolveVars(config.workflowVars, config.rcVars, run.entryVars, envVars.all);
193887
193860
  const on = yield* renderOnEdgesOrFail(resolved.stateDef.on, vars);
193888
193861
  const stepDef = withRenderedOn(def, resolved.state, on);
193889
193862
  const reviewBase = reviewBaseFor(def, run);
193890
- const changes = yield* pendingChanges(git, windowHead);
193863
+ const changes = yield* pendingChanges(git);
193891
193864
  const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase);
193892
193865
  const memory = memoryKeyFor(config.stateScopes, resolved, run);
193893
193866
  const memoryResumed = memoryResumedFor(def, config.stateScopes, resolved, run);
@@ -193899,7 +193872,6 @@ const restAt = (ref) => gen(function* () {
193899
193872
  on,
193900
193873
  stepDef,
193901
193874
  changes,
193902
- windowHead,
193903
193875
  memory,
193904
193876
  memoryResumed,
193905
193877
  hints,
@@ -193929,16 +193901,16 @@ const renderHints = (stateDef, context) => gen(function* () {
193929
193901
  return omitUndefined(hints);
193930
193902
  });
193931
193903
  /**
193932
- * Render a `Rest`'s declared content (script/prompt/message — never `commit`,
193933
- * since a `Rest` never rests at a commit state) plus every `STATE_FIELDS`
193934
- * field carrying a `rest` kind and its computed memory key — all of which
193935
- * already live on `rest` (`rest.context`/`rest.hints`/`rest.memory`, built
193936
- * once by `restAt` via `renderHints`), so this takes no other parameters.
193904
+ * Render a `Rest`'s declared content (script/prompt/message) plus every
193905
+ * `STATE_FIELDS` field carrying a `rest` kind and its computed memory key —
193906
+ * all of which already live on `rest` (`rest.context`/`rest.hints`/
193907
+ * `rest.memory`, built once by `restAt` via `renderHints`), so this takes no
193908
+ * other parameters.
193937
193909
  */
193938
193910
  const renderRest = (rest) => gen(function* () {
193939
193911
  const kind = contentKindOf(rest.stateDef);
193940
193912
  if (kind === void 0) return yield* fail$2(/* @__PURE__ */ new Error(`state "${rest.state}" declares no content — invalid definition`));
193941
- const template = rest.stateDef.script ?? rest.stateDef.prompt ?? rest.stateDef.message ?? rest.stateDef.commit;
193913
+ const template = rest.stateDef.script ?? rest.stateDef.prompt ?? rest.stateDef.message;
193942
193914
  const content = yield* try_({
193943
193915
  try: () => renderStateTemplate(template, rest.context),
193944
193916
  catch: (e) => e instanceof Error ? e : new Error(String(e))
@@ -193973,21 +193945,7 @@ const stalledAt = (rest) => rest.changes.length === 0 && rest.run.headTurn?.stat
193973
193945
  * `PatternMachine.step`'s own tests (a defensive message beats a lie).
193974
193946
  */
193975
193947
  const formatStepRefusal = (refusal) => refusal.reason === "out-of-turn" ? `gtd land: out of turn — "${refusal.state}" awaits ${refusal.awaits}` : `gtd land: no declared pattern matches the pending changes at "${refusal.state}" — declared patterns: ${refusal.patterns.length > 0 ? refusal.patterns.join(", ") : "(none)"}`;
193976
- /**
193977
- * Build the `TemplateContext` for rendering a DIFFERENT state than `rest`'s
193978
- * own resting one, with a step's `cost`/`model` folded in — a squash's commit
193979
- * template needs this; it is NOT interchangeable with `rest.context`, which
193980
- * is pinned to the resting state with `cost: 0`. Exported for `program.ts`'s
193981
- * `planLanding`, which assembles the same script by hand and must pick the
193982
- * same context.
193983
- */
193984
- const contextAt = (rest, targetState, invoker, cost, model) => gen(function* () {
193985
- const git = yield* GitService;
193986
- const files = yield* RepoFiles;
193987
- const onEdges = targetState === rest.state ? rest.on : yield* renderOnEdgesOrFail(rest.def.states[targetState]?.on, rest.vars);
193988
- return yield* buildTemplateContext(git, templateRead(files), targetState, invoker, rest.run, rest.vars, onEdges, cost ?? 0, model, reviewBaseFor(rest.def, rest.run));
193989
- });
193990
- /** `updateRef(HISTORY_REF, tip)` as a single-element (or empty) step list — OMITTED when `tip === startParentHash`, mirroring `retainHistory`'s own no-op check. Shared by the squash branch and the commit branch's collapse case below. */
193948
+ /** `updateRef(HISTORY_REF, tip)` as a single-element (or empty) step list — OMITTED when `tip === startParentHash`, mirroring `retainHistory`'s own no-op check. Used by the commit branch's collapse case below. */
193991
193949
  const retainHistoryStep = (tip, startParentHash) => tip === startParentHash ? [] : [{
193992
193950
  kind: "gitWrite",
193993
193951
  command: updateRef(HISTORY_REF, tip)
@@ -194002,20 +193960,20 @@ const commitDecisionOutcome = (decision) => ({
194002
193960
  * state retaining nothing" collapse — target is the initial state, the
194003
193961
  * process started somewhere other than the empty tree, and nothing pending or
194004
193962
  * already-committed since its start parent survives (`retainsNothing`).
194005
- * Always `false` for a `"squash"` decision and for an ATTEMPT: an agent that
194006
- * did nothing must never rewind a process, and an attempt landing at a prompt
194007
- * state that's also the initial state would otherwise satisfy every other
194008
- * criterion identically to a genuine collapse.
193963
+ * Always `false` for an ATTEMPT: an agent that did nothing must never rewind
193964
+ * a process, and an attempt landing at a prompt state that's also the initial
193965
+ * state would otherwise satisfy every other criterion identically to a
193966
+ * genuine collapse.
194009
193967
  */
194010
193968
  const collapsesWith = (git, rest, decision) => gen(function* () {
194011
- if (decision.kind === "squash" || decision.attempt === true) return false;
194012
- return parseStateSubject(decision.subject)?.state === initialStateOf(rest.def) && rest.run.startParentHash !== EMPTY_TREE$2 && (yield* retainsNothing(git, rest.run, rest.changes));
193969
+ if (decision.attempt === true) return false;
193970
+ return parseStateSubject(decision.subject)?.state === initialStateOf(rest.def) && rest.run.startParentHash !== EMPTY_TREE$1 && (yield* retainsNothing(git, rest.run, rest.changes));
194013
193971
  });
194014
193972
  /**
194015
193973
  * The service-requiring twin of `collapsesWith`, for `program.ts` to ask
194016
193974
  * instead of reaching into `GitService` itself — the boundary AGENTS.md pins.
194017
193975
  * `planLanding` calls this to fill `LandResult.settled` for a `"commit"`/
194018
- * `"squash"` plan, off the same `rest`/`decision` `renderDecision` already
193976
+ * `"commit"` plan, off the same `rest`/`decision` `renderDecision` already
194019
193977
  * decided against, so the two git reads (`changedPathsSince` + `resolveRef`)
194020
193978
  * can never disagree with each other.
194021
193979
  */
@@ -194024,100 +193982,54 @@ const collapsesToInitialState = (rest, decision) => gen(function* () {
194024
193982
  return yield* collapsesWith(git, rest, decision);
194025
193983
  });
194026
193984
  /**
194027
- * Render a `"commit"`/`"squash"` decision as the `EmitStep`s the external
194028
- * driver runs to produce its git effect — the ONE place a decision becomes
194029
- * git commands. Every git call here is a READ; nothing is written — gtd
194030
- * itself never writes git, only a driven script does.
193985
+ * Render a `"commit"` decision as the `EmitStep`s the external driver runs to
193986
+ * produce its git effect — the ONE place a decision becomes git commands.
193987
+ * Every git call here is a READ; nothing is written — gtd itself never writes
193988
+ * git, only a driven script does.
194031
193989
  *
194032
- * The commit branch carries one non-obvious case: a commit whose target is
194033
- * the workflow's INITIAL state, from a process that retained nothing, is a
194034
- * no-op probe (`gtd --entry fix-precheck` against a green suite is the
194035
- * shipped example) and must leave no trace — it emits retain-history plus a
194036
- * mixed reset to the process start instead of a commit, so the entry commit
194037
- * and the probe collapse away together rather than dirtying the log with a
194038
- * round trip to `idle`.
193990
+ * One non-obvious case: a commit whose target is the workflow's INITIAL
193991
+ * state, from a process that retained nothing, is a no-op probe
193992
+ * (`gtd --entry fix-precheck` against a green suite is the shipped example)
193993
+ * and must leave no trace — it emits retain-history plus a mixed reset to the
193994
+ * process start instead of a commit, so the entry commit and the probe
193995
+ * collapse away together rather than dirtying the log with a round trip to
193996
+ * `idle`.
194039
193997
  */
194040
- const renderDecision = (git, rest, decision, context, cost, model) => gen(function* () {
193998
+ const renderDecision = (git, rest, decision, cost, model) => gen(function* () {
194041
193999
  const run = rest.run;
194042
- switch (decision.kind) {
194043
- case "commit":
194044
- if (yield* collapsesWith(git, rest, decision)) {
194045
- const tip = yield* git.resolveRef("HEAD");
194046
- return [
194047
- ...retainHistoryStep(tip, run.startParentHash),
194048
- {
194049
- kind: "gitWrite",
194050
- command: mixedResetTo(run.startParentHash)
194051
- },
194052
- {
194053
- kind: "outcome",
194054
- command: noteOutcome(COLLAPSED_TEXT)
194055
- }
194056
- ];
194057
- }
194058
- return [{
194000
+ if (yield* collapsesWith(git, rest, decision)) {
194001
+ const tip = yield* git.resolveRef("HEAD");
194002
+ return [
194003
+ ...retainHistoryStep(tip, run.startParentHash),
194004
+ {
194059
194005
  kind: "gitWrite",
194060
- command: commitAll(withCostTrailer(decision.subject, cost, model))
194061
- }, commitDecisionOutcome(decision)];
194062
- case "squash": {
194063
- const message = yield* try_({
194064
- try: () => renderStateTemplate(decision.template, context),
194065
- catch: (e) => /* @__PURE__ */ new Error(`gtd: rendering the "${decision.state}" commit template failed — nothing was committed: ${e instanceof Error ? e.message : String(e)}`)
194066
- });
194067
- const tip = yield* git.resolveRef("HEAD");
194068
- return [
194069
- ...retainHistoryStep(tip, run.startParentHash),
194070
- {
194071
- kind: "gitWrite",
194072
- command: softResetTo(run.startParentHash)
194073
- },
194074
- {
194075
- kind: "gitWrite",
194076
- command: commitAsIs(withHistoryTrailer(message, tip))
194077
- },
194078
- {
194079
- kind: "gitWrite",
194080
- command: discardPending()
194081
- },
194082
- {
194083
- kind: "outcome",
194084
- command: commitOutcome(subjectOf(message))
194085
- }
194086
- ];
194087
- }
194006
+ command: mixedResetTo(run.startParentHash)
194007
+ },
194008
+ {
194009
+ kind: "outcome",
194010
+ command: noteOutcome(COLLAPSED_TEXT)
194011
+ }
194012
+ ];
194088
194013
  }
194014
+ return [{
194015
+ kind: "gitWrite",
194016
+ command: commitAll(withCostTrailer(decision.subject, cost, model))
194017
+ }, commitDecisionOutcome(decision)];
194089
194018
  });
194090
194019
  /**
194091
194020
  * The `EmitPreconditions` a step's/entry's assembled scripts assert against:
194092
194021
  * `expectedHead` is the resolved HEAD hash the `Rest` snapshot was taken at.
194093
- * When `targetState` declares `reviewWindow: true` and a window is currently
194094
- * open, the script also pins the window's saved-head hash — a later run whose
194095
- * window has since closed or moved must re-decide rather than trust a stale
194096
- * script.
194097
- */
194098
- const buildPreconditions = (git, rest, targetState) => gen(function* () {
194099
- const expectedHead = rest.context.currentCommit;
194100
- if (!isReviewWindowState(rest.def, targetState)) return { expectedHead };
194101
- const windowHead = yield* git.readRefOption(REVIEW_HEAD_REF$1);
194102
- if (isNone(windowHead)) return { expectedHead };
194103
- return {
194104
- expectedHead,
194105
- reviewWindow: {
194106
- ref: REVIEW_HEAD_REF$1,
194107
- expectedHash: windowHead.value
194108
- }
194109
- };
194110
- });
194022
+ */
194023
+ const buildPreconditions = (rest) => ({ expectedHead: rest.context.currentCommit });
194111
194024
  /**
194112
- * The `EmittedScripts` a `"commit"`/`"squash"` `StepPlan` carries alongside
194113
- * `perform` — built from the SAME `renderDecision` output, but a render
194114
- * failure collapses to an EMPTY script (`emitScripts` with no steps) instead
194115
- * of failing this Effect: `perform` stays the one place a render failure is
194116
- * ever reported to a caller, unchanged by this addition.
194025
+ * The `EmittedScripts` a `"commit"` `StepPlan` carries alongside `perform` —
194026
+ * built from the SAME `renderDecision` output, but a render failure collapses
194027
+ * to an EMPTY script (`emitScripts` with no steps) instead of failing this
194028
+ * Effect: `perform` stays the one place a render failure is ever reported to
194029
+ * a caller, unchanged by this addition.
194117
194030
  */
194118
- const buildStepScripts = (git, rest, decision, context, cost, model) => gen(function* () {
194119
- const targetState = decision.kind === "commit" ? decision.to : decision.state;
194120
- return emitScripts(yield* buildPreconditions(git, rest, targetState), yield* renderDecision(git, rest, decision, context, cost, model).pipe(catchAll(() => succeed$3([]))));
194031
+ const buildStepScripts = (git, rest, decision, cost, model) => gen(function* () {
194032
+ return emitScripts(buildPreconditions(rest), yield* renderDecision(git, rest, decision, cost, model).pipe(catchAll(() => succeed$3([]))));
194121
194033
  });
194122
194034
  /**
194123
194035
  * A no-op is TERMINAL only at a `script` rest: gtd rendered the script, the
@@ -194134,10 +194046,8 @@ const noOpSettles = (rest) => contentKindOf(rest.stateDef) === "script";
194134
194046
  /**
194135
194047
  * Decide what landing at `rest` does, authenticated as `rest.actor` (the
194136
194048
  * state's own declared actor, so out-of-turn is unreachable by construction),
194137
- * and — for a `"commit"`/`"squash"` decision — assemble the emitted `scripts`
194138
- * a driver runs to land it (against `rest.context` for a commit, or a
194139
- * freshly built `contextAt` for a squash, which renders a DIFFERENT state's
194140
- * template with the step's cost folded in).
194049
+ * and — for a `"commit"` decision — assemble the emitted `scripts` a driver
194050
+ * runs to land it, against `rest.context`.
194141
194051
  */
194142
194052
  const planStep = (rest, opts = {}) => gen(function* () {
194143
194053
  const decision = step(rest.stepDef, rest.state, rest.actor, {
@@ -194155,8 +194065,7 @@ const planStep = (rest, opts = {}) => gen(function* () {
194155
194065
  };
194156
194066
  const { cost, model } = opts;
194157
194067
  const git = yield* GitService;
194158
- const scriptContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, rest.actor, cost, model);
194159
- const scripts = yield* buildStepScripts(git, rest, decision, scriptContext, cost, model);
194068
+ const scripts = yield* buildStepScripts(git, rest, decision, cost, model);
194160
194069
  return {
194161
194070
  kind: decision.kind,
194162
194071
  state: rest.state,
@@ -194166,7 +194075,7 @@ const planStep = (rest, opts = {}) => gen(function* () {
194166
194075
  });
194167
194076
  /**
194168
194077
  * `gtd --entry <state>`: start a brand NEW process at `entry.state` — any
194169
- * declared, non-commit state — writing an ordinary turn commit carrying zero
194078
+ * declared state — writing an ordinary turn commit carrying zero
194170
194079
  * or more `Gtd-Var:` trailers for each `entry.vars` override, plus — when
194171
194080
  * `entry.state` declares a string `reviewBase:` — a `Gtd-Review-Base:`
194172
194081
  * trailer pinning the new process's diff base. Captures whatever the working
@@ -194234,12 +194143,11 @@ const planEntry = (rest, actor, entry) => gen(function* () {
194234
194143
  ...base !== void 0 ? { base } : {},
194235
194144
  vars: varOverrides
194236
194145
  });
194237
- const scriptsGit = yield* GitService;
194238
194146
  return {
194239
194147
  kind: "entry",
194240
194148
  state: entryState,
194241
194149
  subject,
194242
- scripts: emitScripts(yield* buildPreconditions(scriptsGit, rest, entryState), [{
194150
+ scripts: emitScripts(buildPreconditions(rest), [{
194243
194151
  kind: "gitWrite",
194244
194152
  command: commitAll(message)
194245
194153
  }, {
@@ -194249,106 +194157,36 @@ const planEntry = (rest, actor, entry) => gen(function* () {
194249
194157
  };
194250
194158
  });
194251
194159
  //#endregion
194252
- //#region src/ReviewWindow.ts
194160
+ //#region src/Summary.ts
194253
194161
  /**
194254
- * Per-worktree (`refs/worktree/*`) so linked worktrees sharing one `.git`
194255
- * don't clobber each other's window.
194162
+ * Every `human`-authored commit in `run.trace`, oldest to newest, minus
194163
+ * `entryHash` — derived generically off the invoking actor `computeProcessRun`
194164
+ * already parsed onto each `TraceEntry`, never by naming a state.
194256
194165
  */
194257
- const REVIEW_HEAD_REF = "refs/worktree/gtd/review-head";
194258
- const REVIEW_BASE_REF = "refs/worktree/gtd/review-base";
194259
- /** The pre-7.2 shared refs — gtd never writes them, only reads them to finish a window an older gtd left open across the upgrade. */
194260
- const LEGACY_REVIEW_HEAD_REF = "refs/gtd/review-head";
194261
- const LEGACY_REVIEW_BASE_REF = "refs/gtd/review-base";
194262
- const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
194263
- /** The manual-recovery incantation every close refusal ends with. */
194264
- const manualRecovery = (refs) => `\`git reset --mixed ${refs.headRef} && git update-ref -d ${refs.headRef} && git update-ref -d ${refs.baseRef}\``;
194265
- /**
194266
- * Which ref pair (if any) records an open window: this worktree's own
194267
- * `refs/worktree/gtd/*` pair first, falling back to the pre-7.2 shared
194268
- * `refs/gtd/*` pair so an upgrade mid-window still closes cleanly.
194269
- */
194270
- const openWindowRefs = (git) => gen(function* () {
194271
- const scoped = yield* git.readRefOption(REVIEW_HEAD_REF);
194272
- if (isSome(scoped)) return some({
194273
- headRef: REVIEW_HEAD_REF,
194274
- baseRef: REVIEW_BASE_REF,
194275
- headHash: scoped.value,
194276
- legacy: false
194277
- });
194278
- const legacy = yield* git.readRefOption(LEGACY_REVIEW_HEAD_REF);
194279
- if (isNone(legacy)) return none$4();
194280
- return some({
194281
- headRef: LEGACY_REVIEW_HEAD_REF,
194282
- baseRef: LEGACY_REVIEW_BASE_REF,
194283
- headHash: legacy.value,
194284
- legacy: true
194285
- });
194286
- });
194287
- const decideCloseWindow = gen(function* () {
194288
- const git = yield* GitService;
194289
- const open = yield* openWindowRefs(git);
194290
- if (isNone(open)) return { shouldClose: false };
194291
- const refs = open.value;
194292
- const base = yield* git.readRefOption(refs.baseRef);
194293
- if (isSome(base) && base.value !== EMPTY_TREE$1) {
194294
- if (!(yield* git.isAncestor(base.value, "HEAD"))) return yield* fail$2(/* @__PURE__ */ new Error(`a gtd review checkout window is open but HEAD has moved off the reviewed branch — return to it, or restore manually with ${manualRecovery(refs)}`));
194295
- }
194296
- if (refs.legacy) {
194297
- if (!(yield* git.isAncestor("HEAD", refs.headHash))) return yield* fail$2(/* @__PURE__ */ new Error(`a review checkout window is recorded under the shared ${LEGACY_REVIEW_HEAD_REF} (written by gtd 7.1 or older) but it does not belong to this worktree — HEAD is not contained in it. gtd now scopes the window per worktree (${REVIEW_HEAD_REF}); in the worktree that owns it, run ${manualRecovery(refs)}`));
194298
- }
194299
- return {
194300
- shouldClose: true,
194301
- refs
194302
- };
194303
- });
194166
+ const humanCommitsOf = (run, entryHash) => run.trace.filter((entry) => entry.actor === "human" && entry.hash !== entryHash).map((entry) => ({
194167
+ hash: entry.hash,
194168
+ state: entry.state
194169
+ }));
194304
194170
  /**
194305
- * PURE: the close sequence's bash text — reset to the saved head, then delete
194306
- * both refs — built from `src/GitScript.ts`'s `mixedResetTo`/`deleteRef`.
194307
- * Assumes its caller's guards (the reviewed-branch check, the legacy
194308
- * containment check — see `decideCloseWindow`) have already passed; this
194309
- * builder performs none of them, only the write sequence for the case where
194310
- * they did.
194311
- *
194312
- * Resets to `refs.headHash` (the resolved hash), not `refs.headRef` by name —
194313
- * the very next statement deletes that ref, so naming it for the reset would
194314
- * fail on re-entry. A hash stays resolvable regardless, keeping the sequence
194315
- * idempotent.
194316
- */
194317
- const buildCloseWindowScript = (refs) => [
194318
- mixedResetTo(refs.headHash),
194319
- deleteRef(refs.headRef),
194320
- deleteRef(refs.baseRef)
194321
- ].join(" &&\n");
194322
- const decideOpenWindow = (def, target, run, head) => {
194323
- if (!isReviewWindowState(def, target)) return { shouldOpen: false };
194324
- const base = reviewBaseFor(def, run);
194325
- if (base === EMPTY_TREE$1 || base === head) return { shouldOpen: false };
194326
- return {
194327
- shouldOpen: true,
194328
- base,
194329
- head
194171
+ * Build `SummaryContext` for `run` and render `def.summary` against it — pure,
194172
+ * touching no git and no filesystem (`base` already carries a resolved
194173
+ * `read`/`vars`/`processCost`, built by the caller against real git). Returns
194174
+ * `undefined` when the workflow declares no `summary:` or `run.trace` is
194175
+ * empty — both refusal conditions belong to the caller (`program.ts`), not an
194176
+ * Effect failure here.
194177
+ */
194178
+ const buildSummary = (def, run, base) => {
194179
+ if (def.summary === void 0 || run.trace.length === 0) return void 0;
194180
+ const entryCommit = run.trace[0].hash;
194181
+ const processTip = run.trace[run.trace.length - 1].hash;
194182
+ const context = {
194183
+ ...base,
194184
+ entryCommit,
194185
+ humanCommits: humanCommitsOf(run, entryCommit),
194186
+ processTip
194330
194187
  };
194188
+ return renderStateTemplate(def.summary, context);
194331
194189
  };
194332
- /**
194333
- * The open sequence's bash text: base-ref write, head-ref write,
194334
- * mixed-reset-to-base, then a `.gtd/`-scoped `restoreStagedFrom` pin. No `git
194335
- * add --intent-to-add` — that both loses the index-lock race and truncates
194336
- * discarded files to zero bytes — so new files stay untracked; new files
194337
- * outside `.gtd/` stay unpinned too, since `restoreStagedFrom` is scoped
194338
- * there alone. Idempotent under re-entry.
194339
- *
194340
- * `".gtd"` is a deliberate literal, not derived from states' `file:`
194341
- * declarations — it must also cover paths no state declares (a check
194342
- * script's temp output), and `src/testing/EmittedScriptRecognizer.ts`'s
194343
- * `recognizeReviewWindowOpen` re-derives this exact string to recognize the
194344
- * script, which only works while it's fixed.
194345
- */
194346
- const buildOpenWindowScript = (decision) => [
194347
- updateRef(REVIEW_BASE_REF, decision.base),
194348
- updateRef(REVIEW_HEAD_REF, decision.head),
194349
- mixedResetTo(decision.base),
194350
- restoreStagedFrom(REVIEW_HEAD_REF, [".gtd"])
194351
- ].join(" &&\n");
194352
194190
  //#endregion
194353
194191
  //#region node_modules/vscode-languageserver/lib/common/utils/is.js
194354
194192
  var require_is$2 = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -205983,9 +205821,10 @@ const isPlumbingPath = (path) => path === ".gtd" || path.startsWith(`.gtd/`);
205983
205821
  const isCodePath = (path) => !isPlumbingPath(path);
205984
205822
  /** The `mode:` name of gtd's built-in REVIEW.md checkbox format — the only mode the review-doc guard understands. */
205985
205823
  const REVIEW_MODE = "review";
205824
+ /** Selects exactly the human review gate (`await-review` in the bundled template) — a human-actor state declaring `mode: review`. */
205986
205825
  const reviewDocGuard = {
205987
205826
  name: "review-doc",
205988
- appliesTo: (rest) => isReviewWindowState(rest.def, rest.state) && rest.stateDef.mode === REVIEW_MODE,
205827
+ appliesTo: (rest) => rest.stateDef.actor === "human" && rest.stateDef.mode === REVIEW_MODE,
205989
205828
  check: (ctx) => succeed$3(ctx.fileDeleted ? `${ctx.file} was deleted at "${ctx.rest.state}" — restore it, or leave a note (or edit code) to request changes.` : void 0)
205990
205829
  };
205991
205830
  /** The one-line marker `feedback-collecting` writes when a review round left nothing actionable — the ONLY content that lets a `requireProgress` state's instructions file be deleted without a code change. */
@@ -206037,7 +205876,7 @@ const stepGuards = [
206037
205876
  * of the current committed/working bytes, and fail with the first refusal's
206038
205877
  * reason, prefixed `gtd land: ` (the one place that prefix, and
206039
205878
  * `Commentary.ts`'s `renderFailure` `/^gtd[: ]/` contract, is satisfied). A
206040
- * no-op for a squash/no-op decision, an ATTEMPT commit (empty diff by
205879
+ * no-op for a `"noop"` decision, an ATTEMPT commit (empty diff by
206041
205880
  * construction — nothing for any guard to check), a state with no `file:`, or
206042
205881
  * a state no guard applies to.
206043
205882
  */
@@ -206058,7 +205897,7 @@ const enforceStepGuards = (input) => gen(function* () {
206058
205897
  template: input.context
206059
205898
  };
206060
205899
  const files = yield* RepoFiles;
206061
- const head = yield* cached(files.committed(file, input.windowHead));
205900
+ const head = yield* cached(files.committed(file));
206062
205901
  const worktree = yield* cached(try_(() => files.working(file)));
206063
205902
  const ctx = {
206064
205903
  ...base,
@@ -206858,76 +206697,27 @@ const steeringModeSteps = (rest) => gen(function* () {
206858
206697
  if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
206859
206698
  return yield* renderSteeringModeCommandSteps(resolved, file, rest.context);
206860
206699
  });
206861
- /**
206862
- * The `optional` half: re-open the review checkout window when the step
206863
- * lands at a `reviewWindow: true` state, `""` otherwise. Takes the
206864
- * already-decided `OpenWindowDecision` rather than deciding itself, since
206865
- * narration belongs at the caller.
206866
- *
206867
- * `"HEAD"` is deliberately the literal string, not a resolved hash — git
206868
- * resolves it when this script runs, after the required script has already
206869
- * landed the new commit. For the same reason it carries no `expectedHead`
206870
- * precondition: that hash doesn't exist yet at decide time.
206871
- */
206872
- const openWindowScript = (decision) => {
206873
- if (!decision.shouldOpen) return "";
206874
- return emitScripts({}, [], [{
206875
- kind: "gitWrite",
206876
- command: buildOpenWindowScript({
206877
- base: decision.base,
206878
- head: decision.head
206879
- })
206880
- }]).optional;
206881
- };
206882
- /** `decideOpenWindow`'s result, narrated here (the one caller with `Narrator` in scope) — `decideOpenWindow` itself stays pure. */
206883
- const decideAndNarrateOpenWindow = (rest, targetState) => gen(function* () {
206884
- const decision = decideOpenWindow(rest.def, targetState, rest.run, "HEAD");
206885
- yield* (yield* Narrator).narrate(decision.shouldOpen ? `review-window: open (base ${decision.base.slice(0, 7)})` : "review-window: no-op");
206886
- return decision;
206887
- });
206888
206700
  /** True for a `"commit"` decision that is an ATTEMPT (`PatternMachine.StepCommit.attempt`) — the one flag both `enforceStepGuards` and `buildRequiredScript` bypass their own steps for (see each call site). */
206889
206701
  const isAttemptDecision = (decision) => decision.kind === "commit" && decision.attempt === true;
206702
+ /** The subject the required script will produce. */
206703
+ const previewSubject = (decision) => succeed$3(decision.subject);
206890
206704
  /**
206891
- * The subject the required script will produce. Unreachable-on-failure:
206892
- * `renderDecision` already rendered the same template and refused the
206893
- * command if it threw, so this second render can never be the first to fail.
206894
- */
206895
- const previewSubject = (decision, rest) => decision.kind === "commit" ? succeed$3(decision.subject) : try_(() => renderStateTemplate(decision.template, rest.context).split("\n")[0].trim()).pipe(catchAll(() => succeed$3(null)));
206896
- /**
206897
- * The `required` half: the review-window close (when one is open), the
206898
- * resting state's own steering-mode commands, then the commit/squash steps.
206899
- * The steering-mode step is skipped for an attempt commit — nothing to
206900
- * format/validate in an empty diff, and running `format:` ahead of the
206901
- * commit could dirty the tree and turn an "empty" attempt non-empty, breaking
206902
- * `stalledAt`'s derivation. The window close still runs regardless —
206903
- * committing with a window open would land the attempt on the review base.
206705
+ * The `required` half: the resting state's own steering-mode commands, then
206706
+ * the commit steps. The steering-mode step is skipped for an attempt commit —
206707
+ * nothing to format/validate in an empty diff, and running `format:` ahead of
206708
+ * the commit could dirty the tree and turn an "empty" attempt non-empty,
206709
+ * breaking `stalledAt`'s derivation.
206904
206710
  */
206905
206711
  const buildRequiredScript = (rest, decision, cost, model) => gen(function* () {
206906
206712
  const git = yield* GitService;
206907
- const closeDecision = yield* decideCloseWindow;
206908
- yield* (yield* Narrator).narrate(closeDecision.shouldClose ? "review-window: close" : "review-window: no-op");
206909
- const commitContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, rest.actor, cost, model);
206910
- const isAttempt = isAttemptDecision(decision);
206911
- const steps = [
206912
- ...closeDecision.shouldClose ? [{
206913
- kind: "gitWrite",
206914
- command: buildCloseWindowScript(closeDecision.refs)
206915
- }] : [],
206916
- ...isAttempt ? [] : yield* steeringModeSteps(rest),
206917
- ...yield* renderDecision(git, rest, decision, commitContext, cost, model)
206918
- ];
206713
+ const steps = [...isAttemptDecision(decision) ? [] : yield* steeringModeSteps(rest), ...yield* renderDecision(git, rest, decision, cost, model)];
206919
206714
  return emitScripts(headPreconditions(rest.context.currentCommit), steps).required;
206920
206715
  });
206921
206716
  /**
206922
- * Decide the one resulting transition (commit or squash) for `gtd land`
206717
+ * Decide the one resulting transition (a commit) for `gtd land`
206923
206718
  * without performing it, authenticating as `rest.actor`. Refusals fail the
206924
206719
  * Effect with a formatted message; a no-op returns `subject: null` and empty
206925
206720
  * scripts.
206926
- *
206927
- * `plan.scripts` (built by `Edge.ts`) is not reused here: its precondition
206928
- * uses `rest.context.currentCommit`, which while a review window is open is
206929
- * the window's base, not the real head, and it contains no window close/open
206930
- * steps. This function builds its own required/optional pair instead.
206931
206721
  */
206932
206722
  const planLanding = (opts = {}) => gen(function* () {
206933
206723
  const rest = yield* currentRest;
@@ -206949,32 +206739,61 @@ const planLanding = (opts = {}) => gen(function* () {
206949
206739
  };
206950
206740
  }
206951
206741
  const decision = plan.decision;
206952
- if (decision.kind !== "commit" && decision.kind !== "squash") return yield* fail$2(/* @__PURE__ */ new Error(`gtd: internal error — plan kind "${plan.kind}" but decision kind "${decision.kind}"`));
206742
+ if (decision.kind !== "commit") return yield* fail$2(/* @__PURE__ */ new Error(`gtd: internal error — plan kind "${plan.kind}" but decision kind "${decision.kind}"`));
206953
206743
  yield* enforceStepGuards({
206954
206744
  rest,
206955
206745
  context: rest.context,
206956
206746
  file: rest.hints.file,
206957
206747
  changes: rest.changes,
206958
- windowHead: rest.windowHead,
206959
206748
  kind: decision.kind,
206960
206749
  attempt: isAttemptDecision(decision)
206961
206750
  });
206962
- const targetState = decision.kind === "commit" ? decision.to : decision.state;
206963
- const restingState = decision.kind === "commit" ? decision.to : initialStateOf(rest.def);
206751
+ const restingState = decision.to;
206964
206752
  const settled = yield* collapsesToInitialState(rest, decision);
206965
- const openDecision = yield* decideAndNarrateOpenWindow(rest, targetState);
206966
206753
  const required = yield* buildRequiredScript(rest, decision, opts.cost, opts.model);
206967
206754
  return {
206968
206755
  state: restingState,
206969
- subject: yield* previewSubject(decision, rest),
206756
+ subject: yield* previewSubject(decision),
206970
206757
  cost: opts.cost ?? null,
206971
206758
  model: opts.model ?? null,
206972
- script: normalizeScriptNewline(combinedScript(required, openWindowScript(openDecision))),
206759
+ script: normalizeScriptNewline(combinedScript(required, "")),
206973
206760
  settled,
206974
206761
  idle: restingState === initialStateOf(rest.def)
206975
206762
  };
206976
206763
  });
206977
206764
  /**
206765
+ * `gtd summary`: print the prompt for an agent to write the process HEAD
206766
+ * closes or sits inside its own closing message. Writes nothing — no git, no
206767
+ * state transition, no file. Refuses (throws, mapped to the runtime-error
206768
+ * exit code) on either of the two conditions `src/Summary.ts`'s
206769
+ * `buildSummary` folds into one `undefined`: the workflow declares no
206770
+ * `summary:` template, or the resolved run has no commits to name.
206771
+ */
206772
+ const runSummaryCommand = (out) => gen(function* () {
206773
+ const config = yield* (yield* ConfigService).load;
206774
+ const run = yield* summaryRun;
206775
+ const context = yield* summaryTemplateContext(run);
206776
+ const rendered = buildSummary(config.workflow, run, context);
206777
+ if (rendered === void 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd summary: refused — either this workflow declares no \"summary:\" template, or there is no finished/in-flight process to summarize at HEAD"));
206778
+ out.write(rendered.endsWith("\n") ? rendered : rendered + "\n");
206779
+ });
206780
+ /**
206781
+ * `gtd base`: print the review anchor hash — `reviewBaseFor(rest.def,
206782
+ * rest.run)` — bare and newline-terminated, so an external tool (a diff, a
206783
+ * PR tool, another agent) can be pointed at the range under review. Shaped
206784
+ * exactly like `runSummaryCommand`: one `Rest` resolved, nothing written — no
206785
+ * git, no state transition, no session identity. Refuses (mapped to the
206786
+ * runtime-error exit code) when the resolved run has no commits to name —
206787
+ * the only case where the hash would name a range that corresponds to no
206788
+ * review, exactly the way `runSummaryCommand` refuses on an empty trace.
206789
+ */
206790
+ const runBaseCommand = (out) => gen(function* () {
206791
+ const rest = yield* currentRest;
206792
+ if (rest.run.trace.length === 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd base: refused — no process is underway at HEAD"));
206793
+ const base = reviewBaseFor(rest.def, rest.run);
206794
+ out.write(`${base}\n`);
206795
+ });
206796
+ /**
206978
206797
  * `gtd land [--cost=<n>] [--model=<name>]`: land whatever the tree shows at
206979
206798
  * the currently resolved rest, authenticated as the rest's own actor.
206980
206799
  * Plain output writes `result.script` verbatim so `gtd land | sh` keeps
@@ -206990,7 +206809,7 @@ const runLandCommand = (opts, json, sh, out) => gen(function* () {
206990
206809
  });
206991
206810
  /**
206992
206811
  * `gtd --entry <state> [--var <name>=<value> ...]` (`actor` always `"human"`):
206993
- * start a brand new process at `<state>` — any declared, non-commit state.
206812
+ * start a brand new process at `<state>` — any declared state.
206994
206813
  * Writes an ordinary turn commit carrying zero or more `Gtd-Var:` trailers,
206995
206814
  * plus a `Gtd-Review-Base:` trailer when `<state>` declares a `reviewBase:`.
206996
206815
  * Commits via `commitAllWithPrefix` — capturing whatever the working tree
@@ -207049,13 +206868,8 @@ const runAbandonCommand = (out) => gen(function* () {
207049
206868
  }
207050
206869
  const restState = run.trace[run.trace.length - 1].state;
207051
206870
  if (run.startParentHash === EMPTY_TREE) return yield* fail$2(/* @__PURE__ */ new Error(`gtd abandon: the process underway (resting at "${restState}") starts at the repository's first commit — there is no earlier commit to rewind to`));
207052
- const closeDecision = yield* decideCloseWindow;
207053
- const tip = closeDecision.shouldClose ? closeDecision.refs.headHash : yield* git.resolveRef("HEAD");
206871
+ const tip = yield* git.resolveRef("HEAD");
207054
206872
  const steps = [
207055
- ...closeDecision.shouldClose ? [{
207056
- kind: "gitWrite",
207057
- command: buildCloseWindowScript(closeDecision.refs)
207058
- }] : [],
207059
206873
  {
207060
206874
  kind: "gitWrite",
207061
206875
  command: updateRef(HISTORY_REF, tip)
@@ -207069,12 +206883,12 @@ const runAbandonCommand = (out) => gen(function* () {
207069
206883
  command: abandonedOutcome(restState, run.startParentHash, initial)
207070
206884
  }
207071
206885
  ];
207072
- const required = emitScripts(headPreconditions(yield* git.resolveRef("HEAD")), steps).required;
206886
+ const required = emitScripts(headPreconditions(tip), steps).required;
207073
206887
  out.write(combinedScript(required, ""));
207074
206888
  });
207075
206889
  /**
207076
- * `gtd restore`: hard-reset HEAD back to the pre-squash tip a squash or
207077
- * `gtd abandon` retained, undoing either.
206890
+ * `gtd restore`: hard-reset HEAD back to the tip `gtd abandon` (or the
206891
+ * initial-state collapse's own mixed reset) retained.
207078
206892
  *
207079
206893
  * Guarded by `restorability` so it never discards work it didn't create:
207080
206894
  * refuses on a dirty working tree, no retained history, or HEAD having
@@ -207087,7 +206901,7 @@ const runRestoreCommand = (out) => gen(function* () {
207087
206901
  if (isNone(retained)) return yield* fail$2(/* @__PURE__ */ new Error("gtd restore: no retained history to restore."));
207088
206902
  const tip = retained.value;
207089
206903
  const headHash = yield* git.resolveRef("HEAD");
207090
- const check = yield* restorability(git, headHash, yield* git.lastCommitMessage(), tip);
206904
+ const check = yield* restorability(git, headHash, tip);
207091
206905
  if (!check.ok) return yield* fail$2(/* @__PURE__ */ new Error(`gtd restore: ${check.reason} — HEAD ${headHash.slice(0, 7)} is ahead of the retained tip ${tip.slice(0, 7)}.`));
207092
206906
  const after = yield* restAt(tip);
207093
206907
  const steps = [
@@ -207394,14 +207208,11 @@ const gatherBeatFields = (rest, rendered) => gen(function* () {
207394
207208
  });
207395
207209
  /**
207396
207210
  * Best-effort resolution of the currently-rested state for the viewer's
207397
- * `/state.json` route: prefers the review window's saved head over HEAD
207398
- * itself, since a request landing mid-window would otherwise read a HEAD
207399
- * that's been temporarily rewound. Any failure is swallowed to `null` — the
207400
- * browser just hides the panel.
207211
+ * `/state.json` route. Any failure is swallowed to `null` — the browser just
207212
+ * hides the panel.
207401
207213
  */
207402
207214
  const computeCurrentState = (model) => gen(function* () {
207403
- const reviewHead = yield* (yield* GitService).readRefOption(REVIEW_HEAD_REF);
207404
- const rest = yield* restAt(getOrUndefined(reviewHead));
207215
+ const rest = yield* restAt(void 0);
207405
207216
  const group = model.states.find((s) => s.name === rest.state)?.group;
207406
207217
  return buildCurrentStateModel(rest, rest.changes, rest.on, group);
207407
207218
  });
@@ -207504,6 +207315,8 @@ const dispatchVoidCommand = (command, json, sh, out) => {
207504
207315
  case "validate": return runValidateCommand(out);
207505
207316
  case "check": return runCheckCommand(command.mode, command.file, command.openQuestions ?? false);
207506
207317
  case "install": return runInstallCommand(out);
207318
+ case "summary": return runSummaryCommand(out);
207319
+ case "base": return runBaseCommand(out);
207507
207320
  }
207508
207321
  };
207509
207322
  /**
@@ -207512,10 +207325,6 @@ const dispatchVoidCommand = (command, json, sh, out) => {
207512
207325
  * repo-root-and-commit guard exactly when `needsOf(command.kind) === "state"`
207513
207326
  * (the `standaloneKinds` run bare). Both guard checks run before `dispatch`,
207514
207327
  * so a refusal writes nothing and emits no script by construction.
207515
- *
207516
- * No review-window close/open bracket runs here — every state-command
207517
- * handler decides for itself, off `ReviewWindow.ts`'s pure decision
207518
- * functions, whether a close/open belongs in the script it emits.
207519
207328
  */
207520
207329
  const runCommand = (command, json, sh, out) => {
207521
207330
  const dispatch = dispatchVoidCommand(command, json, sh, out);
@@ -207614,11 +207423,7 @@ const FLAGS = [
207614
207423
  decode: ([raw]) => right(raw ?? ""),
207615
207424
  scopeError: "gtd: --entry is only valid with no other command — use the bare `gtd --entry <state>` form; landing and entering are different verbs",
207616
207425
  valueHint: "<state>",
207617
- help: [
207618
- "(with no command at all) start a brand new process at",
207619
- "<state> — any declared, non-commit state — authenticated",
207620
- "as human"
207621
- ]
207426
+ help: ["(with no command at all) start a brand new process at", "<state> — any declared state — authenticated as human"]
207622
207427
  },
207623
207428
  {
207624
207429
  name: "--var",
@@ -207674,8 +207479,8 @@ const FLAGS = [
207674
207479
  help: [
207675
207480
  "enable stderr narration for this invocation: which rest",
207676
207481
  "resolved, which declared pattern each pending change",
207677
- "matched, which review-window action was emitted, and how",
207678
- "config resolved across layers. Aliased to -v"
207482
+ "matched, and how config resolved across layers. Aliased",
207483
+ "to -v"
207679
207484
  ]
207680
207485
  }
207681
207486
  ];
@@ -207716,8 +207521,8 @@ const COMMAND_ROWS = [
207716
207521
  arity: "none",
207717
207522
  details: [
207718
207523
  "Land whatever the tree now shows at the currently resolved",
207719
- "rest — a human capture, an agent/check turn, an empty",
207720
- "attempt (a fruitless prompt turn), or a squash. Pass",
207524
+ "rest — a human capture, an agent/check turn, or an empty",
207525
+ "attempt (a fruitless prompt turn). Pass",
207721
207526
  "--cost=<n> (optionally --model=<name>) to record the",
207722
207527
  "just-finished invocation's token cost and model on the",
207723
207528
  "turn commit (summed into it.processCost/",
@@ -207736,10 +207541,9 @@ const COMMAND_ROWS = [
207736
207541
  arity: "none",
207737
207542
  details: [
207738
207543
  "End the process currently underway without completing it:",
207739
- "close any open review checkout window, then rewind HEAD to",
207740
- "the commit the process started from, keeping everything it",
207741
- "produced as uncommitted changes. A no-op when no process is",
207742
- "underway"
207544
+ "rewind HEAD to the commit the process started from,",
207545
+ "keeping everything it produced as uncommitted changes. A",
207546
+ "no-op when no process is underway"
207743
207547
  ]
207744
207548
  },
207745
207549
  {
@@ -207747,12 +207551,12 @@ const COMMAND_ROWS = [
207747
207551
  kind: "restore",
207748
207552
  arity: "none",
207749
207553
  details: [
207750
- "Hard-reset HEAD back to the pre-squash tip retained by the",
207751
- "last squash/abandon (refs/worktree/gtd/history), undoing a",
207752
- "squash or bringing back an abandoned process's turns.",
207753
- "Refuses on a dirty working tree, when there is no retained",
207754
- "history, or when HEAD has advanced past the squash with",
207755
- "commits that would be lost"
207554
+ "Hard-reset HEAD back to the tip retained by the last",
207555
+ "abandon (refs/worktree/gtd/history), bringing back an",
207556
+ "abandoned process's turns. Refuses on a dirty working",
207557
+ "tree, when there is no retained history, or when HEAD has",
207558
+ "advanced past the retained tip with commits that would be",
207559
+ "lost"
207756
207560
  ]
207757
207561
  },
207758
207562
  {
@@ -207838,6 +207642,39 @@ const COMMAND_ROWS = [
207838
207642
  "'Writing your own driver'. Writes nothing: this installs",
207839
207643
  "knowledge into the calling agent's context, not files on disk."
207840
207644
  ]
207645
+ },
207646
+ {
207647
+ token: "summary",
207648
+ kind: "summary",
207649
+ arity: "none",
207650
+ details: [
207651
+ "Print the prompt for an agent to write the process HEAD",
207652
+ "closes or sits inside its own closing message — the entry",
207653
+ "commit, each human-authored commit (a review round, an",
207654
+ "answered question gate), the diff range to inspect, and",
207655
+ "it.processCost/processCostByModel. Writes nothing: no git,",
207656
+ "no state transition, no file, no session identity — the",
207657
+ "driver pipes the output to a cold agent and does what it",
207658
+ "wants with the result (a squash, an amend, a PR body).",
207659
+ "Refuses (exit 1) when the workflow declares no summary:",
207660
+ "template, or when the resolved run has no commits to name",
207661
+ "— runnable any time before the next thing lands on the",
207662
+ "branch"
207663
+ ]
207664
+ },
207665
+ {
207666
+ token: "base",
207667
+ kind: "base",
207668
+ arity: "none",
207669
+ details: [
207670
+ "Print the review anchor hash — the diff base an external",
207671
+ "tool (a diff, a PR tool, another agent) should point at —",
207672
+ "bare, newline-terminated, and nothing else. Writes nothing:",
207673
+ "no git, no state transition, no session identity. Before",
207674
+ "the first review round it's the process's diff base;",
207675
+ "afterward it's the most-recent review round's boundary.",
207676
+ "Refuses (exit 1) when no process is underway."
207677
+ ]
207841
207678
  }
207842
207679
  ];
207843
207680
  const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);