@pmelab/gtd 7.0.0 → 7.1.1

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.
@@ -2,6 +2,7 @@
2
2
  import { createRequire as __createRequire } from 'module';
3
3
  const require = __createRequire(import.meta.url);
4
4
  import { createRequire } from "node:module";
5
+ import { createServer } from "node:http";
5
6
  import * as Crypto from "node:crypto";
6
7
  import crypto from "node:crypto";
7
8
  import * as NFS from "node:fs";
@@ -11,7 +12,7 @@ import { homedir } from "node:os";
11
12
  import * as Path from "node:path";
12
13
  import { basename, dirname, join, resolve } from "node:path";
13
14
  import * as ChildProcess from "node:child_process";
14
- import { spawnSync } from "node:child_process";
15
+ import { spawn, spawnSync } from "node:child_process";
15
16
  import * as NodeUrl from "node:url";
16
17
  import { fileURLToPath, pathToFileURL } from "node:url";
17
18
  import * as readline from "node:readline";
@@ -8353,6 +8354,10 @@ const zip$3 = /*#__PURE__*/ dual(2, (self, that) => flatMap$6(self, (a) => map$8
8353
8354
  const zipLeft$1 = /*#__PURE__*/ dual(2, (self, that) => flatMap$6(self, (a) => as$1(that, a)));
8354
8355
  const zipRight$3 = /*#__PURE__*/ dual(2, (self, that) => flatMap$6(self, () => that));
8355
8356
  const zipWith$2 = /*#__PURE__*/ dual(3, (self, that, f) => flatMap$6(self, (a) => map$8(that, (b) => f(a, b))));
8357
+ const never$1 = /*#__PURE__*/ asyncInterrupt(() => {
8358
+ const interval = setInterval(() => {}, 2 ** 31 - 1);
8359
+ return sync$2(() => clearInterval(interval));
8360
+ });
8356
8361
  const interruptFiber = (self) => flatMap$6(fiberId, (fiberId) => pipe(self, interruptAsFiber(fiberId)));
8357
8362
  const interruptAsFiber = /*#__PURE__*/ dual(2, (self, fiberId) => flatMap$6(self.interruptAsFork(fiberId), () => self.await));
8358
8363
  /** @internal */
@@ -16968,6 +16973,21 @@ const dieMessage = dieMessage$1;
16968
16973
  */
16969
16974
  const gen = gen$1;
16970
16975
  /**
16976
+ * An effect that that runs indefinitely and never produces any result. The
16977
+ * moral equivalent of `while(true) {}`, only without the wasted CPU cycles.
16978
+ *
16979
+ * **When to Use**
16980
+ *
16981
+ * It could be useful for long-running background tasks or to simulate waiting
16982
+ * behavior without actually consuming resources. This effect is ideal for cases
16983
+ * where you want to keep the program alive or in a certain state without
16984
+ * performing any active work.
16985
+ *
16986
+ * @since 2.0.0
16987
+ * @category Creating Effects
16988
+ */
16989
+ const never = never$1;
16990
+ /**
16971
16991
  * Creates an `Effect` that always succeeds with a given value.
16972
16992
  *
16973
16993
  * **When to Use**
@@ -189648,6 +189668,323 @@ const validateDefinition = (def) => {
189648
189668
  ];
189649
189669
  };
189650
189670
  //#endregion
189671
+ //#region src/Submachines.ts
189672
+ /**
189673
+ * Compile-time SUB-MACHINE EXPANSION (Option C) — a raw → raw pre-pass that runs
189674
+ * at the very top of `compileWorkflowConfig` (src/PatternConfig.ts), BEFORE any
189675
+ * per-state compilation. It turns two OPTIONAL top-level `workflow:` keys —
189676
+ *
189677
+ * ```yaml
189678
+ * submachines: # reusable, parameterized clusters of states
189679
+ * <name>:
189680
+ * params: [<param>, ...]
189681
+ * states: { <local>: <StateDef, with `$param` placeholders> }
189682
+ * use: # invocations, each expanded into concrete states
189683
+ * - submachine: <name>
189684
+ * as: { <local>: <concreteName>, ... } # rename map (default: identity)
189685
+ * with: { <param>: <value>, ... } # bind each `$param`
189686
+ * set: { <local>: { <field>: <value> } } # extra per-instance state fields
189687
+ * ```
189688
+ *
189689
+ * — into ordinary concrete `states`, then strips `submachines`/`use` from its
189690
+ * output so the rest of the compiler (and `KNOWN_TOP_KEYS`) only ever sees the
189691
+ * familiar `{ vars?, states, modes? }` shape. The pure engine (`PatternMachine`)
189692
+ * is entirely unaware sub-machines exist: expansion is a source-authoring
189693
+ * convenience with NO runtime footprint (see STATES.md, "Sub-machines"). Aliasing
189694
+ * each invocation's local state names (via `as:`) to the exact concrete names a
189695
+ * hand-written workflow would use makes the expanded definition BYTE-IDENTICAL to
189696
+ * the flat one — the guarantee the bundled template's golden test pins
189697
+ * (src/workflows/templates.test.ts).
189698
+ *
189699
+ * ## Two uses of one mechanism
189700
+ *
189701
+ * - **Dedup** — a multi-instance sub-machine (a gate/loop authored once, invoked
189702
+ * several times with different `with:`/`as:`) removes duplicated states.
189703
+ * - **Encapsulation** — a single-instance sub-machine groups a complex cluster
189704
+ * into one named block for source comprehension. It expands 1:1, so it has no
189705
+ * runtime effect; its value is authoring clarity (and a hook for a future
189706
+ * viewer).
189707
+ *
189708
+ * ## Substitution
189709
+ *
189710
+ * A `$name` token is resolved from the invocation's `with:` bindings. It is
189711
+ * matched ONLY as a WHOLE string value — a field whose entire value is `$name`,
189712
+ * or an `on`/`retry.otherwise` target that is exactly `$name` — never as a
189713
+ * substring, so bash `$var`/`${var}` inside a `script:` and Eta
189714
+ * `<%= it.vars.x %>` inside any content are left untouched. Text that varies per
189715
+ * instance (a message, a describe sentence, a prompt) is therefore passed WHOLE
189716
+ * as a `$name` binding, not spliced in.
189717
+ *
189718
+ * An `on`/`retry.otherwise` target that names one of the sub-machine's OWN local
189719
+ * states is rewritten through the `as:` rename map (so intra-cluster edges point
189720
+ * at the renamed concrete states); any other target passes through verbatim (it
189721
+ * names a concrete top-level state, resolved after the merge). `on` PATTERN keys
189722
+ * are never touched (they are literal, exactly as the engine treats them).
189723
+ *
189724
+ * All findings are collected into the shared `errors` array the caller threads
189725
+ * through, aggregated with every other config-shape/validation finding into the
189726
+ * single error `compileWorkflowConfig` throws — a bad `submachines:`/`use:` fails
189727
+ * loudly at load time, never at step time.
189728
+ */
189729
+ const isPlainObject$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
189730
+ const describeType$1 = (v) => {
189731
+ if (v === null) return "null";
189732
+ if (Array.isArray(v)) return "array";
189733
+ return typeof v;
189734
+ };
189735
+ /** A whole-value parameter reference: a string that is EXACTLY `$name`. */
189736
+ const PARAM_REF = /^\$([A-Za-z_][A-Za-z0-9_]*)$/;
189737
+ const KNOWN_SUBMACHINE_KEYS = /* @__PURE__ */ new Set(["params", "states"]);
189738
+ const KNOWN_INVOCATION_KEYS = /* @__PURE__ */ new Set([
189739
+ "submachine",
189740
+ "as",
189741
+ "with",
189742
+ "set",
189743
+ "name"
189744
+ ]);
189745
+ /** Rewrite an `on`/`retry.otherwise` target: a local -> its concrete name, a `$param` -> its binding, anything else verbatim. */
189746
+ const resolveTarget = (target, ctx, errors) => {
189747
+ const m = PARAM_REF.exec(target);
189748
+ if (m) {
189749
+ const value = ctx.params[m[1]];
189750
+ if (value === void 0) {
189751
+ errors.push(`${ctx.where}: target references unbound param "$${m[1]}"`);
189752
+ return target;
189753
+ }
189754
+ if (typeof value !== "string") {
189755
+ errors.push(`${ctx.where}: param "$${m[1]}" used as a target must be a string`);
189756
+ return target;
189757
+ }
189758
+ return value;
189759
+ }
189760
+ if (ctx.locals.has(target)) return ctx.as[target] ?? target;
189761
+ return target;
189762
+ };
189763
+ /** Substitute a WHOLE-value `$param` in any scalar string field; non-`$param` strings and non-strings pass through unchanged. */
189764
+ const substituteScalar = (value, ctx, errors) => {
189765
+ if (typeof value !== "string") return value;
189766
+ const m = PARAM_REF.exec(value);
189767
+ if (!m) return value;
189768
+ const bound = ctx.params[m[1]];
189769
+ if (bound === void 0) {
189770
+ errors.push(`${ctx.where}: references unbound param "$${m[1]}"`);
189771
+ return value;
189772
+ }
189773
+ return bound;
189774
+ };
189775
+ /** Rewrite the `on` mapping — each row's target (string, or `{ to, describe }`) — preserving pattern keys and declaration order. */
189776
+ const expandOn = (raw, ctx, errors) => {
189777
+ if (!isPlainObject$2(raw)) return raw;
189778
+ const out = {};
189779
+ for (const [pattern, value] of Object.entries(raw)) if (typeof value === "string") out[pattern] = resolveTarget(value, ctx, errors);
189780
+ else if (isPlainObject$2(value)) {
189781
+ const next = { ...value };
189782
+ if (typeof value["to"] === "string") next["to"] = resolveTarget(value["to"], ctx, errors);
189783
+ if (typeof value["describe"] === "string") next["describe"] = substituteScalar(value["describe"], ctx, errors);
189784
+ out[pattern] = next;
189785
+ } else out[pattern] = value;
189786
+ return out;
189787
+ };
189788
+ /** Rewrite a `retry` block's `otherwise` target (and pass `max` through). */
189789
+ const expandRetry = (raw, ctx, errors) => {
189790
+ if (!isPlainObject$2(raw)) return raw;
189791
+ const next = { ...raw };
189792
+ if (typeof raw["otherwise"] === "string") next["otherwise"] = resolveTarget(raw["otherwise"], ctx, errors);
189793
+ return next;
189794
+ };
189795
+ /** Clone one sub-machine local state, applying param substitution and target rewriting. */
189796
+ const expandState = (stateRaw, ctx, errors) => {
189797
+ if (!isPlainObject$2(stateRaw)) {
189798
+ errors.push(`${ctx.where}: sub-machine state must be an object, got ${describeType$1(stateRaw)}`);
189799
+ return {};
189800
+ }
189801
+ const out = {};
189802
+ for (const [key, value] of Object.entries(stateRaw)) if (key === "on") out[key] = expandOn(value, ctx, errors);
189803
+ else if (key === "retry") out[key] = expandRetry(value, ctx, errors);
189804
+ else out[key] = substituteScalar(value, ctx, errors);
189805
+ return out;
189806
+ };
189807
+ /** Validate the `submachines:` map's shape (findings pushed onto `errors`), returning it best-effort. */
189808
+ const readSubmachines = (raw, declared, errors) => {
189809
+ const submachines = isPlainObject$2(raw) ? raw : {};
189810
+ if (declared && !isPlainObject$2(raw)) errors.push(`"submachines" must be a mapping of name -> { params, states }, got ${describeType$1(raw)}`);
189811
+ for (const [name, sm] of Object.entries(submachines)) {
189812
+ if (!isPlainObject$2(sm)) {
189813
+ errors.push(`submachine "${name}": must be an object with "params" and "states"`);
189814
+ continue;
189815
+ }
189816
+ const unknown = Object.keys(sm).filter((k) => !KNOWN_SUBMACHINE_KEYS.has(k));
189817
+ if (unknown.length > 0) errors.push(`submachine "${name}": unknown key(s) ${unknown.join(", ")}`);
189818
+ if (!isPlainObject$2(sm["states"]) || Object.keys(sm["states"]).length === 0) errors.push(`submachine "${name}": "states" must be a non-empty mapping`);
189819
+ }
189820
+ return submachines;
189821
+ };
189822
+ /** Read one invocation's `as:` rename map (local -> concrete), validating against the sub-machine's locals. */
189823
+ const readAs = (raw, smStates, where, errors) => {
189824
+ const as = {};
189825
+ if (raw === void 0) return as;
189826
+ if (!isPlainObject$2(raw)) {
189827
+ errors.push(`${where}: "as" must be a mapping of local -> concrete name`);
189828
+ return as;
189829
+ }
189830
+ for (const [local, concrete] of Object.entries(raw)) {
189831
+ if (typeof concrete !== "string") {
189832
+ errors.push(`${where}: "as.${local}" must be a string`);
189833
+ continue;
189834
+ }
189835
+ if (!(local in smStates)) errors.push(`${where}: "as" names "${local}", not a state of it`);
189836
+ as[local] = concrete;
189837
+ }
189838
+ return as;
189839
+ };
189840
+ /** Read one invocation's `set:` overrides (local -> extra fields), validating against the sub-machine's locals. */
189841
+ const readSet = (raw, smStates, where, errors) => {
189842
+ const set = {};
189843
+ if (raw === void 0) return set;
189844
+ if (!isPlainObject$2(raw)) {
189845
+ errors.push(`${where}: "set" must be a mapping of local -> { field: value }`);
189846
+ return set;
189847
+ }
189848
+ for (const [local, fields] of Object.entries(raw)) {
189849
+ if (!isPlainObject$2(fields)) {
189850
+ errors.push(`${where}: "set.${local}" must be an object of extra fields`);
189851
+ continue;
189852
+ }
189853
+ if (!(local in smStates)) errors.push(`${where}: "set" names "${local}", not a state of it`);
189854
+ set[local] = fields;
189855
+ }
189856
+ return set;
189857
+ };
189858
+ /** Validate one invocation's shape and resolve its named sub-machine; `undefined` (with a finding) on anything malformed. */
189859
+ const resolveInvocation = (invRaw, index, submachines, errors) => {
189860
+ const where = `use[${index}]`;
189861
+ if (!isPlainObject$2(invRaw)) {
189862
+ errors.push(`${where}: each invocation must be an object with a "submachine" key`);
189863
+ return;
189864
+ }
189865
+ const unknown = Object.keys(invRaw).filter((k) => !KNOWN_INVOCATION_KEYS.has(k));
189866
+ if (unknown.length > 0) errors.push(`${where}: unknown key(s) ${unknown.join(", ")}`);
189867
+ const smName = invRaw["submachine"];
189868
+ if (typeof smName !== "string") {
189869
+ errors.push(`${where}: "submachine" must be a string naming a declared sub-machine`);
189870
+ return;
189871
+ }
189872
+ const sm = submachines[smName];
189873
+ if (!isPlainObject$2(sm) || !isPlainObject$2(sm["states"])) {
189874
+ errors.push(`${where}: unknown sub-machine "${smName}"`);
189875
+ return;
189876
+ }
189877
+ return {
189878
+ smStates: sm["states"],
189879
+ scope: `${where} (${smName})`,
189880
+ invocation: invRaw
189881
+ };
189882
+ };
189883
+ /** Clone each local state into its concrete name, claiming names (collision → finding) and applying `set:` overrides. */
189884
+ const produceStates = (smStates, ctx, set, claimed, errors) => {
189885
+ const out = {};
189886
+ for (const [local, stateRaw] of Object.entries(smStates)) {
189887
+ const concrete = ctx.as[local] ?? local;
189888
+ if (claimed.has(concrete)) {
189889
+ errors.push(`${ctx.where}: expands to state "${concrete}", which collides with an existing state`);
189890
+ continue;
189891
+ }
189892
+ claimed.add(concrete);
189893
+ const state = expandState(stateRaw, ctx, errors);
189894
+ const overrides = set[local];
189895
+ out[concrete] = overrides !== void 0 ? {
189896
+ ...state,
189897
+ ...overrides
189898
+ } : state;
189899
+ }
189900
+ return out;
189901
+ };
189902
+ /**
189903
+ * Expand ONE `use:` invocation into concrete states, tracking claimed names in
189904
+ * `claimed` (mutated) and pushing findings onto `errors`. Returns the produced
189905
+ * `concreteName -> stateDef` map (empty on a malformed invocation).
189906
+ */
189907
+ const expandInvocation = (invRaw, index, submachines, claimed, errors) => {
189908
+ const resolved = resolveInvocation(invRaw, index, submachines, errors);
189909
+ if (resolved === void 0) return {};
189910
+ const { smStates, scope, invocation } = resolved;
189911
+ const as = readAs(invocation["as"], smStates, scope, errors);
189912
+ const withRaw = invocation["with"];
189913
+ const params = isPlainObject$2(withRaw) ? withRaw : {};
189914
+ if (withRaw !== void 0 && !isPlainObject$2(withRaw)) errors.push(`${scope}: "with" must be a mapping of param -> value`);
189915
+ const set = readSet(invocation["set"], smStates, scope, errors);
189916
+ const ctx = {
189917
+ locals: new Set(Object.keys(smStates)),
189918
+ as,
189919
+ params,
189920
+ where: scope
189921
+ };
189922
+ return produceStates(smStates, ctx, set, claimed, errors);
189923
+ };
189924
+ /**
189925
+ * Expand `submachines:`/`use:` into concrete `states`. Returns a NEW raw object
189926
+ * with those two keys removed and `states` augmented; a non-object input, or one
189927
+ * declaring NEITHER key, is returned unchanged (so ordinary workflows are a
189928
+ * pure passthrough). Findings are pushed onto `errors`.
189929
+ */
189930
+ const expandSubmachines = (raw, errors) => {
189931
+ if (!isPlainObject$2(raw)) return raw;
189932
+ if (raw["submachines"] === void 0 && raw["use"] === void 0) return raw;
189933
+ const submachines = readSubmachines(raw["submachines"], raw["submachines"] !== void 0, errors);
189934
+ const useRaw = raw["use"];
189935
+ const invocations = Array.isArray(useRaw) ? useRaw : [];
189936
+ if (raw["use"] !== void 0 && !Array.isArray(useRaw)) errors.push(`"use" must be an array of invocations, got ${describeType$1(useRaw)}`);
189937
+ const baseStates = isPlainObject$2(raw["states"]) ? { ...raw["states"] } : {};
189938
+ const claimed = new Set(Object.keys(baseStates));
189939
+ const expanded = {};
189940
+ invocations.forEach((invRaw, index) => {
189941
+ Object.assign(expanded, expandInvocation(invRaw, index, submachines, claimed, errors));
189942
+ });
189943
+ const result = {
189944
+ ...raw,
189945
+ states: {
189946
+ ...baseStates,
189947
+ ...expanded
189948
+ }
189949
+ };
189950
+ delete result["submachines"];
189951
+ delete result["use"];
189952
+ return result;
189953
+ };
189954
+ /**
189955
+ * Describe a raw workflow's `use:` invocations as visualization GROUPS — which
189956
+ * concrete states each sub-machine invocation produced — WITHOUT expanding or
189957
+ * validating (a best-effort read for tooling like `gtd visualize`; malformed
189958
+ * entries are skipped, not reported). A workflow with no `use:` yields `[]`.
189959
+ * This is the only way to recover the sub-machine grouping after
189960
+ * `compileWorkflowConfig` has flattened it away.
189961
+ */
189962
+ const collectGroups = (raw) => {
189963
+ if (!isPlainObject$2(raw)) return [];
189964
+ const submachines = isPlainObject$2(raw["submachines"]) ? raw["submachines"] : {};
189965
+ const use = Array.isArray(raw["use"]) ? raw["use"] : [];
189966
+ const smUseCount = /* @__PURE__ */ new Map();
189967
+ for (const inv of use) if (isPlainObject$2(inv) && typeof inv["submachine"] === "string") smUseCount.set(inv["submachine"], (smUseCount.get(inv["submachine"]) ?? 0) + 1);
189968
+ const groups = [];
189969
+ use.forEach((inv, index) => {
189970
+ if (!isPlainObject$2(inv)) return;
189971
+ const smName = inv["submachine"];
189972
+ if (typeof smName !== "string") return;
189973
+ const sm = submachines[smName];
189974
+ if (!isPlainObject$2(sm) || !isPlainObject$2(sm["states"])) return;
189975
+ const smStates = sm["states"];
189976
+ const as = isPlainObject$2(inv["as"]) ? inv["as"] : {};
189977
+ const states = Object.keys(smStates).map((local) => typeof as[local] === "string" ? as[local] : local);
189978
+ const name = typeof inv["name"] === "string" ? inv["name"] : (smUseCount.get(smName) ?? 0) > 1 ? `${smName}#${index}` : smName;
189979
+ groups.push({
189980
+ submachine: smName,
189981
+ name,
189982
+ states
189983
+ });
189984
+ });
189985
+ return groups;
189986
+ };
189987
+ //#endregion
189651
189988
  //#region src/PatternConfig.ts
189652
189989
  /**
189653
189990
  * The v3 `.gtdrc` `workflow:` config compiler (see
@@ -190161,11 +190498,13 @@ const compileState = (name, raw, configDir, errors, inlineFileRefs) => {
190161
190498
  const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true) => {
190162
190499
  if (!isPlainObject$1(raw)) throw new Error(`workflow config: must be an object, got ${describeType(raw)}`);
190163
190500
  const errors = [];
190164
- const unknownTopKeys = Object.keys(raw).filter((k) => !KNOWN_TOP_KEYS.has(k));
190501
+ const expanded = expandSubmachines(raw, errors);
190502
+ const cfg = isPlainObject$1(expanded) ? expanded : raw;
190503
+ const unknownTopKeys = Object.keys(cfg).filter((k) => !KNOWN_TOP_KEYS.has(k));
190165
190504
  if (unknownTopKeys.length > 0) errors.push(`unknown top-level key(s) ${unknownTopKeys.join(", ")}`);
190166
- const vars = compileVarsMap(raw.vars, errors);
190167
- const modes = mergeModes(compileModesMap(raw.modes, errors), rcModes);
190168
- const rawStates = raw.states;
190505
+ const vars = compileVarsMap(cfg.vars, errors);
190506
+ const modes = mergeModes(compileModesMap(cfg.modes, errors), rcModes);
190507
+ const rawStates = cfg.states;
190169
190508
  if (!isPlainObject$1(rawStates) || Object.keys(rawStates).length === 0) {
190170
190509
  errors.push(`"states" must be a non-empty object`);
190171
190510
  throw new Error(formatErrors(errors));
@@ -190186,7 +190525,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true) =
190186
190525
  };
190187
190526
  //#endregion
190188
190527
  //#region src/workflows/unified.yaml
190189
- var unified_default = "# The unified gtd workflow template — gtd's BUILT-IN DEFAULT workflow. When no\n# `workflow:` key is configured anywhere in the cwd→home config chain,\n# src/Config.ts runs this template directly, so a state command works with no\n# configuration at all. `gtd init` seeds only the settings a project tunes\n# (vars.testCommand + a modes: formatting suggestion), never the workflow\n# itself. See STATES.md §10 for the full walkthrough.\n#\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/templates.ts does the compiling). Every content string below\n# is INLINE (no `./`-relative file references): the bundled template ships\n# inside a single-file build (`dist/gtd.bundle.mjs`), so its content must\n# already be resolved at BUILD time. `renderInitConfig` (src/workflows/\n# templates.ts) can MATERIALIZE this inline source into a `.gtdrc.json`\n# `workflow:` value — the way to eject/customize the machine, and the hermetic\n# test fixture — but `gtd init` itself no longer writes the workflow.\n#\n# ── Four entry points behind a green-baseline gate, one shared tail ─────────\n#\n# EVERY entry first runs the test suite and only proceeds when it is GREEN — you\n# never start new work (or a review) on top of a red baseline. A red run halts\n# at a human `*-start-blocked` gate (fix the failures and re-run, exactly like\n# the `escalate` states), or is what `gtd fix` exists to repair.\n#\n# The initial `idle` state FORKS on which steering file the human creates,\n# routing into that flow's OWN start gate:\n#\n# * .gtd/REQUIREMENTS.md -> `adv-start-check` -> the ADVANCED flow (product +\n# technical Q&A, package decomposition, per-package parallel build +\n# agentic spec-review).\n# * anything else (e.g. .gtd/TODO.md) -> `start-check` -> the SIMPLE flow (one\n# grilling Q&A loop, a monolithic build, no decomposition, no agentic\n# review).\n#\n# A third entry, `gtd review <commitish>`, jumps to `review-start-check`\n# (reviewEntry) and, once green, to `reviewing` to review a foreign branch\n# through the same shared tail. A fourth, `gtd fix`, jumps to `fix-check`\n# (fixEntry): a red suite drops straight into the shared `fixing` loop and out\n# through the review + squash tail; a green suite is a no-op back to `idle`.\n#\n# All three converge at `reviewing` -> `await-review`. Ticking a REVIEW.md box\n# means only \"I reviewed this hunk\"; a COMMENT (not an unticked box) is what asks\n# for changes. Every human step at `await-review` goes to `review-deciding`,\n# which inspects the step and routes:\n# * no comment (every box ticked, no REVIEW.md note, no code edit) -> the diff\n# is a bare REVIEW.md deletion -> squashing -> done (the ONLY path to the\n# finale — the whole cycle collapses into one commit).\n# * any comment — a note in REVIEW.md OR a hand-edit to code — ->\n# review-deciding writes REVIEW_FEEDBACK.md -> feedback-building implements\n# it (completing any code the human hand-edited, never reverting their\n# lines) -> checking -> reviewing regenerates an INCREMENTAL REVIEW.md\n# (review-deciding carries `reviewBase: true`, so each feedback round's\n# review + checkout window covers only `last-review..HEAD`, i.e. the agent's\n# follow-through, not the human's own edit) -> await-review, looping until\n# sign-off.\n# Two dead ends never commit: the sign-off gate (src/program.ts) REFUSES a step\n# that leaves a box unticked with no comment (finish reviewing first) and a\n# deleted REVIEW.md (restore it and tick the boxes).\n#\n# The shared health segment (`checking`/`fixing`/`escalate`) serves the simple\n# build and the feedback loop — its green edge targets\n# `reviewing`. The advanced per-package loop keeps its OWN health triple\n# (`adv-checking`/`adv-fixing`/`adv-escalate`) because its green edge targets\n# the per-package `spec-review` gate instead.\n#\n# KNOWN LIMITATION: `on` pattern keys are NOT Eta templates — they keep the\n# LITERAL `.gtd/...` paths matching these vars' DEFAULT values. Repointing a\n# file var changes what templates read/write WITHOUT changing what the `on`\n# patterns match, desyncing the machine (see docs/configuration.md).\n\nvars:\n testCommand: npm test\n todoFile: .gtd/TODO.md\n requirementsFile: .gtd/REQUIREMENTS.md\n architectureFile: .gtd/ARCHITECTURE.md\n packagesDir: .gtd/packages\n nextFile: .gtd/NEXT.md\n reviewFile: .gtd/REVIEW.md\n reviewRawFile: .gtd/REVIEW_RAW.md\n reviewFeedbackFile: .gtd/REVIEW_FEEDBACK.md\n feedbackFile: .gtd/FEEDBACK.md\n specFeedbackFile: .gtd/SPEC_FEEDBACK.md\n commitMsgFile: .gtd/COMMIT_MSG.md\n # The two opaque model tiers every agent state draws from — repoint them here\n # (or via a top-level `.gtdrc` `vars:` key / a `GTD_VAR_plannerModel` env var)\n # to change every state on that tier at once. `plannerModel` (default \"smart\")\n # is the heavier tier for the one-shot planning/architecting/review turns;\n # `coderModel` (default \"base\") is the tier for the build/fix turns.\n plannerModel: smart\n coderModel: base\n\nstates:\n # ── Shared entry ────────────────────────────────────────────────────────\n idle:\n actor: human\n initial: true\n # Every human-gate `message:` below closes with the same rendered \"What\n # each change does next\" list, built from `it.edges` (the resting state's\n # own `on` edges — each edge's `describe:` is the single source of truth).\n message: |\n No active gtd cycle.\n\n To start one, create a steering file describing what you want built:\n\n - `.gtd/TODO.md` — a short sketch — starts the SIMPLE flow (plan, build,\n review).\n - `.gtd/REQUIREMENTS.md` — the product requirements — starts the ADVANCED\n flow (product + technical Q&A, package decomposition, parallel build,\n per-package agentic review).\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* .gtd/REQUIREMENTS.md\":\n to: adv-start-check\n describe: >-\n Save the product requirements to `.gtd/REQUIREMENTS.md` to start the\n ADVANCED flow — gtd first checks the test baseline is green, then the\n agent develops them through product then technical Q&A, decomposes the\n work into packages, and builds each in parallel (**adv-start-check**).\n \"* **\":\n to: start-check\n describe: >-\n Save a short sketch to `.gtd/TODO.md` (or any other change) to start\n the SIMPLE flow — gtd first checks the test baseline is green, then the\n agent reads it, explores the codebase, and develops it into a concrete\n implementation plan (**start-check**).\n\n # ── Green-baseline gate: simple flow ──────────────────────────────────────\n # Runs the suite before any planning starts. Its script is `checking`'s test\n # wrapper verbatim (red -> .gtd/FEEDBACK.md, green -> remove it); the `on`\n # rules turn that into \"halt at start-blocked\" vs \"proceed to planning\".\n start-check:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite before starting new work; a red run\n # records .gtd/FEEDBACK.md (halt), a green run cleans it up (proceed).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A .gtd/FEEDBACK.md\": start-blocked\n \"M .gtd/FEEDBACK.md\": start-blocked\n \"D .gtd/FEEDBACK.md\": planning\n \"C\": planning\n\n start-blocked:\n actor: human\n file: <%= it.vars.feedbackFile %>\n message: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: start-check\n describe: >-\n Fix the failing tests — edit the code and/or `.gtd/FEEDBACK.md` — to\n re-run the baseline check (**start-check**). To repair the baseline as\n its own separate reviewed commit instead, abandon this start and run\n `gtd fix` from a clean `idle`.\n\n # ── Simple flow: plan-iteration loop, monolithic build ────────────────────\n planning:\n actor: agent\n file: <%= it.vars.todoFile %>\n model: <%= it.vars.plannerModel %>\n memory: plan\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.todoFile %>`, which this prompt tells you to develop.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build — or, on a\n return lap, the plan you already wrote plus the human's edits and comments\n on it. Read it, explore the codebase, and develop it into a concrete\n implementation plan: the files to change, the approach, and the steps to\n take.\n\n Do NOT ask questions and do NOT add an \"Open Questions\" section — decide\n every open point yourself and write the plan as a set of concrete\n decisions. Where a decision is genuinely a judgement call, state the\n choice you made and, in one line, why. The human iterates on the plan by\n editing it directly, not by answering questions.\n\n On a return lap, `<%= it.vars.todoFile %>` carries the human's edits and\n inline comments on your previous plan. Fold every edit and comment into a\n revised, self-contained plan and remove the addressed comments — the plan\n is always the current, clean proposal, never a diff.\n\n Leave `<%= it.vars.todoFile %>` uncommitted and finish your turn — do\n not commit, and do not run `gtd step agent` yourself; the harness does\n that.\n on:\n \"* **\": plan-review\n\n plan-review:\n actor: human\n file: <%= it.vars.todoFile %>\n message: |\n `<%= it.vars.todoFile %>` holds the proposed implementation plan. Read it,\n then either accept it as-is or edit it — rewrite a step, add a\n constraint, or drop an inline comment — to send it back for another round.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: building\n describe: >-\n Change nothing to accept the plan as-is and move on to implementation\n (**building**).\n \"* **\":\n to: planning\n describe: >-\n Edit the plan — rewrite a step, add a constraint, or leave an inline\n comment — to send it back for another round (**planning**).\n\n building:\n actor: agent\n file: <%= it.vars.todoFile %>\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create or edit anything under `.gtd/` except deleting\n `<%= it.vars.todoFile %>`, which this prompt tells you to delete once its\n work is complete.\n\n The plan to implement is: <%~ it.read(it.vars.todoFile) %>\n\n Read that plan and implement it in full. Use TDD discipline: one test,\n then the implementation that passes it, then the next — never write all\n the tests first.\n\n Delete `<%= it.vars.todoFile %>` once the work it describes is complete\n and verified. Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": checking\n\n # ── Green-baseline gate: advanced flow ────────────────────────────────────\n adv-start-check:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite before starting the advanced flow; a red\n # run records .gtd/FEEDBACK.md (halt), a green run cleans it up (proceed).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A .gtd/FEEDBACK.md\": adv-start-blocked\n \"M .gtd/FEEDBACK.md\": adv-start-blocked\n \"D .gtd/FEEDBACK.md\": adv-grilling\n \"C\": adv-grilling\n\n adv-start-blocked:\n actor: human\n file: <%= it.vars.feedbackFile %>\n message: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: adv-start-check\n describe: >-\n Fix the failing tests — edit the code and/or `.gtd/FEEDBACK.md` — to\n re-run the baseline check (**adv-start-check**). To repair the baseline\n as its own separate reviewed commit instead, abandon this start and run\n `gtd fix` from a clean `idle`.\n\n # ── Advanced flow: product + technical Q&A ────────────────────────────────\n adv-grilling:\n actor: agent\n file: <%= it.vars.requirementsFile %>\n mode: qa\n model: <%= it.vars.plannerModel %>\n memory: plan\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.requirementsFile %>`, which this prompt tells you to develop.\n\n `<%= it.vars.requirementsFile %>` holds the product requirements (or the\n plan you already wrote plus the human's answers on a return lap). Read it,\n explore the codebase, and develop it into a concrete, PRODUCT-level plan\n in this one turn.\n\n Scope: product and user-facing decisions only. Leave implementation\n details (file/module structure, data models, tech-stack choices) for the\n next phase (`<%= it.vars.architectureFile %>`).\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading near the top: one `### <question>` sub-heading\n each, followed by a checkbox list of candidate answers — aim for TWO\n concrete options plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself — the human ticks exactly one per question. The\n last `- [ ] _your answer_` line is the human's own-answer slot; write it\n with the literal `_your answer_` placeholder and leave it for them.\n\n On a RETURN lap the human has ticked one option per question (or replaced\n `_your answer_` with their own text). For each answered question: fold the\n chosen answer into the plan prose, then MOVE the question down into a\n `## Answered Questions` section as `### <question>` followed by the\n resolved answer in plain PROSE (drop the checkboxes). Never re-raise a\n question the human deleted; if the human deleted the whole\n `## Open Questions` section, treat the plan as accepted and surface no new\n questions unless something is genuinely still blocking. Omit\n `## Open Questions` entirely once there are none left.\n\n Leave `<%= it.vars.requirementsFile %>` uncommitted and finish your turn.\n on:\n \"* **\": adv-grilling-answer\n\n adv-grilling-answer:\n actor: human\n file: <%= it.vars.requirementsFile %>\n mode: qa\n answerGate: true\n message: |\n `<%= it.vars.requirementsFile %>` holds the product plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: architecting\n describe: >-\n With no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to accept the\n product plan and move on to technical architecting (**architecting**).\n \"* **\":\n to: adv-grilling\n describe: >-\n Tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**adv-grilling**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n\n architecting:\n actor: agent\n file: <%= it.vars.architectureFile %>\n mode: qa\n model: <%= it.vars.plannerModel %>\n memory: plan\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.architectureFile %>` and `<%= it.vars.requirementsFile %>`,\n which this prompt tells you to write and delete respectively.\n\n Read `<%= it.vars.requirementsFile %>` (the converged product plan) and\n develop `<%= it.vars.architectureFile %>` from it in this one turn:\n file/module structure, data models, library/tech-stack choices,\n error-handling strategy — the *how*, building on the *what* already\n settled. Do not re-open product-level decisions; treat them as settled.\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading exactly as the grilling phase does: one\n `### <question>` each, followed by a checkbox list — TWO concrete options\n plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself. On a RETURN lap the human has ticked one option\n per question (or replaced `_your answer_` with their own text): fold each\n chosen answer into the plan prose, then MOVE the question into a\n `## Answered Questions` section as `### <question>` + the resolved answer\n in plain PROSE (drop the checkboxes). Never re-raise a deleted question;\n treat a deleted `## Open Questions` section as acceptance. Omit\n `## Open Questions` once there are none left.\n\n Once `<%= it.vars.architectureFile %>` is written, delete\n `<%= it.vars.requirementsFile %>` — its content has been folded in and it\n must not linger. Leave everything uncommitted and finish your turn.\n on:\n \"* **\": architecting-answer\n\n architecting-answer:\n actor: human\n file: <%= it.vars.architectureFile %>\n mode: qa\n answerGate: true\n message: |\n `<%= it.vars.architectureFile %>` holds the technical plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: decompose\n describe: >-\n With no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to accept the\n technical plan and move on to decomposing the work into packages\n (**decompose**).\n \"* **\":\n to: architecting\n describe: >-\n Tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**architecting**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n\n # ── Advanced flow: decomposition + per-package parallel build loop ─────────\n decompose:\n actor: agent\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except the\n package files this prompt tells you to write (under\n `<%= it.vars.packagesDir %>/`), and `<%= it.vars.architectureFile %>`,\n which it tells you to delete.\n\n Read `<%= it.vars.architectureFile %>` (the converged technical plan) and\n decompose it into an ordered set of WORK PACKAGES, one file each under\n `<%= it.vars.packagesDir %>/` (e.g. `<%= it.vars.packagesDir %>/01-name.md`,\n `<%= it.vars.packagesDir %>/02-name.md`, ...). Order the packages so the\n test suite can pass again after each one lands.\n\n Each package file describes a set of INDEPENDENT tasks that can be\n implemented concurrently without ordering constraints between them — a\n single build turn will fan them out to parallel subagents. Give each task\n a clear description, acceptance criteria as `- [ ]` checkboxes, and the\n relevant paths. Also state the package's overall requirement so it can be\n reviewed against its spec. Do NOT put ordering-dependent tasks in the same\n package — split them across ordered packages instead.\n\n No package file may reference any other `.gtd/` file. Once the package\n files are written, delete `<%= it.vars.architectureFile %>`. Leave\n everything uncommitted and finish your turn.\n on:\n \"* .gtd/packages/**\": picking\n\n picking:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn (the package queue arbiter) — the loop driver executes\n # this verbatim, then steps the check actor. Mechanics only: take the\n # first package file (by name) under the packages dir into NEXT.md, or\n # remove NEXT.md when the queue is empty. What NEXT.md's presence/absence\n # MEANS (another package to build vs. the queue closing out to review) is\n # decided by this state's own `on` rules at capture time — never here.\n set +e\n # Hoist the NEXT path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a line —\n # it would glue the next line's `else`/`fi` onto it and break the script.\n # Below this point everything is plain bash.\n nextFile=\"<%~ it.vars.nextFile %>\"\n next=$(ls <%~ it.vars.packagesDir %>/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n printf '%s' \"$next\" > \"$nextFile\"\n else\n rm -f \"$nextFile\"\n fi\n on:\n \"D .gtd/NEXT.md\": reviewing\n \"* .gtd/NEXT.md\": adv-building\n \"C\": reviewing\n\n adv-building:\n actor: agent\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/`; in\n particular do NOT delete the package file (the `spec-review` gate reads it\n after you) and never touch `<%= it.vars.nextFile %>` — the `picking` state\n owns it.\n\n The package to implement is: <%~ it.read(it.vars.nextFile) %>\n\n That file describes a set of INDEPENDENT tasks. Implement ALL of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement EXACTLY the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"* **\": adv-checking\n\n adv-checking:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A .gtd/FEEDBACK.md\": adv-fixing\n \"M .gtd/FEEDBACK.md\": adv-fixing\n \"D .gtd/FEEDBACK.md\": spec-review\n \"C\": spec-review\n\n adv-fixing:\n actor: agent\n file: <%= it.vars.feedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: fix\n retry:\n max: 3\n otherwise: adv-escalate\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.feedbackFile %>`, which this prompt tells you to address.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue.\n\n Leave everything uncommitted and finish your turn — do not commit.\n on:\n \"* **\": adv-checking\n\n adv-escalate:\n actor: human\n file: <%= it.vars.feedbackFile %>\n message: |\n The agent could not get the check to pass after repeated attempts.\n `<%= it.vars.feedbackFile %>` holds the last failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: adv-checking\n describe: >-\n Fix the failure yourself — edit the code and/or\n `.gtd/FEEDBACK.md` — to re-run the check (**adv-checking**).\n\n spec-review:\n actor: agent\n model: <%= it.vars.plannerModel %>\n memory: review\n # Bounded per cycle: once entered `max` times, a transition into it is\n # redirected to `closing` (force-close the package and move on — the\n # unresolved concern is deferred to the shared human review at the tail,\n # which sees the whole diff anyway). NOTE the cap is PROCESS-scoped, so it\n # pools across packages — the same documented limitation the shared\n # `fixing` cap has (see STATES.md / docs/configuration.md).\n retry:\n max: 3\n otherwise: closing\n prompt: |\n You are an autonomous coding agent reviewing a freshly-built work package\n against its own spec. `.gtd/` holds this workflow's own state — never\n create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.specFeedbackFile %>`, which this prompt tells you to write\n when — and only when — you find problems.\n\n The package spec is: <%~ it.read(it.vars.nextFile) %>\n\n Read that spec, then verify the package's implementation (the diff below)\n against it: are all its tasks done, acceptance criteria met, and the code\n sound and consistent with the codebase?\n\n - If it fully satisfies the spec, write NOTHING and finish your turn — a\n clean turn is your approval.\n - If it does not, write `<%= it.vars.specFeedbackFile %>` listing the\n concrete problems for a fix turn to address. Be specific.\n\n Do not fix anything yourself and do not delete the package file — a later\n step owns that. Finish your turn.\n <% if (it.processDiff.trim()) { %>\n\n ## Diff under review\n\n ```diff\n <%~ it.processDiff %>\n ```\n <% } %>\n on:\n \"A .gtd/SPEC_FEEDBACK.md\": spec-fix\n \"M .gtd/SPEC_FEEDBACK.md\": spec-fix\n \"D .gtd/SPEC_FEEDBACK.md\": closing\n \"C\": closing\n\n spec-fix:\n actor: agent\n file: <%= it.vars.specFeedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: fix\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.specFeedbackFile %>`, which this prompt tells you to address\n and then delete.\n\n Read `<%= it.vars.specFeedbackFile %>` (the reviewer's concerns about the\n current package) and the package spec (`<%= it.read(it.vars.nextFile) %>`),\n then fix the code to resolve every concern. Keep the change focused.\n\n Delete `<%= it.vars.specFeedbackFile %>` once every concern is addressed.\n Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": adv-checking\n\n closing:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn (the package closer) — remove the just-reviewed package\n # file (its path is in NEXT.md) plus any leftover spec feedback, so\n # `picking` selects the NEXT package (or falls through to review when the\n # queue is empty). Reached on approval (spec-review clean) and on the\n # spec-review retry cap (force-close, deferring the concern to the tail).\n set +e\n pkg=$(cat <%~ it.vars.nextFile %> 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f <%~ it.vars.specFeedbackFile %> <%~ it.vars.nextFile %>\n on:\n \"* **\": picking\n\n # ── Shared health segment (simple build, feedback loop, code-only edits) ──\n checking:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A .gtd/FEEDBACK.md\": fixing\n \"M .gtd/FEEDBACK.md\": fixing\n \"D .gtd/FEEDBACK.md\": reviewing\n \"C\": reviewing\n\n fixing:\n actor: agent\n file: <%= it.vars.feedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: fix\n retry:\n max: 3\n otherwise: escalate\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.feedbackFile %>`, which this prompt tells you to address.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue.\n\n Leave everything uncommitted and finish your turn — do not commit.\n on:\n \"* **\": checking\n\n escalate:\n actor: human\n file: <%= it.vars.feedbackFile %>\n message: |\n The agent could not get the check to pass after repeated attempts.\n `<%= it.vars.feedbackFile %>` holds the last failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: checking\n describe: >-\n Fix the failure yourself — edit the code and/or `.gtd/FEEDBACK.md` —\n to re-run the check (**checking**).\n\n # ── Green-baseline gate: `gtd review` entry ───────────────────────────────\n # `gtd review <commitish>` enters HERE (reviewEntry), not `reviewing`. It runs\n # the suite on HEAD first; green proceeds to `reviewing`, red halts. The\n # `Gtd-Review-Base:` trailer rides on this state's entry commit, so the review\n # window / incremental-diff machinery downstream is unchanged.\n review-start-check:\n actor: check\n reviewEntry: true\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite before reviewing; a red run records\n # .gtd/FEEDBACK.md (halt), a green run cleans it up (proceed to reviewing).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A .gtd/FEEDBACK.md\": review-start-blocked\n \"M .gtd/FEEDBACK.md\": review-start-blocked\n \"D .gtd/FEEDBACK.md\": reviewing\n \"C\": reviewing\n\n review-start-blocked:\n actor: human\n file: <%= it.vars.feedbackFile %>\n message: |\n The test baseline is red — gtd will not start a review on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: review-start-check\n describe: >-\n Fix the failing tests — edit the code and/or `.gtd/FEEDBACK.md` — to\n re-run the baseline check (**review-start-check**).\n\n # ── Green-baseline gate: `gtd fix` entry ──────────────────────────────────\n # `gtd fix` enters HERE (fixEntry): run the suite, then a red run drops\n # straight into the shared `fixing` loop (-> checking -> review + squash\n # tail); a green run is a no-op back to `idle` (nothing to fix).\n fix-check:\n actor: check\n fixEntry: true\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite to find what needs fixing; a red run\n # records .gtd/FEEDBACK.md (-> fixing), a green run cleans it up (-> idle).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A .gtd/FEEDBACK.md\": fixing\n \"M .gtd/FEEDBACK.md\": fixing\n \"D .gtd/FEEDBACK.md\": idle\n \"C\": idle\n\n # ── Shared tail: agent review -> human review -> squash finale ────────────\n reviewing:\n actor: agent\n file: <%= it.vars.reviewFile %>\n mode: review\n model: <%= it.vars.plannerModel %>\n memory: review\n # `gtd review <commitish>` (a plain non-gtd branch, e.g. a colleague's PR)\n # does NOT enter here directly — it enters `review-start-check` (reviewEntry)\n # first, which runs the green-baseline gate and, once green, transitions\n # here. The `Gtd-Review-Base:` trailer rides on that entry commit (the\n # process's oldest), so everything below still reuses the same\n # reviewing/await-review/feedback machinery over <commitish>..HEAD.\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewFile %>`, which this prompt tells you to write.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the diff below in\n this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.startCommit %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation of\n what changed and why, then one `- [ ] ./path/to/file.ts#42 — note` file\n pointer per hunk (checkboxes are for the human to tick, not you; every\n path is `./`-relative, with an optional `#line` and an optional\n ` — note`).\n\n The diff below is `it.reviewDiff` — on a feedback round it covers ONLY the\n changes made since the previous review, so you review just what changed,\n not items already signed off. Leave `<%= it.vars.reviewFile %>`\n uncommitted and finish your turn.\n <% if (it.reviewDiff.trim()) { %>\n\n ## Full diff under review\n\n ```diff\n <%~ it.reviewDiff %>\n ```\n <% } %>\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n file: <%= it.vars.reviewFile %>\n mode: review\n # While the cycle rests here, gtd opens a \"review checkout window\": raw HEAD\n # is rewound to the review base (the most-recent `review-deciding` commit —\n # so a feedback round shows only `last-review..HEAD` — or the process start\n # on the first review) with the working tree untouched, surfacing the diff\n # as ordinary uncommitted changes. It closes on the next invocation.\n #\n # Every human step here routes to `review-deciding`, which decides sign-off\n # vs. feedback from the step's CONTENT (a comment, not a tick). Ticking a box\n # only records \"I reviewed this hunk\". Two dead ends can't be told apart by a\n # file-pattern edge, so the sign-off gate (`enforceReviewSignoffGate` in\n # src/program.ts) catches them BEFORE the step commits and refuses: an\n # unfinished review (a box still `- [ ]` with no comment) and a deleted\n # REVIEW.md.\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the cycle — one\n `- [ ]` checkbox per reviewable item, grouped into chunks. Tick a box\n (`- [x]`) as you review each hunk; ticking only records that you've read\n it, it is not sign-off.\n\n When you've been through the whole diff, run `gtd step human`:\n\n - Tick EVERY box and leave no comment — no note in\n `<%= it.vars.reviewFile %>`, no code edit — to sign off and collapse the\n cycle into one commit (**squashing**).\n - Leave a comment to request changes — a note on a\n `<%= it.vars.reviewFile %>` line, or a direct code edit — to send it to a\n build + re-review round (**review-deciding** → **feedback-building**). A\n code edit counts as your own fix: the agent completes the follow-through\n it implies and never reverts your lines.\n\n Stepping with a box still unticked and no comment is refused (finish\n reviewing first); so is deleting `<%= it.vars.reviewFile %>`.\n on:\n \"* **\": review-deciding\n\n review-deciding:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n # Anchors the INCREMENTAL review base: its most-recent in-process commit is\n # the point the previous review round ended, so the next `reviewing` turn\n # (and the review checkout window) covers only the changes since then. On\n # the first review there is no such commit, so the base is the cycle start.\n reviewBase: true\n script: |\n #!/usr/bin/env bash\n # gtd check turn — decide the human's review step from its CONTENT. HEAD is\n # the human's `await-review -> review-deciding` commit; HEAD^ is the\n # `reviewing` commit that wrote the agent's original REVIEW.md (the sign-off\n # gate refuses an incomplete step, so review-deciding is entered exactly\n # once per round — nothing commits in between). It is FEEDBACK when the\n # human left a comment: a note in REVIEW.md (any edit beyond a \"[ ]\"->\"[x]\"\n # tick) OR a hand-edit to any non-.gtd/ file this round. Otherwise it is a\n # clean sign-off. This turn only CAPTURES the raw material into REVIEW_RAW.md\n # (the `feedback-collecting` agent then turns it into instructions); it does\n # NOT interpret it. Either way, remove REVIEW.md; what the resulting diff\n # MEANS is decided by this state's own `on` rules — the A/M REVIEW_RAW.md\n # rows are declared BEFORE the D REVIEW.md row so a feedback round (which\n # also deletes REVIEW.md) is captured as feedback, not mistaken for a\n # sign-off.\n set +e\n mkdir -p .gtd\n # Hoist the paths once, at the TOP: Eta's autoTrim eats the newline after\n # an interpolation tag, so no tag may be the last token on a line — it\n # would glue the next line onto it and break the script. Below this point\n # everything is plain bash.\n reviewFile=\"<%~ it.vars.reviewFile %>\"\n reviewRawFile=\"<%~ it.vars.reviewRawFile %>\"\n # Which non-.gtd/ files did the human hand-edit this round?\n codeFiles=$(git diff-tree --no-commit-id --name-only -r HEAD | grep -v '^\\.gtd/')\n # A note is any REVIEW.md change beyond a checkbox flip: normalize every\n # \"[ ]\"/\"[x]\" to a placeholder, then compare the agent's original (HEAD^)\n # against the human's version (HEAD).\n notes=\"\"\n before=$(git show \"HEAD^:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n after=$(git show \"HEAD:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n if [ \"$before\" != \"$after\" ]; then notes=1; fi\n if [ -n \"$codeFiles\" ] || [ -n \"$notes\" ]; then\n {\n echo \"Raw review material captured for classification. The human reviewed the diff and left the feedback below; a downstream agent turns it into an instruction list. This file is machine-captured input, not instructions.\"\n if [ -n \"$notes\" ]; then\n echo\n echo \"## Notes the human added to REVIEW.md this round\"\n echo\n echo '```diff'\n git show HEAD --format= -- \"$reviewFile\"\n echo '```'\n fi\n if [ -n \"$codeFiles\" ]; then\n echo\n echo \"## Files the human hand-edited this round (already committed)\"\n echo\n echo '```diff'\n git show HEAD --format= -- $codeFiles\n echo '```'\n fi\n } > \"$reviewRawFile\"\n fi\n rm -f \"$reviewFile\"\n on:\n \"A .gtd/REVIEW_RAW.md\": feedback-collecting\n \"M .gtd/REVIEW_RAW.md\": feedback-collecting\n \"D .gtd/REVIEW.md\": squashing\n\n feedback-collecting:\n actor: agent\n file: <%= it.vars.reviewFeedbackFile %>\n model: <%= it.vars.plannerModel %>\n memory: review\n prompt: |\n You are an autonomous coding agent turning raw review feedback into an\n explicit instruction list for a builder. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewRawFile %>` (delete it when done) and\n `<%= it.vars.reviewFeedbackFile %>` (write it). Do NOT change any other\n file — you classify, you do not build.\n\n The raw review material is: <%~ it.read(it.vars.reviewRawFile) %>\n\n Classify every piece of it into `<%= it.vars.reviewFeedbackFile %>` as a\n flat, numbered instruction list. Each item is `N. ./path/to/file.ts#line —\n <imperative>`. Read the two RAW sections and apply these rules exactly:\n\n 1. NOTES the human added to REVIEW.md — each note is a MANDATORY change,\n regardless of whether its checkbox is ticked (ticking means only \"I read\n this hunk\", never sign-off). Anchor each to the `./path#line` its review\n item names (or the chunk it sits under). Emit it as a concrete\n imperative.\n 2. Every COMMENT the human ADDED this round (a `+` line that is a code\n comment, in the hand-edited files) is a MANDATORY instruction the human\n is giving the builder — even a plain-prose one, even if its box is\n ticked. Emit an imperative to satisfy it and note that the comment line\n itself must be REMOVED once addressed. If a comment turns out to describe\n behaviour the code already has, the instruction is still \"confirm the\n code matches and remove the comment line\".\n 3. Every NON-comment code line the human hand-edited is an already-committed\n INTENT, NOT a do-then-delete item. Emit an instruction that names those\n files and says: their lines are final — complete ONLY the follow-through\n they imply (callers, tests, types, docs) and NEVER revert or rewrite\n them. Do not turn their own edit into a task.\n\n Split each hand-edited hunk per line: comment `+` lines follow rule 2, the\n surrounding real code follows rule 3.\n\n If — and only if — nothing in the raw material is actionable (e.g. the human\n left only an approving remark), write `<%= it.vars.reviewFeedbackFile %>`\n containing exactly one line: `NOTHING ACTIONABLE — <one-sentence reason>`.\n Never invent work to satisfy a non-actionable remark.\n\n Delete `<%= it.vars.reviewRawFile %>`, leave everything else uncommitted,\n and finish your turn.\n on:\n \"A .gtd/REVIEW_FEEDBACK.md\": feedback-building\n \"M .gtd/REVIEW_FEEDBACK.md\": feedback-building\n\n feedback-building:\n actor: agent\n file: <%= it.vars.reviewFeedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: build\n # Refuse a work-free turn: a step whose only pending change is deleting the\n # instructions file (the original \"captured then discarded\" bug). The edge\n # gate (`enforceFeedbackProgressGate` in src/program.ts) reads this flag and\n # exempts a `NOTHING ACTIONABLE` sentinel (a non-actionable review round,\n # which legitimately makes no code change). The pure engine carries it inert.\n requireProgress: true\n prompt: |\n You are an autonomous coding agent addressing review feedback. `.gtd/`\n holds this workflow's own state — never create, edit, or delete anything\n under `.gtd/` except `<%= it.vars.reviewFeedbackFile %>`, which this\n prompt tells you to address and then delete.\n\n The instructions to implement are: <%~ it.read(it.vars.reviewFeedbackFile) %>\n\n This is a flat instruction list, not a diff — execute every item exactly as\n written, no Q&A, no re-planning, using TDD discipline:\n\n - An item that says the human's own lines are final: build ON TOP of them to\n complete only the follow-through it names (callers, tests, types, docs).\n NEVER revert or rewrite the lines they already committed.\n - An item derived from a human comment: satisfy it AND delete that comment\n line from the code (the human's review comments are transient — none of\n them survive this round).\n\n If the file's sole content is a `NOTHING ACTIONABLE` line, make no code\n change; just delete the instructions file.\n\n Delete `<%= it.vars.reviewFeedbackFile %>` once every item is addressed.\n Leave everything else uncommitted and finish your turn. Deleting the\n instructions file WITHOUT making the changes it asks for is refused — the\n feedback must actually be addressed.\n on:\n \"* **\": checking\n\n squashing:\n actor: agent\n file: <%= it.vars.commitMsgFile %>\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. The cycle is approved and done.\n `.gtd/` holds this workflow's own state — never create, edit, or delete\n anything under `.gtd/` except `<%= it.vars.commitMsgFile %>`, which this\n prompt tells you to write.\n\n Leave `<%= it.vars.commitMsgFile %>` uncommitted and finish your turn —\n entering this file squashes everything this process retains into one\n commit using its content as the message. The diff below is exactly what\n that commit will contain: `it.retainedDiff`, the changes THIS process\n made (its trace/retry boundary onward). For a normal build cycle that is\n the whole feature; for a `gtd review` process it is only the fixes made\n during the review — NOT the changeset that was under review.\n <% if (it.retainedDiff.trim()) { %>\n\n Write `<%= it.vars.commitMsgFile %>` with ONE conventional-commits message\n for the changes below (subject line `type(scope): subject`, imperative\n mood, <= 72 characters; a body explaining the why, trade-offs, and key\n decisions). Plain text, no markdown wrapper.\n\n ## Retained diff\n\n ```diff\n <%~ it.retainedDiff %>\n ```\n <% } else { %>\n\n This process retained NO changes (a clean review sign-off with no fixes).\n Write `<%= it.vars.commitMsgFile %>` with exactly this one line and\n nothing else:\n\n chore: human review\n <% } %>\n on:\n \"A .gtd/COMMIT_MSG.md\": done\n \"M .gtd/COMMIT_MSG.md\": done\n\n done:\n commit: |\n <%~ it.read(it.vars.commitMsgFile) %>\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n";
190528
+ var unified_default = "# The unified gtd workflow template — gtd's BUILT-IN DEFAULT workflow. When no\n# `workflow:` key is configured anywhere in the cwd→home config chain,\n# src/Config.ts runs this template directly, so a state command works with no\n# configuration at all. `gtd init` seeds only the settings a project tunes\n# (vars.testCommand + a modes: formatting suggestion), never the workflow\n# itself. See STATES.md §10 for the full walkthrough.\n#\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/templates.ts does the compiling). Every content string below\n# is INLINE (no `./`-relative file references): the bundled template ships\n# inside a single-file build (`dist/gtd.bundle.mjs`), so its content must\n# already be resolved at BUILD time. `renderInitConfig` (src/workflows/\n# templates.ts) can MATERIALIZE this inline source into a `.gtdrc.json`\n# `workflow:` value — the way to eject/customize the machine, and the hermetic\n# test fixture — but `gtd init` itself no longer writes the workflow.\n#\n# ── Authored with SUB-MACHINES (Option C) ──────────────────────────────────\n#\n# This template is written with `submachines:`/`use:` (see src/Submachines.ts\n# and STATES.md). Expansion is a compile-time raw → raw pre-pass with NO runtime\n# footprint: the pure engine only ever sees the flat concrete states, and each\n# invocation's `as:` rename map reproduces the exact state names the flat\n# template used — so the compiled `WorkflowDefinition` is BYTE-IDENTICAL to the\n# frozen `unified.flat.yaml` (pinned by src/workflows/golden.test.ts). The\n# sub-machines split into DEDUP (a gate/loop authored once, invoked several\n# times) and ENCAPSULATION (a single-instance cluster grouped for reading):\n#\n# * makeGreen (dedup ×2) — the check/fix/escalate health triple shared by\n# the simple build tail and the advanced package\n# loop (identical script/prompt/message).\n# * assertGreen (dedup ×3) — the green-baseline gate (check + human blocked)\n# guarding the simple, advanced and review entries.\n# * qaLoop (dedup ×2) — the checkbox Q&A author/answer loop shared by the\n# product (adv-grilling) and technical (architecting)\n# phases of the advanced flow.\n# * planLoop (encap) — the simple flow's plan-iteration loop.\n# * humanReview (encap) — the shared review tail (reviewing → await-review →\n# review-deciding → feedback collect/build).\n# * specGate (encap) — the advanced flow's per-package agentic review.\n# * packageLoop (encap) — the advanced flow's per-package build queue.\n#\n# ── Four entry points behind a green-baseline gate, one shared tail ─────────\n#\n# EVERY entry first runs the test suite and only proceeds when it is GREEN — you\n# never start new work (or a review) on top of a red baseline. A red run halts\n# at a human `*-start-blocked` gate (fix the failures and re-run, exactly like\n# the `escalate` states), or is what `gtd fix` exists to repair.\n#\n# The initial `idle` state FORKS on which steering file the human creates,\n# routing into that flow's OWN start gate:\n#\n# * .gtd/REQUIREMENTS.md -> `adv-start-check` -> the ADVANCED flow (product +\n# technical Q&A, package decomposition, per-package parallel build +\n# agentic spec-review).\n# * anything else (e.g. .gtd/TODO.md) -> `start-check` -> the SIMPLE flow (one\n# grilling Q&A loop, a monolithic build, no decomposition, no agentic\n# review).\n#\n# A third entry, `gtd review <commitish>`, jumps to `review-start-check`\n# (reviewEntry) and, once green, to `reviewing` to review a foreign branch\n# through the same shared tail. A fourth, `gtd fix`, jumps to `fix-check`\n# (fixEntry): a red suite drops straight into the shared `fixing` loop and out\n# through the review + squash tail; a green suite is a no-op back to `idle`.\n#\n# All three converge at `reviewing` -> `await-review`. Ticking a REVIEW.md box\n# means only \"I reviewed this hunk\"; a COMMENT (not an unticked box) is what asks\n# for changes. Every human step at `await-review` goes to `review-deciding`,\n# which inspects the step and routes:\n# * no comment (every box ticked, no REVIEW.md note, no code edit) -> the diff\n# is a bare REVIEW.md deletion -> squashing -> done (the ONLY path to the\n# finale — the whole cycle collapses into one commit).\n# * any comment — a note in REVIEW.md OR a hand-edit to code — ->\n# review-deciding writes REVIEW_FEEDBACK.md -> feedback-building implements\n# it (completing any code the human hand-edited, never reverting their\n# lines) -> checking -> reviewing regenerates an INCREMENTAL REVIEW.md\n# (review-deciding carries `reviewBase: true`, so each feedback round's\n# review + checkout window covers only `last-review..HEAD`, i.e. the agent's\n# follow-through, not the human's own edit) -> await-review, looping until\n# sign-off.\n# Two dead ends never commit: the sign-off gate (src/program.ts) REFUSES a step\n# that leaves a box unticked with no comment (finish reviewing first) and a\n# deleted REVIEW.md (restore it and tick the boxes).\n#\n# The shared health segment (`checking`/`fixing`/`escalate`) serves the simple\n# build and the feedback loop — its green edge targets\n# `reviewing`. The advanced per-package loop keeps its OWN health triple\n# (`adv-checking`/`adv-fixing`/`adv-escalate`) because its green edge targets\n# the per-package `spec-review` gate instead.\n#\n# KNOWN LIMITATION: `on` pattern keys are NOT Eta templates — they keep the\n# LITERAL `.gtd/...` paths matching these vars' DEFAULT values. Repointing a\n# file var changes what templates read/write WITHOUT changing what the `on`\n# patterns match, desyncing the machine (see docs/configuration.md).\n\nvars:\n testCommand: npm test\n todoFile: .gtd/TODO.md\n requirementsFile: .gtd/REQUIREMENTS.md\n architectureFile: .gtd/ARCHITECTURE.md\n packagesDir: .gtd/packages\n nextFile: .gtd/NEXT.md\n reviewFile: .gtd/REVIEW.md\n reviewRawFile: .gtd/REVIEW_RAW.md\n reviewFeedbackFile: .gtd/REVIEW_FEEDBACK.md\n feedbackFile: .gtd/FEEDBACK.md\n specFeedbackFile: .gtd/SPEC_FEEDBACK.md\n commitMsgFile: .gtd/COMMIT_MSG.md\n # The two opaque model tiers every agent state draws from — repoint them here\n # (or via a top-level `.gtdrc` `vars:` key / a `GTD_VAR_plannerModel` env var)\n # to change every state on that tier at once. `plannerModel` (default \"smart\")\n # is the heavier tier for the one-shot planning/architecting/review turns;\n # `coderModel` (default \"base\") is the tier for the build/fix turns.\n plannerModel: smart\n coderModel: base\n\nsubmachines:\n # ── DEDUP: the check/fix/escalate health triple ───────────────────────────\n # Runs the suite; a red run records .gtd/FEEDBACK.md, a green run cleans it up.\n # The `on` rules turn that into \"fix\" vs \"$onGreen\". The fix turn retries up to\n # 3× before escalating to a human. Invoked for the shared build tail\n # (checking/fixing/escalate -> reviewing) and the advanced package loop\n # (adv-checking/adv-fixing/adv-escalate -> spec-review).\n makeGreen:\n params: [onGreen, escalateDescribe]\n states:\n check:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A .gtd/FEEDBACK.md\": fix\n \"M .gtd/FEEDBACK.md\": fix\n \"D .gtd/FEEDBACK.md\": $onGreen\n \"C\": $onGreen\n fix:\n actor: agent\n file: <%= it.vars.feedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: fix\n retry:\n max: 3\n otherwise: escalate\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.feedbackFile %>`, which this prompt tells you to address.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue.\n\n Leave everything uncommitted and finish your turn — do not commit.\n on:\n \"* **\": check\n escalate:\n actor: human\n file: <%= it.vars.feedbackFile %>\n message: |\n The agent could not get the check to pass after repeated attempts.\n `<%= it.vars.feedbackFile %>` holds the last failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n describe: $escalateDescribe\n\n # ── DEDUP: the green-baseline gate ─────────────────────────────────────────\n # Runs the suite before any work starts (its script is the health check's test\n # wrapper — red -> .gtd/FEEDBACK.md, green -> remove it); the `on` rules turn\n # that into \"halt at blocked\" vs \"proceed to $onGreen\". The script and blocked\n # message/describe differ per entry, so they ride in as `with:` params.\n assertGreen:\n params: [onGreen, checkScript, blockedMessage, blockedDescribe]\n states:\n check:\n actor: check\n script: $checkScript\n on:\n \"A .gtd/FEEDBACK.md\": blocked\n \"M .gtd/FEEDBACK.md\": blocked\n \"D .gtd/FEEDBACK.md\": $onGreen\n \"C\": $onGreen\n blocked:\n actor: human\n file: <%= it.vars.feedbackFile %>\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n describe: $blockedDescribe\n\n # ── DEDUP: the checkbox Q&A author/answer loop ─────────────────────────────\n # The agent develops a plan file, surfacing `## Open Questions` as checkbox\n # lists; the human answers (answerGate refuses an unanswered step) and either\n # advances or sends it back. Invoked for the product (adv-grilling) and\n # technical (architecting) phases of the advanced flow.\n qaLoop:\n params: [authorFile, authorPrompt, answerMessage, onAdvance, advanceDescribe, editDescribe]\n states:\n author:\n actor: agent\n file: $authorFile\n mode: qa\n model: <%= it.vars.plannerModel %>\n memory: plan\n prompt: $authorPrompt\n on:\n \"* **\": answer\n answer:\n actor: human\n file: $authorFile\n mode: qa\n answerGate: true\n message: $answerMessage\n on:\n \"C\":\n to: $onAdvance\n describe: $advanceDescribe\n \"* **\":\n to: author\n describe: $editDescribe\n\n # ── ENCAPSULATION: the simple flow's plan-iteration loop ───────────────────\n planLoop:\n params: [onAccept]\n states:\n planning:\n actor: agent\n file: <%= it.vars.todoFile %>\n model: <%= it.vars.plannerModel %>\n memory: plan\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.todoFile %>`, which this prompt tells you to develop.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build — or, on a\n return lap, the plan you already wrote plus the human's edits and comments\n on it. Read it, explore the codebase, and develop it into a concrete\n implementation plan: the files to change, the approach, and the steps to\n take.\n\n Do NOT ask questions and do NOT add an \"Open Questions\" section — decide\n every open point yourself and write the plan as a set of concrete\n decisions. Where a decision is genuinely a judgement call, state the\n choice you made and, in one line, why. The human iterates on the plan by\n editing it directly, not by answering questions.\n\n On a return lap, `<%= it.vars.todoFile %>` carries the human's edits and\n inline comments on your previous plan. Fold every edit and comment into a\n revised, self-contained plan and remove the addressed comments — the plan\n is always the current, clean proposal, never a diff.\n\n Leave `<%= it.vars.todoFile %>` uncommitted and finish your turn — do\n not commit, and do not run `gtd step agent` yourself; the harness does\n that.\n on:\n \"* **\": plan-review\n\n plan-review:\n actor: human\n file: <%= it.vars.todoFile %>\n message: |\n `<%= it.vars.todoFile %>` holds the proposed implementation plan. Read it,\n then either accept it as-is or edit it — rewrite a step, add a\n constraint, or drop an inline comment — to send it back for another round.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: $onAccept\n describe: >-\n Change nothing to accept the plan as-is and move on to implementation\n (**building**).\n \"* **\":\n to: planning\n describe: >-\n Edit the plan — rewrite a step, add a constraint, or leave an inline\n comment — to send it back for another round (**planning**).\n\n # ── ENCAPSULATION: the shared review tail ──────────────────────────────────\n humanReview:\n params: [onSignoff]\n states:\n reviewing:\n actor: agent\n file: <%= it.vars.reviewFile %>\n mode: review\n model: <%= it.vars.plannerModel %>\n memory: review\n # `gtd review <commitish>` (a plain non-gtd branch, e.g. a colleague's PR)\n # does NOT enter here directly — it enters `review-start-check` (reviewEntry)\n # first, which runs the green-baseline gate and, once green, transitions\n # here. The `Gtd-Review-Base:` trailer rides on that entry commit (the\n # process's oldest), so everything below still reuses the same\n # reviewing/await-review/feedback machinery over <commitish>..HEAD.\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewFile %>`, which this prompt tells you to write.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the diff below in\n this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.startCommit %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation of\n what changed and why, then one `- [ ] ./path/to/file.ts#42 — note` file\n pointer per hunk (checkboxes are for the human to tick, not you; every\n path is `./`-relative, with an optional `#line` and an optional\n ` — note`).\n\n The diff below is `it.reviewDiff` — on a feedback round it covers ONLY the\n changes made since the previous review, so you review just what changed,\n not items already signed off. Leave `<%= it.vars.reviewFile %>`\n uncommitted and finish your turn.\n <% if (it.reviewDiff.trim()) { %>\n\n ## Full diff under review\n\n ```diff\n <%~ it.reviewDiff %>\n ```\n <% } %>\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n file: <%= it.vars.reviewFile %>\n mode: review\n # While the cycle rests here, gtd opens a \"review checkout window\": raw HEAD\n # is rewound to the review base (the most-recent `review-deciding` commit —\n # so a feedback round shows only `last-review..HEAD` — or the process start\n # on the first review) with the working tree untouched, surfacing the diff\n # as ordinary uncommitted changes. It closes on the next invocation.\n #\n # Every human step here routes to `review-deciding`, which decides sign-off\n # vs. feedback from the step's CONTENT (a comment, not a tick). Ticking a box\n # only records \"I reviewed this hunk\". Two dead ends can't be told apart by a\n # file-pattern edge, so the sign-off gate (`enforceReviewSignoffGate` in\n # src/program.ts) catches them BEFORE the step commits and refuses: an\n # unfinished review (a box still `- [ ]` with no comment) and a deleted\n # REVIEW.md.\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the cycle — one\n `- [ ]` checkbox per reviewable item, grouped into chunks. Tick a box\n (`- [x]`) as you review each hunk; ticking only records that you've read\n it, it is not sign-off.\n\n When you've been through the whole diff, run `gtd step human`:\n\n - Tick EVERY box and leave no comment — no note in\n `<%= it.vars.reviewFile %>`, no code edit — to sign off and collapse the\n cycle into one commit (**squashing**).\n - Leave a comment to request changes — a note on a\n `<%= it.vars.reviewFile %>` line, or a direct code edit — to send it to a\n build + re-review round (**review-deciding** → **feedback-building**). A\n code edit counts as your own fix: the agent completes the follow-through\n it implies and never reverts your lines.\n\n Stepping with a box still unticked and no comment is refused (finish\n reviewing first); so is deleting `<%= it.vars.reviewFile %>`.\n on:\n \"* **\": review-deciding\n\n review-deciding:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n # Anchors the INCREMENTAL review base: its most-recent in-process commit is\n # the point the previous review round ended, so the next `reviewing` turn\n # (and the review checkout window) covers only the changes since then. On\n # the first review there is no such commit, so the base is the cycle start.\n reviewBase: true\n script: |\n #!/usr/bin/env bash\n # gtd check turn — decide the human's review step from its CONTENT. HEAD is\n # the human's `await-review -> review-deciding` commit; HEAD^ is the\n # `reviewing` commit that wrote the agent's original REVIEW.md (the sign-off\n # gate refuses an incomplete step, so review-deciding is entered exactly\n # once per round — nothing commits in between). It is FEEDBACK when the\n # human left a comment: a note in REVIEW.md (any edit beyond a \"[ ]\"->\"[x]\"\n # tick) OR a hand-edit to any non-.gtd/ file this round. Otherwise it is a\n # clean sign-off. This turn only CAPTURES the raw material into REVIEW_RAW.md\n # (the `feedback-collecting` agent then turns it into instructions); it does\n # NOT interpret it. Either way, remove REVIEW.md; what the resulting diff\n # MEANS is decided by this state's own `on` rules — the A/M REVIEW_RAW.md\n # rows are declared BEFORE the D REVIEW.md row so a feedback round (which\n # also deletes REVIEW.md) is captured as feedback, not mistaken for a\n # sign-off.\n set +e\n mkdir -p .gtd\n # Hoist the paths once, at the TOP: Eta's autoTrim eats the newline after\n # an interpolation tag, so no tag may be the last token on a line — it\n # would glue the next line onto it and break the script. Below this point\n # everything is plain bash.\n reviewFile=\"<%~ it.vars.reviewFile %>\"\n reviewRawFile=\"<%~ it.vars.reviewRawFile %>\"\n # Which non-.gtd/ files did the human hand-edit this round?\n codeFiles=$(git diff-tree --no-commit-id --name-only -r HEAD | grep -v '^\\.gtd/')\n # A note is any REVIEW.md change beyond a checkbox flip: normalize every\n # \"[ ]\"/\"[x]\" to a placeholder, then compare the agent's original (HEAD^)\n # against the human's version (HEAD).\n notes=\"\"\n before=$(git show \"HEAD^:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n after=$(git show \"HEAD:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n if [ \"$before\" != \"$after\" ]; then notes=1; fi\n if [ -n \"$codeFiles\" ] || [ -n \"$notes\" ]; then\n {\n echo \"Raw review material captured for classification. The human reviewed the diff and left the feedback below; a downstream agent turns it into an instruction list. This file is machine-captured input, not instructions.\"\n if [ -n \"$notes\" ]; then\n echo\n echo \"## Notes the human added to REVIEW.md this round\"\n echo\n echo '```diff'\n git show HEAD --format= -- \"$reviewFile\"\n echo '```'\n fi\n if [ -n \"$codeFiles\" ]; then\n echo\n echo \"## Files the human hand-edited this round (already committed)\"\n echo\n echo '```diff'\n git show HEAD --format= -- $codeFiles\n echo '```'\n fi\n } > \"$reviewRawFile\"\n fi\n rm -f \"$reviewFile\"\n on:\n \"A .gtd/REVIEW_RAW.md\": feedback-collecting\n \"M .gtd/REVIEW_RAW.md\": feedback-collecting\n \"D .gtd/REVIEW.md\": $onSignoff\n\n feedback-collecting:\n actor: agent\n file: <%= it.vars.reviewFeedbackFile %>\n model: <%= it.vars.plannerModel %>\n memory: review\n prompt: |\n You are an autonomous coding agent turning raw review feedback into an\n explicit instruction list for a builder. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewRawFile %>` (delete it when done) and\n `<%= it.vars.reviewFeedbackFile %>` (write it). Do NOT change any other\n file — you classify, you do not build.\n\n The raw review material is: <%~ it.read(it.vars.reviewRawFile) %>\n\n Classify every piece of it into `<%= it.vars.reviewFeedbackFile %>` as a\n flat, numbered instruction list. Each item is `N. ./path/to/file.ts#line —\n <imperative>`. Read the two RAW sections and apply these rules exactly:\n\n 1. NOTES the human added to REVIEW.md — each note is a MANDATORY change,\n regardless of whether its checkbox is ticked (ticking means only \"I read\n this hunk\", never sign-off). Anchor each to the `./path#line` its review\n item names (or the chunk it sits under). Emit it as a concrete\n imperative.\n 2. Every COMMENT the human ADDED this round (a `+` line that is a code\n comment, in the hand-edited files) is a MANDATORY instruction the human\n is giving the builder — even a plain-prose one, even if its box is\n ticked. Emit an imperative to satisfy it and note that the comment line\n itself must be REMOVED once addressed. If a comment turns out to describe\n behaviour the code already has, the instruction is still \"confirm the\n code matches and remove the comment line\".\n 3. Every NON-comment code line the human hand-edited is an already-committed\n INTENT, NOT a do-then-delete item. Emit an instruction that names those\n files and says: their lines are final — complete ONLY the follow-through\n they imply (callers, tests, types, docs) and NEVER revert or rewrite\n them. Do not turn their own edit into a task.\n\n Split each hand-edited hunk per line: comment `+` lines follow rule 2, the\n surrounding real code follows rule 3.\n\n If — and only if — nothing in the raw material is actionable (e.g. the human\n left only an approving remark), write `<%= it.vars.reviewFeedbackFile %>`\n containing exactly one line: `NOTHING ACTIONABLE — <one-sentence reason>`.\n Never invent work to satisfy a non-actionable remark.\n\n Delete `<%= it.vars.reviewRawFile %>`, leave everything else uncommitted,\n and finish your turn.\n on:\n \"A .gtd/REVIEW_FEEDBACK.md\": feedback-building\n \"M .gtd/REVIEW_FEEDBACK.md\": feedback-building\n\n feedback-building:\n actor: agent\n file: <%= it.vars.reviewFeedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: build\n # Refuse a work-free turn: a step whose only pending change is deleting the\n # instructions file (the original \"captured then discarded\" bug). The edge\n # gate (`enforceFeedbackProgressGate` in src/program.ts) reads this flag and\n # exempts a `NOTHING ACTIONABLE` sentinel (a non-actionable review round,\n # which legitimately makes no code change). The pure engine carries it inert.\n requireProgress: true\n prompt: |\n You are an autonomous coding agent addressing review feedback. `.gtd/`\n holds this workflow's own state — never create, edit, or delete anything\n under `.gtd/` except `<%= it.vars.reviewFeedbackFile %>`, which this\n prompt tells you to address and then delete.\n\n The instructions to implement are: <%~ it.read(it.vars.reviewFeedbackFile) %>\n\n This is a flat instruction list, not a diff — execute every item exactly as\n written, no Q&A, no re-planning, using TDD discipline:\n\n - An item that says the human's own lines are final: build ON TOP of them to\n complete only the follow-through it names (callers, tests, types, docs).\n NEVER revert or rewrite the lines they already committed.\n - An item derived from a human comment: satisfy it AND delete that comment\n line from the code (the human's review comments are transient — none of\n them survive this round).\n\n If the file's sole content is a `NOTHING ACTIONABLE` line, make no code\n change; just delete the instructions file.\n\n Delete `<%= it.vars.reviewFeedbackFile %>` once every item is addressed.\n Leave everything else uncommitted and finish your turn. Deleting the\n instructions file WITHOUT making the changes it asks for is refused — the\n feedback must actually be addressed.\n on:\n \"* **\": checking\n\n # ── ENCAPSULATION: the advanced flow's per-package agentic review ──────────\n specGate:\n params: [onApproved]\n states:\n spec-review:\n actor: agent\n model: <%= it.vars.plannerModel %>\n memory: review\n # Bounded per cycle: once entered `max` times, a transition into it is\n # redirected to `closing` (force-close the package and move on — the\n # unresolved concern is deferred to the shared human review at the tail,\n # which sees the whole diff anyway). NOTE the cap is PROCESS-scoped, so it\n # pools across packages — the same documented limitation the shared\n # `fixing` cap has (see STATES.md / docs/configuration.md).\n retry:\n max: 3\n otherwise: $onApproved\n prompt: |\n You are an autonomous coding agent reviewing a freshly-built work package\n against its own spec. `.gtd/` holds this workflow's own state — never\n create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.specFeedbackFile %>`, which this prompt tells you to write\n when — and only when — you find problems.\n\n The package spec is: <%~ it.read(it.vars.nextFile) %>\n\n Read that spec, then verify the package's implementation (the diff below)\n against it: are all its tasks done, acceptance criteria met, and the code\n sound and consistent with the codebase?\n\n - If it fully satisfies the spec, write NOTHING and finish your turn — a\n clean turn is your approval.\n - If it does not, write `<%= it.vars.specFeedbackFile %>` listing the\n concrete problems for a fix turn to address. Be specific.\n\n Do not fix anything yourself and do not delete the package file — a later\n step owns that. Finish your turn.\n <% if (it.processDiff.trim()) { %>\n\n ## Diff under review\n\n ```diff\n <%~ it.processDiff %>\n ```\n <% } %>\n on:\n \"A .gtd/SPEC_FEEDBACK.md\": spec-fix\n \"M .gtd/SPEC_FEEDBACK.md\": spec-fix\n \"D .gtd/SPEC_FEEDBACK.md\": $onApproved\n \"C\": $onApproved\n\n spec-fix:\n actor: agent\n file: <%= it.vars.specFeedbackFile %>\n model: <%= it.vars.coderModel %>\n memory: fix\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.specFeedbackFile %>`, which this prompt tells you to address\n and then delete.\n\n Read `<%= it.vars.specFeedbackFile %>` (the reviewer's concerns about the\n current package) and the package spec (`<%= it.read(it.vars.nextFile) %>`),\n then fix the code to resolve every concern. Keep the change focused.\n\n Delete `<%= it.vars.specFeedbackFile %>` once every concern is addressed.\n Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": adv-checking\n\n # ── ENCAPSULATION: the advanced flow's per-package build queue ─────────────\n packageLoop:\n params: [onDrained]\n states:\n picking:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn (the package queue arbiter) — the loop driver executes\n # this verbatim, then steps the check actor. Mechanics only: take the\n # first package file (by name) under the packages dir into NEXT.md, or\n # remove NEXT.md when the queue is empty. What NEXT.md's presence/absence\n # MEANS (another package to build vs. the queue closing out to review) is\n # decided by this state's own `on` rules at capture time — never here.\n set +e\n # Hoist the NEXT path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a line —\n # it would glue the next line's `else`/`fi` onto it and break the script.\n # Below this point everything is plain bash.\n nextFile=\"<%~ it.vars.nextFile %>\"\n next=$(ls <%~ it.vars.packagesDir %>/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n printf '%s' \"$next\" > \"$nextFile\"\n else\n rm -f \"$nextFile\"\n fi\n on:\n \"D .gtd/NEXT.md\": $onDrained\n \"* .gtd/NEXT.md\": adv-building\n \"C\": $onDrained\n\n adv-building:\n actor: agent\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/`; in\n particular do NOT delete the package file (the `spec-review` gate reads it\n after you) and never touch `<%= it.vars.nextFile %>` — the `picking` state\n owns it.\n\n The package to implement is: <%~ it.read(it.vars.nextFile) %>\n\n That file describes a set of INDEPENDENT tasks. Implement ALL of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement EXACTLY the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"* **\": adv-checking\n\n closing:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn (the package closer) — remove the just-reviewed package\n # file (its path is in NEXT.md) plus any leftover spec feedback, so\n # `picking` selects the NEXT package (or falls through to review when the\n # queue is empty). Reached on approval (spec-review clean) and on the\n # spec-review retry cap (force-close, deferring the concern to the tail).\n set +e\n pkg=$(cat <%~ it.vars.nextFile %> 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f <%~ it.vars.specFeedbackFile %> <%~ it.vars.nextFile %>\n on:\n \"* **\": picking\n\nuse:\n # Shared health triple: checking/fixing/escalate -> reviewing.\n - submachine: makeGreen\n name: build\n as: { check: checking, fix: fixing, escalate: escalate }\n with:\n onGreen: reviewing\n escalateDescribe: >-\n Fix the failure yourself — edit the code and/or `.gtd/FEEDBACK.md` —\n to re-run the check (**checking**).\n # Advanced package health triple: adv-checking/adv-fixing/adv-escalate -> spec-review.\n - submachine: makeGreen\n name: pkg\n as: { check: adv-checking, fix: adv-fixing, escalate: adv-escalate }\n with:\n onGreen: spec-review\n escalateDescribe: >-\n Fix the failure yourself — edit the code and/or\n `.gtd/FEEDBACK.md` — to re-run the check (**adv-checking**).\n\n # Green-baseline gate: simple flow entry.\n - submachine: assertGreen\n name: start\n as: { check: start-check, blocked: start-blocked }\n with:\n onGreen: planning\n checkScript: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite before starting new work; a red run\n # records .gtd/FEEDBACK.md (halt), a green run cleans it up (proceed).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n Fix the failing tests — edit the code and/or `.gtd/FEEDBACK.md` — to\n re-run the baseline check (**start-check**). To repair the baseline as\n its own separate reviewed commit instead, abandon this start and run\n `gtd fix` from a clean `idle`.\n\n # Green-baseline gate: advanced flow entry.\n - submachine: assertGreen\n name: adv-start\n as: { check: adv-start-check, blocked: adv-start-blocked }\n with:\n onGreen: adv-grilling\n checkScript: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite before starting the advanced flow; a red\n # run records .gtd/FEEDBACK.md (halt), a green run cleans it up (proceed).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n Fix the failing tests — edit the code and/or `.gtd/FEEDBACK.md` — to\n re-run the baseline check (**adv-start-check**). To repair the baseline\n as its own separate reviewed commit instead, abandon this start and run\n `gtd fix` from a clean `idle`.\n\n # Green-baseline gate: `gtd review` entry (reviewEntry -> reviewing).\n - submachine: assertGreen\n name: review-start\n as: { check: review-start-check, blocked: review-start-blocked }\n set:\n check: { reviewEntry: true }\n with:\n onGreen: reviewing\n checkScript: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite before reviewing; a red run records\n # .gtd/FEEDBACK.md (halt), a green run cleans it up (proceed to reviewing).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n blockedMessage: |\n The test baseline is red — gtd will not start a review on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n Fix the failing tests — edit the code and/or `.gtd/FEEDBACK.md` — to\n re-run the baseline check (**review-start-check**).\n\n # Advanced flow: product Q&A loop (adv-grilling/adv-grilling-answer -> architecting).\n - submachine: qaLoop\n name: grill\n as: { author: adv-grilling, answer: adv-grilling-answer }\n with:\n authorFile: <%= it.vars.requirementsFile %>\n onAdvance: architecting\n authorPrompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.requirementsFile %>`, which this prompt tells you to develop.\n\n `<%= it.vars.requirementsFile %>` holds the product requirements (or the\n plan you already wrote plus the human's answers on a return lap). Read it,\n explore the codebase, and develop it into a concrete, PRODUCT-level plan\n in this one turn.\n\n Scope: product and user-facing decisions only. Leave implementation\n details (file/module structure, data models, tech-stack choices) for the\n next phase (`<%= it.vars.architectureFile %>`).\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading near the top: one `### <question>` sub-heading\n each, followed by a checkbox list of candidate answers — aim for TWO\n concrete options plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself — the human ticks exactly one per question. The\n last `- [ ] _your answer_` line is the human's own-answer slot; write it\n with the literal `_your answer_` placeholder and leave it for them.\n\n On a RETURN lap the human has ticked one option per question (or replaced\n `_your answer_` with their own text). For each answered question: fold the\n chosen answer into the plan prose, then MOVE the question down into a\n `## Answered Questions` section as `### <question>` followed by the\n resolved answer in plain PROSE (drop the checkboxes). Never re-raise a\n question the human deleted; if the human deleted the whole\n `## Open Questions` section, treat the plan as accepted and surface no new\n questions unless something is genuinely still blocking. Omit\n `## Open Questions` entirely once there are none left.\n\n Leave `<%= it.vars.requirementsFile %>` uncommitted and finish your turn.\n answerMessage: |\n `<%= it.vars.requirementsFile %>` holds the product plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n advanceDescribe: >-\n With no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to accept the\n product plan and move on to technical architecting (**architecting**).\n editDescribe: >-\n Tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**adv-grilling**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n\n # Advanced flow: technical Q&A loop (architecting/architecting-answer -> decompose).\n - submachine: qaLoop\n name: arch\n as: { author: architecting, answer: architecting-answer }\n with:\n authorFile: <%= it.vars.architectureFile %>\n onAdvance: decompose\n authorPrompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.architectureFile %>` and `<%= it.vars.requirementsFile %>`,\n which this prompt tells you to write and delete respectively.\n\n Read `<%= it.vars.requirementsFile %>` (the converged product plan) and\n develop `<%= it.vars.architectureFile %>` from it in this one turn:\n file/module structure, data models, library/tech-stack choices,\n error-handling strategy — the *how*, building on the *what* already\n settled. Do not re-open product-level decisions; treat them as settled.\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading exactly as the grilling phase does: one\n `### <question>` each, followed by a checkbox list — TWO concrete options\n plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself. On a RETURN lap the human has ticked one option\n per question (or replaced `_your answer_` with their own text): fold each\n chosen answer into the plan prose, then MOVE the question into a\n `## Answered Questions` section as `### <question>` + the resolved answer\n in plain PROSE (drop the checkboxes). Never re-raise a deleted question;\n treat a deleted `## Open Questions` section as acceptance. Omit\n `## Open Questions` once there are none left.\n\n Once `<%= it.vars.architectureFile %>` is written, delete\n `<%= it.vars.requirementsFile %>` — its content has been folded in and it\n must not linger. Leave everything uncommitted and finish your turn.\n answerMessage: |\n `<%= it.vars.architectureFile %>` holds the technical plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n advanceDescribe: >-\n With no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to accept the\n technical plan and move on to decomposing the work into packages\n (**decompose**).\n editDescribe: >-\n Tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**architecting**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n\n # Simple flow plan-iteration loop (planning/plan-review -> building).\n - submachine: planLoop\n with:\n onAccept: building\n\n # Shared review tail (reviewing -> await-review -> ... -> squashing).\n - submachine: humanReview\n with:\n onSignoff: squashing\n\n # Advanced per-package agentic review (spec-review/spec-fix -> closing).\n - submachine: specGate\n with:\n onApproved: closing\n\n # Advanced per-package build queue (picking/adv-building/closing -> reviewing).\n - submachine: packageLoop\n with:\n onDrained: reviewing\n\nstates:\n # ── Shared entry ────────────────────────────────────────────────────────\n idle:\n actor: human\n initial: true\n # Every human-gate `message:` below closes with the same rendered \"What\n # each change does next\" list, built from `it.edges` (the resting state's\n # own `on` edges — each edge's `describe:` is the single source of truth).\n message: |\n No active gtd cycle.\n\n To start one, create a steering file describing what you want built:\n\n - `.gtd/TODO.md` — a short sketch — starts the SIMPLE flow (plan, build,\n review).\n - `.gtd/REQUIREMENTS.md` — the product requirements — starts the ADVANCED\n flow (product + technical Q&A, package decomposition, parallel build,\n per-package agentic review).\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* .gtd/REQUIREMENTS.md\":\n to: adv-start-check\n describe: >-\n Save the product requirements to `.gtd/REQUIREMENTS.md` to start the\n ADVANCED flow — gtd first checks the test baseline is green, then the\n agent develops them through product then technical Q&A, decomposes the\n work into packages, and builds each in parallel (**adv-start-check**).\n \"* **\":\n to: start-check\n describe: >-\n Save a short sketch to `.gtd/TODO.md` (or any other change) to start\n the SIMPLE flow — gtd first checks the test baseline is green, then the\n agent reads it, explores the codebase, and develops it into a concrete\n implementation plan (**start-check**).\n\n # ── Simple flow: monolithic build (plan loop is the `planLoop` sub-machine) ─\n building:\n actor: agent\n file: <%= it.vars.todoFile %>\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create or edit anything under `.gtd/` except deleting\n `<%= it.vars.todoFile %>`, which this prompt tells you to delete once its\n work is complete.\n\n The plan to implement is: <%~ it.read(it.vars.todoFile) %>\n\n Read that plan and implement it in full. Use TDD discipline: one test,\n then the implementation that passes it, then the next — never write all\n the tests first.\n\n Delete `<%= it.vars.todoFile %>` once the work it describes is complete\n and verified. Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": checking\n\n # ── Advanced flow: decomposition (Q&A loops are the `qaLoop` sub-machine) ───\n decompose:\n actor: agent\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except the\n package files this prompt tells you to write (under\n `<%= it.vars.packagesDir %>/`), and `<%= it.vars.architectureFile %>`,\n which it tells you to delete.\n\n Read `<%= it.vars.architectureFile %>` (the converged technical plan) and\n decompose it into an ordered set of WORK PACKAGES, one file each under\n `<%= it.vars.packagesDir %>/` (e.g. `<%= it.vars.packagesDir %>/01-name.md`,\n `<%= it.vars.packagesDir %>/02-name.md`, ...). Order the packages so the\n test suite can pass again after each one lands.\n\n Each package file describes a set of INDEPENDENT tasks that can be\n implemented concurrently without ordering constraints between them — a\n single build turn will fan them out to parallel subagents. Give each task\n a clear description, acceptance criteria as `- [ ]` checkboxes, and the\n relevant paths. Also state the package's overall requirement so it can be\n reviewed against its spec. Do NOT put ordering-dependent tasks in the same\n package — split them across ordered packages instead.\n\n No package file may reference any other `.gtd/` file. Once the package\n files are written, delete `<%= it.vars.architectureFile %>`. Leave\n everything uncommitted and finish your turn.\n on:\n \"* .gtd/packages/**\": picking\n\n # ── Green-baseline gate: `gtd fix` entry ──────────────────────────────────\n # `gtd fix` enters HERE (fixEntry): run the suite, then a red run drops\n # straight into the shared `fixing` loop (-> checking -> review + squash\n # tail); a green run is a no-op back to `idle` (nothing to fix).\n fix-check:\n actor: check\n fixEntry: true\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite to find what needs fixing; a red run\n # records .gtd/FEEDBACK.md (-> fixing), a green run cleans it up (-> idle).\n set +e\n mkdir -p .gtd\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output \"$feedback\"\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n else\n rm -f .gtd/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A .gtd/FEEDBACK.md\": fixing\n \"M .gtd/FEEDBACK.md\": fixing\n \"D .gtd/FEEDBACK.md\": idle\n \"C\": idle\n\n # ── Shared tail: squash finale (review loop is the `humanReview` sub-machine) ─\n squashing:\n actor: agent\n file: <%= it.vars.commitMsgFile %>\n model: <%= it.vars.coderModel %>\n memory: build\n prompt: |\n You are an autonomous coding agent. The cycle is approved and done.\n `.gtd/` holds this workflow's own state — never create, edit, or delete\n anything under `.gtd/` except `<%= it.vars.commitMsgFile %>`, which this\n prompt tells you to write.\n\n Leave `<%= it.vars.commitMsgFile %>` uncommitted and finish your turn —\n entering this file squashes everything this process retains into one\n commit using its content as the message. The diff below is exactly what\n that commit will contain: `it.retainedDiff`, the changes THIS process\n made (its trace/retry boundary onward). For a normal build cycle that is\n the whole feature; for a `gtd review` process it is only the fixes made\n during the review — NOT the changeset that was under review.\n <% if (it.retainedDiff.trim()) { %>\n\n Write `<%= it.vars.commitMsgFile %>` with ONE conventional-commits message\n for the changes below (subject line `type(scope): subject`, imperative\n mood, <= 72 characters; a body explaining the why, trade-offs, and key\n decisions). Plain text, no markdown wrapper.\n\n ## Retained diff\n\n ```diff\n <%~ it.retainedDiff %>\n ```\n <% } else { %>\n\n This process retained NO changes (a clean review sign-off with no fixes).\n Write `<%= it.vars.commitMsgFile %>` with exactly this one line and\n nothing else:\n\n chore: human review\n <% } %>\n on:\n \"A .gtd/COMMIT_MSG.md\": done\n \"M .gtd/COMMIT_MSG.md\": done\n\n done:\n commit: |\n <%~ it.read(it.vars.commitMsgFile) %>\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n";
190190
190529
  //#endregion
190191
190530
  //#region src/workflows/templates.ts
190192
190531
  /**
@@ -190236,12 +190575,20 @@ const UNIFIED_WORKFLOW = unified_default;
190236
190575
  * `compileWorkflowConfig` a user's `.gtdrc` `workflow:` key goes through — no
190237
190576
  * privileged code path. `src/Config.ts` (and the in-memory test layer) fall
190238
190577
  * back to this whenever no `workflow:` key is configured; `compileTemplate`
190239
- * exposes it fresh for Mermaid rendering and tests. `configDir` is `"."` and
190578
+ * exposes it fresh for tests. `configDir` is `"."` and
190240
190579
  * never consulted: no template content value starts with `./`/`../`.
190241
190580
  */
190242
190581
  const DEFAULT_WORKFLOW = compileWorkflowConfig((0, import_dist$1.parse)(UNIFIED_WORKFLOW), ".");
190243
190582
  /** The compiled built-in default workflow definition — the fallback when no `workflow:` is configured. */
190244
190583
  const defaultWorkflowDefinition = DEFAULT_WORKFLOW.definition;
190584
+ /**
190585
+ * The built-in default's RAW workflow value — `unified.yaml` parsed but NOT
190586
+ * expanded, so it still carries `submachines:`/`use:`. `src/Config.ts` exposes
190587
+ * it as `ConfigOperations.rawWorkflow` for the built-in default; tooling that
190588
+ * needs the sub-machine grouping the compiled definition flattens away (e.g.
190589
+ * `gtd visualize`'s `collectGroups`) reads it. Parsed once at module load.
190590
+ */
190591
+ const defaultWorkflowRaw = (0, import_dist$1.parse)(UNIFIED_WORKFLOW);
190245
190592
  /** The built-in default workflow's own declared `vars:` defaults (layer 1 of the merged `it.vars`). */
190246
190593
  const defaultWorkflowVars = DEFAULT_WORKFLOW.vars;
190247
190594
  /**
@@ -190335,6 +190682,117 @@ const modesJsonSchema = {
190335
190682
  }
190336
190683
  }
190337
190684
  };
190685
+ /** One state's shape — mirrors `PatternConfig.ts`'s `KNOWN_STATE_KEYS` and per-field compilers. */
190686
+ const stateJsonSchema = {
190687
+ type: "object",
190688
+ description: "One workflow state. Declare exactly one content kind (script/prompt/message/commit). A commit state is final: no actor, no on.",
190689
+ additionalProperties: false,
190690
+ properties: {
190691
+ actor: {
190692
+ type: "string",
190693
+ description: "Who acts at this state. Required on every non-commit state; forbidden on a commit state."
190694
+ },
190695
+ script: {
190696
+ type: "string",
190697
+ description: "Content kind: a shell script (Eta template). The loop driver executes it verbatim via bash, then steps this state's actor."
190698
+ },
190699
+ prompt: {
190700
+ type: "string",
190701
+ description: "Content kind: an agent prompt (Eta template), emitted by `gtd next`."
190702
+ },
190703
+ message: {
190704
+ type: "string",
190705
+ description: "Content kind: a human-facing message (Eta template), emitted by `gtd next`."
190706
+ },
190707
+ commit: {
190708
+ type: "string",
190709
+ description: "Content kind: entering this state ends the process by squashing it into one commit with this message (Eta template). Final — no actor, no on."
190710
+ },
190711
+ on: {
190712
+ type: "object",
190713
+ description: "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise). A value is either the target state name (string) or a { to, describe } object whose describe is a human-readable sentence templates can surface as it.edges (e.g. in a human gate's message).",
190714
+ additionalProperties: { oneOf: [{
190715
+ type: "string",
190716
+ description: "The target state name."
190717
+ }, {
190718
+ type: "object",
190719
+ additionalProperties: false,
190720
+ required: ["to"],
190721
+ properties: {
190722
+ to: {
190723
+ type: "string",
190724
+ description: "The target state name."
190725
+ },
190726
+ describe: {
190727
+ type: "string",
190728
+ description: "Human-readable sentence describing where this change routes; surfaced verbatim (never Eta-rendered) to templates as it.edges[].describe."
190729
+ }
190730
+ }
190731
+ }] }
190732
+ },
190733
+ initial: {
190734
+ type: "boolean",
190735
+ description: "Exactly one state in the workflow must declare initial: true (and it must not be a commit state)."
190736
+ },
190737
+ retry: {
190738
+ type: "object",
190739
+ description: "Redirect transitions INTO this state once it has been entered `max` times in the current process.",
190740
+ additionalProperties: false,
190741
+ required: ["max", "otherwise"],
190742
+ properties: {
190743
+ max: {
190744
+ type: "integer",
190745
+ minimum: 0,
190746
+ description: "Entries allowed this process before redirecting."
190747
+ },
190748
+ otherwise: {
190749
+ type: "string",
190750
+ description: "Defined state to redirect to once over the cap."
190751
+ }
190752
+ }
190753
+ },
190754
+ model: {
190755
+ type: "string",
190756
+ description: "Opaque harness hint passed through `gtd next --json`/`gtd status --json` (e.g. \"smart\"). Never interpreted by gtd. Forbidden on a commit state."
190757
+ },
190758
+ memory: {
190759
+ type: "string",
190760
+ description: "Opaque memory-scope label passed through `gtd next --json`/`gtd status --json` (e.g. \"plan\"). A memory-aware driver retains an agent's memory across consecutive agent turns sharing this label and starts fresh when it changes. Never interpreted by gtd. Forbidden on a commit state."
190761
+ },
190762
+ file: {
190763
+ type: "string",
190764
+ description: "The state's steering file: an Eta template naming the file a human/editor should look at while the machine rests here. Forbidden on a commit state."
190765
+ },
190766
+ mode: {
190767
+ type: "string",
190768
+ description: "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."
190769
+ },
190770
+ reviewWindow: {
190771
+ type: "boolean",
190772
+ description: "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."
190773
+ },
190774
+ reviewBase: {
190775
+ type: "boolean",
190776
+ description: "Marks the state whose most-recent in-process commit anchors the review window's diff base; absent any, the base is the process start. Forbidden on a commit state."
190777
+ },
190778
+ reviewEntry: {
190779
+ type: "boolean",
190780
+ description: "Marks the (at most one) state `gtd review <commitish>` enters to start a brand new process reviewing <commitish>..HEAD. Forbidden on a commit state and on the initial state."
190781
+ },
190782
+ fixEntry: {
190783
+ type: "boolean",
190784
+ description: "Marks the (at most one) state `gtd fix` enters to start a brand new process that goes straight into repairing the current failing tests. Forbidden on a commit state and on the initial state."
190785
+ },
190786
+ requireProgress: {
190787
+ type: "boolean",
190788
+ description: "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."
190789
+ },
190790
+ answerGate: {
190791
+ type: "boolean",
190792
+ description: "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."
190793
+ }
190794
+ }
190795
+ };
190338
190796
  /** The whole `workflow:` value — see `PatternConfig.ts`'s module docstring for the authoritative schema. */
190339
190797
  const workflowJsonSchema = {
190340
190798
  type: "object",
@@ -190344,116 +190802,67 @@ const workflowJsonSchema = {
190344
190802
  properties: {
190345
190803
  vars: varsJsonSchema,
190346
190804
  modes: modesJsonSchema,
190347
- states: {
190805
+ submachines: {
190348
190806
  type: "object",
190349
- description: "The workflow's named states. At least one; exactly one with initial: true.",
190350
- minProperties: 1,
190807
+ description: "Optional reusable sub-machines, invoked by `use:` and expanded into concrete states at load time (see src/Submachines.ts).",
190351
190808
  additionalProperties: {
190352
190809
  type: "object",
190353
- description: "One workflow state. Declare exactly one content kind (script/prompt/message/commit). A commit state is final: no actor, no on.",
190810
+ description: "A reusable, parameterized cluster of states, expanded at load time by each `use:` invocation into concrete states (see src/Submachines.ts). Purely a source-authoring convenience — the engine only ever sees the expanded states.",
190354
190811
  additionalProperties: false,
190812
+ required: ["states"],
190355
190813
  properties: {
190356
- actor: {
190357
- type: "string",
190358
- description: "Who acts at this state. Required on every non-commit state; forbidden on a commit state."
190359
- },
190360
- script: {
190361
- type: "string",
190362
- description: "Content kind: a shell script (Eta template). The loop driver executes it verbatim via bash, then steps this state's actor."
190814
+ params: {
190815
+ type: "array",
190816
+ items: { type: "string" },
190817
+ description: "Parameter names. A `$name` token used as a whole field value or `on`/`retry.otherwise` target is replaced by the invocation's `with:` binding."
190363
190818
  },
190364
- prompt: {
190365
- type: "string",
190366
- description: "Content kind: an agent prompt (Eta template), emitted by `gtd next`."
190367
- },
190368
- message: {
190819
+ states: {
190820
+ type: "object",
190821
+ description: "The sub-machine's local states. `on`/`retry.otherwise` targets naming another local are rewritten via `as:`; a `$name` value is bound from `with:`.",
190822
+ minProperties: 1,
190823
+ additionalProperties: stateJsonSchema
190824
+ }
190825
+ }
190826
+ }
190827
+ },
190828
+ use: {
190829
+ type: "array",
190830
+ description: "Sub-machine invocations. Each clones a `submachines:` entry into concrete states: `as:` renames its locals (identity when omitted), `with:` binds its `$param`s, `set:` adds extra per-instance state fields.",
190831
+ items: {
190832
+ type: "object",
190833
+ additionalProperties: false,
190834
+ required: ["submachine"],
190835
+ properties: {
190836
+ submachine: {
190369
190837
  type: "string",
190370
- description: "Content kind: a human-facing message (Eta template), emitted by `gtd next`."
190838
+ description: "Name of a declared sub-machine."
190371
190839
  },
190372
- commit: {
190840
+ name: {
190373
190841
  type: "string",
190374
- description: "Content kind: entering this state ends the process by squashing it into one commit with this message (Eta template). Final — no actor, no on."
190842
+ description: "Optional label for this instance (inert to expansion; used by tooling like `gtd visualize` to name the group). Defaults to the sub-machine name."
190375
190843
  },
190376
- on: {
190844
+ as: {
190377
190845
  type: "object",
190378
- description: "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise). A value is either the target state name (string) or a { to, describe } object whose describe is a human-readable sentence templates can surface as it.edges (e.g. in a human gate's message).",
190379
- additionalProperties: { oneOf: [{
190380
- type: "string",
190381
- description: "The target state name."
190382
- }, {
190383
- type: "object",
190384
- additionalProperties: false,
190385
- required: ["to"],
190386
- properties: {
190387
- to: {
190388
- type: "string",
190389
- description: "The target state name."
190390
- },
190391
- describe: {
190392
- type: "string",
190393
- description: "Human-readable sentence describing where this change routes; surfaced verbatim (never Eta-rendered) to templates as it.edges[].describe."
190394
- }
190395
- }
190396
- }] }
190846
+ description: "Rename map: local state name -> concrete state name (identity when omitted).",
190847
+ additionalProperties: { type: "string" }
190397
190848
  },
190398
- initial: {
190399
- type: "boolean",
190400
- description: "Exactly one state in the workflow must declare initial: true (and it must not be a commit state)."
190401
- },
190402
- retry: {
190849
+ with: {
190403
190850
  type: "object",
190404
- description: "Redirect transitions INTO this state once it has been entered `max` times in the current process.",
190405
- additionalProperties: false,
190406
- required: ["max", "otherwise"],
190407
- properties: {
190408
- max: {
190409
- type: "integer",
190410
- minimum: 0,
190411
- description: "Entries allowed this process before redirecting."
190412
- },
190413
- otherwise: {
190414
- type: "string",
190415
- description: "Defined state to redirect to once over the cap."
190416
- }
190417
- }
190418
- },
190419
- model: {
190420
- type: "string",
190421
- description: "Opaque harness hint passed through `gtd next --json`/`gtd status --json` (e.g. \"smart\"). Never interpreted by gtd. Forbidden on a commit state."
190422
- },
190423
- memory: {
190424
- type: "string",
190425
- description: "Opaque memory-scope label passed through `gtd next --json`/`gtd status --json` (e.g. \"plan\"). A memory-aware driver retains an agent's memory across consecutive agent turns sharing this label and starts fresh when it changes. Never interpreted by gtd. Forbidden on a commit state."
190426
- },
190427
- file: {
190428
- type: "string",
190429
- description: "The state's steering file: an Eta template naming the file a human/editor should look at while the machine rests here. Forbidden on a commit state."
190851
+ description: "Bindings for the sub-machine's `$param`s."
190430
190852
  },
190431
- mode: {
190432
- type: "string",
190433
- description: "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."
190434
- },
190435
- reviewWindow: {
190436
- type: "boolean",
190437
- description: "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."
190438
- },
190439
- reviewBase: {
190440
- type: "boolean",
190441
- description: "Marks the state whose most-recent in-process commit anchors the review window's diff base; absent any, the base is the process start. Forbidden on a commit state."
190442
- },
190443
- reviewEntry: {
190444
- type: "boolean",
190445
- description: "Marks the (at most one) state `gtd review <commitish>` enters to start a brand new process reviewing <commitish>..HEAD. Forbidden on a commit state and on the initial state."
190446
- },
190447
- fixEntry: {
190448
- type: "boolean",
190449
- description: "Marks the (at most one) state `gtd fix` enters to start a brand new process that goes straight into repairing the current failing tests. Forbidden on a commit state and on the initial state."
190450
- },
190451
- requireProgress: {
190452
- type: "boolean",
190453
- description: "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."
190853
+ set: {
190854
+ type: "object",
190855
+ description: "Extra fields merged onto a renamed local state, keyed by local name.",
190856
+ additionalProperties: { type: "object" }
190454
190857
  }
190455
190858
  }
190456
190859
  }
190860
+ },
190861
+ states: {
190862
+ type: "object",
190863
+ description: "The workflow's named states. At least one; exactly one with initial: true.",
190864
+ minProperties: 1,
190865
+ additionalProperties: stateJsonSchema
190457
190866
  }
190458
190867
  }
190459
190868
  };
@@ -190659,14 +191068,16 @@ const toOperations = (decoded, root) => {
190659
191068
  modes
190660
191069
  } : defaultWorkflowDefinition,
190661
191070
  workflowVars: defaultWorkflowVars,
190662
- rcVars
191071
+ rcVars,
191072
+ rawWorkflow: defaultWorkflowRaw
190663
191073
  };
190664
191074
  }
190665
191075
  const { definition, vars: workflowVars } = compileWorkflowConfig(decoded.workflow, root, rcModes, false);
190666
191076
  return {
190667
191077
  workflow: definition,
190668
191078
  workflowVars,
190669
- rcVars
191079
+ rcVars,
191080
+ rawWorkflow: decoded.workflow
190670
191081
  };
190671
191082
  };
190672
191083
  const formatSchemaError = (e) => {
@@ -191394,7 +191805,7 @@ const renderDiff = (files) => {
191394
191805
  };
191395
191806
  //#endregion
191396
191807
  //#region src/Git.ts
191397
- const EMPTY_TREE$2 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
191808
+ const EMPTY_TREE$3 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
191398
191809
  /**
191399
191810
  * Parse `git diff --name-status` output into `{ path, status }` pairs.
191400
191811
  * Status codes: A (added), D (deleted), M (modified), R (renamed), C (copied), etc.
@@ -191525,7 +191936,7 @@ const makeGitImpl = (executor, root) => {
191525
191936
  readFileAtRef: (ref, path) => exec("git", "show", `${ref}:${path}`),
191526
191937
  commitDiff: (hash, exclude = []) => gen(function* () {
191527
191938
  yield* exec("git", "rev-parse", "--verify", hash);
191528
- const parent = yield* exec("git", "rev-parse", "--verify", `${hash}~1`).pipe(map$4((s) => s.trim()), catchAll(() => succeed$3(EMPTY_TREE$2)));
191939
+ const parent = yield* exec("git", "rev-parse", "--verify", `${hash}~1`).pipe(map$4((s) => s.trim()), catchAll(() => succeed$3(EMPTY_TREE$3)));
191529
191940
  const nameStatusOut = yield* exec("git", "diff", "--name-status", parent, hash).pipe(catchAll(() => succeed$3("")));
191530
191941
  const allPaths = parseNameStatus(nameStatusOut);
191531
191942
  const filtered = applyExcludes(allPaths, exclude);
@@ -192167,7 +192578,7 @@ const renderModeCommand = (template, context) => eta.renderString(template, cont
192167
192578
  * one commit (or one squash). A caller that wants several transitions issues
192168
192579
  * several invocations.
192169
192580
  */
192170
- const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
192581
+ const EMPTY_TREE$2 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
192171
192582
  const subjectOf$1 = (message) => (message.split("\n")[0] ?? "").trim();
192172
192583
  const COST_TRAILER_PREFIX = "Gtd-Cost: ";
192173
192584
  const COST_TRAILER_RE = /^Gtd-Cost:[ \t]*([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+?))?[ \t]*$/gm;
@@ -192263,8 +192674,8 @@ const pendingChanges = (git) => git.changedPaths().pipe(map$4((entries) => entri
192263
192674
  const computeProcessRun = (git, def) => gen(function* () {
192264
192675
  if (!(yield* git.hasCommits())) return {
192265
192676
  startHash: "",
192266
- startParentHash: EMPTY_TREE$1,
192267
- diffBase: EMPTY_TREE$1,
192677
+ startParentHash: EMPTY_TREE$2,
192678
+ diffBase: EMPTY_TREE$2,
192268
192679
  trace: [],
192269
192680
  costEntries: []
192270
192681
  };
@@ -192280,7 +192691,7 @@ const computeProcessRun = (git, def) => gen(function* () {
192280
192691
  const processCommits = history.slice(startIdx);
192281
192692
  const trace = processCommits.map((h) => parseStateSubject(subjectOf$1(h.message)).state);
192282
192693
  const costEntries = parseCostTrailers(processCommits.map((h) => h.message));
192283
- const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$1;
192694
+ const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$2;
192284
192695
  return {
192285
192696
  startHash: startIdx < history.length ? history[startIdx].hash : history[history.length - 1].hash,
192286
192697
  startParentHash,
@@ -192497,25 +192908,45 @@ const executeDecision = (git, run, decision, context, cost, model) => gen(functi
192497
192908
  * every state subcommand:
192498
192909
  *
192499
192910
  * - `closeReviewWindow` runs BEFORE anything reads or mutates state, keyed
192500
- * solely on `refs/gtd/review-head` existing: `git reset --mixed
192501
- * refs/gtd/review-head` restores HEAD/index exactly, leaving only the
192502
- * reviewer's own edits dirty (captured by the resting state's own `on`
192503
- * patterns like any other pending change). The pure machine therefore never
192504
- * sees the window.
192911
+ * solely on `REVIEW_HEAD_REF` existing: `git reset --mixed <that ref>`
192912
+ * restores HEAD/index exactly, leaving only the reviewer's own edits dirty
192913
+ * (captured by the resting state's own `on` patterns like any other pending
192914
+ * change). The pure machine therefore never sees the window.
192505
192915
  * - `openReviewWindow` runs AFTER the subcommand finishes and self-guards on
192506
192916
  * the resolved rest declaring `reviewWindow: true`: it saves HEAD to
192507
- * `refs/gtd/review-head` (the base to `refs/gtd/review-base`), then `git
192508
- * reset --mixed <base>`. It re-arms after read-only commands (`gtd next` /
192509
- * `gtd status`) and refused invocations too, so the editor's diff view stays
192510
- * consistent no matter which command the loop last ran.
192917
+ * `REVIEW_HEAD_REF` (the base to `REVIEW_BASE_REF`), then `git reset --mixed
192918
+ * <base>`. It re-arms after read-only commands (`gtd next` / `gtd status`)
192919
+ * and refused invocations too, so the editor's diff view stays consistent no
192920
+ * matter which command the loop last ran.
192511
192921
  *
192512
192922
  * Every open/close step is idempotent under re-entry, so a crash at any point
192513
192923
  * is recovered by the next invocation's close (the saved ref also keeps the
192514
192924
  * real head GC-reachable for the window's whole lifetime).
192515
192925
  */
192516
- const REVIEW_HEAD_REF = "refs/gtd/review-head";
192517
- const REVIEW_BASE_REF = "refs/gtd/review-base";
192518
- const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
192926
+ /**
192927
+ * The window's two state refs live in git's PER-WORKTREE `refs/worktree/*`
192928
+ * namespace, so linked worktrees sharing one `.git` (`git worktree add`) each
192929
+ * get their OWN window: a review resting in one worktree is invisible to — and
192930
+ * un-clobberable by — every other one, and git drops the refs with the
192931
+ * worktree.
192932
+ *
192933
+ * gtd ≤ 7.1 kept them in the SHARED `refs/gtd/*` namespace (the legacy names
192934
+ * below), which made a second worktree's very first gtd invocation close the
192935
+ * FIRST worktree's window: its `git reset --mixed` moved the second worktree's
192936
+ * branch onto the other worktree's saved head, and every later invocation
192937
+ * refused with "a process is already underway" (issue #118).
192938
+ */
192939
+ const REVIEW_HEAD_REF = "refs/worktree/gtd/review-head";
192940
+ const REVIEW_BASE_REF = "refs/worktree/gtd/review-base";
192941
+ /**
192942
+ * The pre-7.2 SHARED refs. gtd never writes them any more; `closeReviewWindow`
192943
+ * only reads them to finish a window an older gtd left open across the upgrade
192944
+ * — and only when HEAD is still contained in the saved head, since a shared ref
192945
+ * may belong to a sibling worktree (see `openWindowRefs`).
192946
+ */
192947
+ const LEGACY_REVIEW_HEAD_REF = "refs/gtd/review-head";
192948
+ const LEGACY_REVIEW_BASE_REF = "refs/gtd/review-base";
192949
+ const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
192519
192950
  const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
192520
192951
  /**
192521
192952
  * The diff base while the window is open: the hash of the most-recent
@@ -192534,28 +192965,63 @@ const reviewBaseHash = (git, def, run) => gen(function* () {
192534
192965
  }
192535
192966
  return base;
192536
192967
  });
192968
+ /** The manual-recovery incantation every close refusal ends with. */
192969
+ const manualRecovery = (refs) => `\`git reset --mixed ${refs.headRef} && git update-ref -d ${refs.headRef} && git update-ref -d ${refs.baseRef}\``;
192970
+ /**
192971
+ * Which ref pair (if any) records an open window: this worktree's own
192972
+ * `refs/worktree/gtd/*` pair first, falling back to the pre-7.2 shared
192973
+ * `refs/gtd/*` pair so an upgrade mid-window still closes cleanly.
192974
+ */
192975
+ const openWindowRefs = (git) => gen(function* () {
192976
+ const scoped = yield* git.readRefOption(REVIEW_HEAD_REF);
192977
+ if (isSome(scoped)) return some({
192978
+ headRef: REVIEW_HEAD_REF,
192979
+ baseRef: REVIEW_BASE_REF,
192980
+ headHash: scoped.value,
192981
+ legacy: false
192982
+ });
192983
+ const legacy = yield* git.readRefOption(LEGACY_REVIEW_HEAD_REF);
192984
+ if (isNone(legacy)) return none$4();
192985
+ return some({
192986
+ headRef: LEGACY_REVIEW_HEAD_REF,
192987
+ baseRef: LEGACY_REVIEW_BASE_REF,
192988
+ headHash: legacy.value,
192989
+ legacy: true
192990
+ });
192991
+ });
192537
192992
  /**
192538
192993
  * Restore the real head if a review checkout window is open; no-op otherwise.
192539
- * Keyed on `refs/gtd/review-head` existence, NOT on any config or machine
192540
- * state, so it always runs first and the machine never sees the window.
192994
+ * Keyed on the saved-head ref's existence (see `openWindowRefs`), NOT on any
192995
+ * config or machine state, so it always runs first and the machine never sees
192996
+ * the window.
192541
192997
  *
192542
192998
  * Fails loudly — refs left in place — when HEAD is no longer on the reviewed
192543
192999
  * branch (the base is not an ancestor of HEAD, e.g. after a branch switch): a
192544
193000
  * mixed reset there would rewrite the wrong branch's tip. Manual commits on
192545
193001
  * top of the base pass the guard: the reset keeps their content in the working
192546
193002
  * tree, where the next turn's capture picks it up as review feedback.
193003
+ *
193004
+ * A window found under the LEGACY shared refs gets one extra guard: a shared
193005
+ * ref may have been written by a sibling worktree, so the close only proceeds
193006
+ * when HEAD is contained in the saved head — the shape a window this worktree
193007
+ * opened itself has. Anything else refuses with the manual recovery rather
193008
+ * than resetting this branch onto another worktree's work (issue #118).
192547
193009
  */
192548
193010
  const closeReviewWindow = gen(function* () {
192549
193011
  const git = yield* GitService;
192550
- const savedHead = yield* git.readRefOption(REVIEW_HEAD_REF);
192551
- if (isNone(savedHead)) return { closed: false };
192552
- const base = yield* git.readRefOption(REVIEW_BASE_REF);
192553
- if (isSome(base) && base.value !== EMPTY_TREE) {
192554
- 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 `git reset --mixed refs/gtd/review-head && git update-ref -d refs/gtd/review-head && git update-ref -d refs/gtd/review-base`"));
192555
- }
192556
- yield* git.mixedResetTo(REVIEW_HEAD_REF);
192557
- yield* git.deleteRef(REVIEW_HEAD_REF);
192558
- yield* git.deleteRef(REVIEW_BASE_REF);
193012
+ const open = yield* openWindowRefs(git);
193013
+ if (isNone(open)) return { closed: false };
193014
+ const refs = open.value;
193015
+ const base = yield* git.readRefOption(refs.baseRef);
193016
+ if (isSome(base) && base.value !== EMPTY_TREE$1) {
193017
+ 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)}`));
193018
+ }
193019
+ if (refs.legacy) {
193020
+ 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)}`));
193021
+ }
193022
+ yield* git.mixedResetTo(refs.headRef);
193023
+ yield* git.deleteRef(refs.headRef);
193024
+ yield* git.deleteRef(refs.baseRef);
192559
193025
  return { closed: true };
192560
193026
  });
192561
193027
  /**
@@ -192585,7 +193051,7 @@ const openReviewWindow = gen(function* () {
192585
193051
  const run = yield* computeProcessRun(git, def);
192586
193052
  const base = (yield* reviewBaseHash(git, def, run)) ?? run.diffBase;
192587
193053
  const headHash = yield* git.resolveRef("HEAD");
192588
- if (base === EMPTY_TREE || base === headHash) return { opened: false };
193054
+ if (base === EMPTY_TREE$1 || base === headHash) return { opened: false };
192589
193055
  yield* git.updateRef(REVIEW_BASE_REF, base);
192590
193056
  yield* git.updateRef(REVIEW_HEAD_REF, headHash);
192591
193057
  yield* git.mixedResetTo(base);
@@ -204422,74 +204888,142 @@ const startLspServer = () => gen(function* () {
204422
204888
  });
204423
204889
  });
204424
204890
  //#endregion
204425
- //#region src/Mermaid.ts
204426
- /**
204427
- * `gtd mermaid`: a pure emitter of the active workflow's SHAPE as Mermaid
204428
- * `stateDiagram-v2` source — one node per state, the `[*] -->` initial-state
204429
- * marker, one edge per declared `on` row (labeled with its raw pattern
204430
- * string), `--> [*]` for every commit state (final, no outgoing edges), and
204431
- * one `note right of` per rest naming its actor/content-kind/retry cap. No
204432
- * git, no Effect, no template rendering: same purity discipline as
204433
- * `PatternMachine.ts` — a plain function of an already-`validateDefinition`d
204434
- * `WorkflowDefinition` (see `Config.ts`'s `toOperations`/`compileWorkflowConfig`
204435
- * — every `WorkflowDefinition` this module ever sees has already been
204436
- * validated, so an `on` target or `retry.otherwise` is trusted to name a
204437
- * defined state rather than re-checked here).
204438
- */
204439
- /**
204440
- * Mermaid identifiers must avoid quotes/newlines inside a quoted label —
204441
- * collapse both so the label stays on one line and the diagram stays
204442
- * parseable even if a state name or `on` pattern happens to carry either.
204443
- */
204444
- const escapeLabel = (text) => text.replace(/"/g, "'").replace(/\r?\n/g, " ");
204445
- /**
204446
- * A Mermaid-safe node id for one state name: every non-word character folds
204447
- * to `_` (state names may carry hyphens — `plan-review`,
204448
- * `review-deciding` — which Mermaid's bare identifier grammar doesn't
204449
- * guarantee), and a leading digit gets an `s_` prefix (Mermaid ids can't
204450
- * start with one). The exact original name still reaches the diagram via
204451
- * this alias's `state "<name>" as <alias>` declaration.
204452
- */
204453
- const aliasFor = (name) => {
204454
- const folded = name.replace(/[^A-Za-z0-9_]/g, "_");
204455
- return /^[0-9]/.test(folded) ? `s_${folded}` : folded;
204456
- };
204457
- /** One rest's actor/content-kind/retry-cap summary, for a `note right of` line. */
204458
- const stateNote = (state) => {
204459
- const parts = [state.actor, contentKindOf(state)].filter((p) => p !== void 0);
204460
- const retry = state.retry !== void 0 ? [`retry ${state.retry.max}→${state.retry.otherwise}`] : [];
204461
- return [...parts, ...retry].join(" · ");
204462
- };
204463
- /**
204464
- * Render `def`'s shape as Mermaid `stateDiagram-v2` source. One
204465
- * `state "<name>" as <alias>` declaration per state up front (so every
204466
- * transition/note below can reference the safe alias while the rendered
204467
- * diagram still shows the exact declared name), then the initial-state
204468
- * marker, then one edge per `on` row (declaration order — same "first match
204469
- * wins" order the engine itself evaluates), then `<alias> --> [*]` for every
204470
- * commit state, then one actor/content-kind/retry note per rest.
204471
- */
204472
- const renderMermaid = (def) => {
204473
- const names = Object.keys(def.states);
204474
- const alias = new Map(names.map((name) => [name, aliasFor(name)]));
204475
- const lines = ["stateDiagram-v2"];
204476
- for (const name of names) lines.push(` state "${escapeLabel(name)}" as ${alias.get(name)}`);
204477
- lines.push(` [*] --> ${alias.get(initialStateOf(def))}`);
204478
- for (const name of names) {
204479
- const state = def.states[name];
204480
- const from = alias.get(name);
204481
- if (isCommitState(state)) {
204482
- lines.push(` ${from} --> [*]`);
204483
- continue;
204484
- }
204485
- for (const [pattern, target] of state.on ?? []) lines.push(` ${from} --> ${alias.get(target)} : ${escapeLabel(pattern)}`);
204486
- }
204487
- for (const name of names) {
204488
- const state = def.states[name];
204489
- if (isCommitState(state)) continue;
204490
- lines.push(` note right of ${alias.get(name)} : ${escapeLabel(stateNote(state))}`);
204891
+ //#region src/visualize.html
204892
+ var visualize_default = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>gtd · workflow visualizer</title>\n <style>\n :root {\n --bg: #f4f6fa;\n --surface: #ffffff;\n --surface-2: #eef1f7;\n --surface-3: #e6ebf3;\n --border: #d9dee8;\n --border-strong: #c2ccdb;\n --text: #1a2233;\n --text-dim: #55607a;\n --text-faint: #8a93a6;\n --accent: #2d6cdf;\n --accent-soft: #e4edfd;\n --human-bg: #fbeccd;\n --human-fg: #7a5310;\n --human-dot: #d99413;\n --agent-bg: #e5e2fb;\n --agent-fg: #463693;\n --agent-dot: #6d5ce0;\n --check-bg: #d3f0ef;\n --check-fg: #0c524f;\n --check-dot: #16a39d;\n --engine-bg: #fbdfe4;\n --engine-fg: #8a1f38;\n --engine-dot: #d84a68;\n --unknown-bg: #eef1f7;\n --unknown-fg: #55607a;\n --unknown-dot: #8a93a6;\n --shadow: 0 1px 2px rgba(20, 30, 50, 0.06), 0 8px 28px rgba(20, 30, 50, 0.1);\n --shadow-sm: 0 1px 2px rgba(20, 30, 50, 0.08);\n --radius: 12px;\n --mono:\n ui-monospace, \"SF Mono\", \"SFMono-Regular\", Menlo, Consolas, \"Liberation Mono\", monospace;\n --sans:\n ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n --dia-bg: #f7f9fc;\n }\n @media (prefers-color-scheme: dark) {\n :root {\n --bg: #0e1420;\n --surface: #161d2b;\n --surface-2: #1c2434;\n --surface-3: #232c3e;\n --border: #2b3547;\n --border-strong: #3a465c;\n --text: #e6ebf4;\n --text-dim: #9aa6bd;\n --text-faint: #6b7690;\n --accent: #5b8def;\n --accent-soft: #1b2740;\n --human-bg: #3a2e12;\n --human-fg: #f0c674;\n --human-dot: #e0a83e;\n --agent-bg: #26224a;\n --agent-fg: #b6abf7;\n --agent-dot: #8b7cf0;\n --check-bg: #123433;\n --check-fg: #68dcd4;\n --check-dot: #2dc4bc;\n --engine-bg: #3a1b24;\n --engine-fg: #f2a3b3;\n --engine-dot: #e05f7c;\n --unknown-bg: #1c2434;\n --unknown-fg: #9aa6bd;\n --unknown-dot: #6b7690;\n --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 12px 34px rgba(0, 0, 0, 0.45);\n --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);\n --dia-bg: #0b1018;\n }\n }\n :root[data-theme=\"light\"] {\n --bg: #f4f6fa;\n --surface: #ffffff;\n --surface-2: #eef1f7;\n --surface-3: #e6ebf3;\n --border: #d9dee8;\n --border-strong: #c2ccdb;\n --text: #1a2233;\n --text-dim: #55607a;\n --text-faint: #8a93a6;\n --accent: #2d6cdf;\n --accent-soft: #e4edfd;\n --human-bg: #fbeccd;\n --human-fg: #7a5310;\n --human-dot: #d99413;\n --agent-bg: #e5e2fb;\n --agent-fg: #463693;\n --agent-dot: #6d5ce0;\n --check-bg: #d3f0ef;\n --check-fg: #0c524f;\n --check-dot: #16a39d;\n --engine-bg: #fbdfe4;\n --engine-fg: #8a1f38;\n --engine-dot: #d84a68;\n --unknown-bg: #eef1f7;\n --unknown-fg: #55607a;\n --unknown-dot: #8a93a6;\n --shadow: 0 1px 2px rgba(20, 30, 50, 0.06), 0 8px 28px rgba(20, 30, 50, 0.1);\n --shadow-sm: 0 1px 2px rgba(20, 30, 50, 0.08);\n --dia-bg: #f7f9fc;\n }\n :root[data-theme=\"dark\"] {\n --bg: #0e1420;\n --surface: #161d2b;\n --surface-2: #1c2434;\n --surface-3: #232c3e;\n --border: #2b3547;\n --border-strong: #3a465c;\n --text: #e6ebf4;\n --text-dim: #9aa6bd;\n --text-faint: #6b7690;\n --accent: #5b8def;\n --accent-soft: #1b2740;\n --human-bg: #3a2e12;\n --human-fg: #f0c674;\n --human-dot: #e0a83e;\n --agent-bg: #26224a;\n --agent-fg: #b6abf7;\n --agent-dot: #8b7cf0;\n --check-bg: #123433;\n --check-fg: #68dcd4;\n --check-dot: #2dc4bc;\n --engine-bg: #3a1b24;\n --engine-fg: #f2a3b3;\n --engine-dot: #e05f7c;\n --unknown-bg: #1c2434;\n --unknown-fg: #9aa6bd;\n --unknown-dot: #6b7690;\n --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 12px 34px rgba(0, 0, 0, 0.45);\n --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);\n --dia-bg: #0b1018;\n }\n\n * {\n box-sizing: border-box;\n }\n html,\n body {\n max-width: 100%;\n overflow-x: hidden;\n }\n body {\n margin: 0;\n background: var(--bg);\n color: var(--text);\n font-family: var(--sans);\n font-size: 15px;\n line-height: 1.55;\n -webkit-font-smoothing: antialiased;\n }\n ::selection {\n background: var(--accent);\n color: #fff;\n }\n .wrap {\n max-width: 1180px;\n margin: 0 auto;\n padding: 0 20px;\n }\n\n /* header */\n header.top {\n position: sticky;\n top: 0;\n z-index: 40;\n background: color-mix(in srgb, var(--bg) 88%, transparent);\n backdrop-filter: blur(10px);\n border-bottom: 1px solid var(--border);\n }\n .top-inner {\n display: flex;\n align-items: center;\n gap: 18px;\n padding: 12px 20px;\n max-width: 1180px;\n margin: 0 auto;\n flex-wrap: wrap;\n }\n .brand {\n display: flex;\n align-items: baseline;\n gap: 10px;\n margin-right: auto;\n }\n .brand .cmd {\n font-family: var(--mono);\n font-weight: 700;\n font-size: 17px;\n letter-spacing: -0.02em;\n }\n .brand .cmd b {\n color: var(--accent);\n }\n .brand .sub {\n font-size: 12px;\n color: var(--text-faint);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n }\n .summary {\n font-size: 12.5px;\n color: var(--text-dim);\n display: flex;\n gap: 10px;\n flex-wrap: wrap;\n align-items: center;\n }\n .summary .s-num {\n font-family: var(--mono);\n font-weight: 700;\n color: var(--text);\n }\n .summary .sep {\n color: var(--border-strong);\n }\n .theme-btn {\n border: 1px solid var(--border);\n background: var(--surface);\n color: var(--text-dim);\n border-radius: 8px;\n width: 32px;\n height: 32px;\n cursor: pointer;\n font-size: 14px;\n display: grid;\n place-items: center;\n }\n .theme-btn:hover {\n color: var(--text);\n border-color: var(--border-strong);\n }\n .theme-btn:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n }\n\n /* hero */\n .hero {\n padding: 40px 0 4px;\n }\n h1 {\n font-size: clamp(26px, 4vw, 36px);\n line-height: 1.08;\n letter-spacing: -0.025em;\n margin: 0 0 12px;\n font-weight: 780;\n }\n h1 .mono {\n font-family: var(--mono);\n font-weight: 700;\n }\n .lede {\n font-size: 16px;\n color: var(--text-dim);\n max-width: 70ch;\n margin: 0;\n }\n .lede code {\n font-family: var(--mono);\n font-size: 0.88em;\n background: var(--surface-2);\n padding: 1px 6px;\n border-radius: 5px;\n color: var(--text);\n }\n\n /* section headers */\n .sec-head {\n display: flex;\n align-items: baseline;\n gap: 12px;\n margin: 46px 0 4px;\n }\n .sec-head h2 {\n font-size: 13px;\n text-transform: uppercase;\n letter-spacing: 0.12em;\n color: var(--text-faint);\n margin: 0;\n font-weight: 700;\n }\n .sec-head .rule {\n flex: 1;\n height: 1px;\n background: var(--border);\n }\n\n /* diagram */\n .diagram-note {\n font-size: 13px;\n color: var(--text-dim);\n margin: 16px 0 14px;\n max-width: 78ch;\n }\n .diagram-note .mono {\n font-family: var(--mono);\n }\n .dia-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n box-shadow: var(--shadow-sm);\n overflow: hidden;\n }\n .dia-scroll {\n overflow-x: auto;\n background: var(--dia-bg);\n padding: 18px 16px;\n }\n .dia-scroll .mflow {\n min-width: min-content;\n display: flex;\n justify-content: center;\n }\n .dia-scroll .mflow svg {\n max-width: none;\n height: auto;\n }\n .dia-msg {\n padding: 22px 18px;\n font-size: 13.5px;\n color: var(--text-dim);\n }\n .dia-msg code {\n font-family: var(--mono);\n font-size: 12px;\n background: var(--surface-2);\n padding: 1px 6px;\n border-radius: 5px;\n }\n .dia-src {\n margin: 10px 0 0;\n padding: 12px 14px;\n background: var(--surface-2);\n border-radius: 9px;\n font-family: var(--mono);\n font-size: 11.5px;\n white-space: pre;\n overflow-x: auto;\n color: var(--text-dim);\n max-height: 260px;\n }\n\n /* sub-machines */\n .sm-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n gap: 14px;\n margin-top: 18px;\n }\n .sm-def {\n border: 1.5px dashed var(--border-strong);\n border-radius: var(--radius);\n background: var(--surface);\n box-shadow: var(--shadow-sm);\n overflow: hidden;\n }\n .sm-def .h {\n padding: 13px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n }\n .sm-def .h .nm {\n font-family: var(--mono);\n font-weight: 700;\n font-size: 15px;\n }\n .sm-def .h .pm {\n font-family: var(--mono);\n font-size: 12px;\n color: var(--text-dim);\n display: block;\n margin-top: 3px;\n }\n .sm-def .h .pm b {\n color: var(--accent);\n }\n .sm-def .body {\n padding: 12px 16px 16px;\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n }\n .muted-note {\n color: var(--text-faint);\n font-style: italic;\n margin-top: 16px;\n }\n\n /* toolbar */\n .toolbar {\n display: flex;\n gap: 12px;\n align-items: center;\n margin: 18px 0 22px;\n flex-wrap: wrap;\n }\n .search {\n flex: 1;\n min-width: 200px;\n position: relative;\n }\n .search input {\n width: 100%;\n font-family: var(--mono);\n font-size: 14px;\n padding: 10px 12px 10px 34px;\n border-radius: 10px;\n border: 1px solid var(--border);\n background: var(--surface);\n color: var(--text);\n }\n .search input:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 3px var(--accent-soft);\n }\n .search svg {\n position: absolute;\n left: 11px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--text-faint);\n }\n .chips {\n display: flex;\n gap: 6px;\n flex-wrap: wrap;\n }\n .chip-btn {\n font: inherit;\n font-size: 12.5px;\n padding: 7px 12px;\n border-radius: 999px;\n border: 1px solid var(--border);\n background: var(--surface);\n color: var(--text-dim);\n cursor: pointer;\n }\n .chip-btn:hover {\n border-color: var(--border-strong);\n color: var(--text);\n }\n .chip-btn.on {\n background: var(--accent);\n border-color: var(--accent);\n color: #fff;\n }\n .chip-btn:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n }\n .legend {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n }\n .legend .li {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 12.5px;\n color: var(--text-dim);\n cursor: pointer;\n user-select: none;\n padding: 5px 9px;\n border-radius: 999px;\n border: 1px solid var(--border);\n background: var(--surface);\n }\n .legend .li:hover {\n border-color: var(--border-strong);\n }\n .legend .li.off {\n opacity: 0.4;\n }\n .legend .li:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n }\n .legend .li .dot {\n width: 10px;\n height: 10px;\n border-radius: 3px;\n }\n\n /* groups + cards */\n .group {\n margin-bottom: 30px;\n }\n .group-head {\n display: flex;\n align-items: baseline;\n gap: 10px;\n margin: 0 0 12px;\n }\n .group-head h3 {\n font-family: var(--mono);\n font-size: 15px;\n margin: 0;\n letter-spacing: -0.01em;\n }\n .group-head .flow-tag {\n font-family: var(--mono);\n font-size: 11px;\n padding: 2px 8px;\n border-radius: 6px;\n background: var(--surface-2);\n color: var(--text-dim);\n border: 1px solid var(--border);\n }\n .grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n gap: 12px;\n }\n .card {\n text-align: left;\n font: inherit;\n cursor: pointer;\n background: var(--surface);\n border: 1px solid var(--border);\n border-left: 4px solid var(--card-accent, var(--border));\n border-radius: 10px;\n padding: 13px 15px;\n box-shadow: var(--shadow-sm);\n transition:\n transform 0.12s ease,\n box-shadow 0.12s ease,\n border-color 0.12s ease;\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n .card:hover {\n transform: translateY(-2px);\n box-shadow: var(--shadow);\n }\n .card:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n }\n .card[data-actor=\"human\"] {\n --card-accent: var(--human-dot);\n }\n .card[data-actor=\"agent\"] {\n --card-accent: var(--agent-dot);\n }\n .card[data-actor=\"check\"] {\n --card-accent: var(--check-dot);\n }\n .card[data-actor=\"commit\"] {\n --card-accent: var(--engine-dot);\n }\n .card[data-actor=\"unknown\"] {\n --card-accent: var(--unknown-dot);\n }\n .card .name {\n font-family: var(--mono);\n font-weight: 700;\n font-size: 14.5px;\n letter-spacing: -0.01em;\n word-break: break-word;\n }\n .card .row {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n }\n .card .init-tick {\n position: absolute;\n top: 11px;\n right: 12px;\n font-size: 10px;\n font-family: var(--mono);\n color: var(--accent);\n background: var(--accent-soft);\n padding: 1px 6px;\n border-radius: 5px;\n font-weight: 700;\n }\n\n .pill {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-family: var(--mono);\n font-size: 11px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 999px;\n }\n .pill.actor-human {\n background: var(--human-bg);\n color: var(--human-fg);\n }\n .pill.actor-agent {\n background: var(--agent-bg);\n color: var(--agent-fg);\n }\n .pill.actor-check {\n background: var(--check-bg);\n color: var(--check-fg);\n }\n .pill.actor-engine {\n background: var(--engine-bg);\n color: var(--engine-fg);\n }\n .pill.actor-unknown {\n background: var(--unknown-bg);\n color: var(--unknown-fg);\n }\n .pill .dot {\n width: 7px;\n height: 7px;\n border-radius: 2px;\n }\n .pill.actor-human .dot {\n background: var(--human-dot);\n }\n .pill.actor-agent .dot {\n background: var(--agent-dot);\n }\n .pill.actor-check .dot {\n background: var(--check-dot);\n }\n .pill.actor-engine .dot {\n background: var(--engine-dot);\n }\n .pill.actor-unknown .dot {\n background: var(--unknown-dot);\n }\n .kind {\n font-family: var(--mono);\n font-size: 11px;\n padding: 2px 7px;\n border-radius: 6px;\n border: 1px solid var(--border-strong);\n color: var(--text-dim);\n background: var(--surface-2);\n }\n .flag {\n font-family: var(--mono);\n font-size: 10.5px;\n padding: 1px 6px;\n border-radius: 5px;\n background: var(--accent-soft);\n color: var(--accent);\n font-weight: 600;\n border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);\n }\n\n /* drawer */\n .backdrop {\n position: fixed;\n inset: 0;\n background: rgba(10, 16, 28, 0.5);\n backdrop-filter: blur(2px);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.22s ease;\n z-index: 90;\n }\n .backdrop.open {\n opacity: 1;\n pointer-events: auto;\n }\n .drawer {\n position: fixed;\n top: 0;\n right: 0;\n height: 100%;\n width: min(560px, 100%);\n background: var(--surface);\n border-left: 1px solid var(--border);\n box-shadow: var(--shadow);\n transform: translateX(100%);\n transition: transform 0.26s cubic-bezier(0.22, 0.61, 0.36, 1);\n z-index: 100;\n display: flex;\n flex-direction: column;\n }\n .drawer.open {\n transform: translateX(0);\n }\n .drawer-head {\n padding: 18px 22px 14px;\n border-bottom: 1px solid var(--border);\n border-top: 4px solid var(--card-accent, var(--accent));\n }\n .drawer-head .top {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n }\n .drawer-head h3 {\n font-family: var(--mono);\n font-size: 20px;\n font-weight: 700;\n margin: 0;\n letter-spacing: -0.02em;\n word-break: break-word;\n }\n .drawer-head .meta-row {\n display: flex;\n gap: 6px;\n flex-wrap: wrap;\n margin-top: 10px;\n }\n .close-x {\n margin-left: auto;\n border: 1px solid var(--border);\n background: var(--surface-2);\n color: var(--text-dim);\n border-radius: 8px;\n width: 30px;\n height: 30px;\n cursor: pointer;\n flex: none;\n font-size: 15px;\n }\n .close-x:hover {\n color: var(--text);\n }\n .close-x:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n }\n .drawer-body {\n overflow-y: auto;\n padding: 20px 22px 40px;\n flex: 1;\n }\n .field {\n margin-bottom: 18px;\n }\n .field > .k {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.09em;\n color: var(--text-faint);\n font-weight: 700;\n margin-bottom: 7px;\n }\n .kv {\n display: grid;\n grid-template-columns: 88px 1fr;\n gap: 4px 14px;\n font-size: 13.5px;\n }\n .kv dt {\n color: var(--text-faint);\n font-size: 12px;\n }\n .kv dd {\n margin: 0;\n font-family: var(--mono);\n font-size: 12.5px;\n word-break: break-word;\n }\n .none {\n color: var(--text-faint);\n font-style: italic;\n font-family: var(--sans);\n }\n\n .edges {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n .edge {\n border: 1px solid var(--border);\n border-radius: 9px;\n padding: 10px 12px;\n background: var(--surface-2);\n }\n .edge .er {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n }\n .edge .pat {\n font-family: var(--mono);\n font-size: 12px;\n font-weight: 700;\n background: var(--surface);\n border: 1px solid var(--border-strong);\n padding: 2px 8px;\n border-radius: 6px;\n white-space: nowrap;\n }\n .edge .arr {\n color: var(--text-faint);\n }\n .goto {\n font: inherit;\n font-family: var(--mono);\n font-size: 12.5px;\n font-weight: 700;\n cursor: pointer;\n border: 1px solid var(--card-accent, var(--accent));\n background: transparent;\n color: var(--card-accent, var(--accent));\n padding: 2px 9px;\n border-radius: 6px;\n }\n .goto:hover {\n background: var(--card-accent, var(--accent));\n color: #fff;\n }\n .goto:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n }\n .goto[data-actor=\"human\"] {\n --card-accent: var(--human-dot);\n }\n .goto[data-actor=\"agent\"] {\n --card-accent: var(--agent-dot);\n }\n .goto[data-actor=\"check\"] {\n --card-accent: var(--check-dot);\n }\n .goto[data-actor=\"commit\"] {\n --card-accent: var(--engine-dot);\n }\n .goto[data-actor=\"unknown\"] {\n --card-accent: var(--unknown-dot);\n }\n .edge .em {\n font-size: 12.5px;\n color: var(--text-dim);\n margin-top: 6px;\n line-height: 1.45;\n }\n .incoming {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n }\n\n .flag-list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n .flag-item {\n display: flex;\n gap: 9px;\n font-size: 13px;\n align-items: baseline;\n }\n .flag-item .fn {\n font-family: var(--mono);\n font-size: 11.5px;\n font-weight: 700;\n color: var(--accent);\n flex: none;\n padding: 1px 6px;\n border-radius: 5px;\n background: var(--accent-soft);\n }\n .flag-item .fd {\n color: var(--text-dim);\n }\n\n /* vars */\n .vars-grid {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 16px;\n }\n .var-chip {\n font-family: var(--mono);\n font-size: 12px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--border);\n background: var(--surface);\n box-shadow: var(--shadow-sm);\n }\n .var-chip b {\n color: var(--accent);\n }\n\n footer {\n border-top: 1px solid var(--border);\n margin-top: 56px;\n padding: 24px 0 60px;\n color: var(--text-faint);\n font-size: 13px;\n }\n footer code {\n font-family: var(--mono);\n background: var(--surface-2);\n padding: 1px 6px;\n border-radius: 5px;\n }\n\n .hidden {\n display: none !important;\n }\n @media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n transition: none !important;\n animation: none !important;\n }\n }\n @media (max-width: 640px) {\n .brand .sub {\n display: none;\n }\n .kv {\n grid-template-columns: 74px 1fr;\n }\n }\n </style>\n </head>\n <body>\n <header class=\"top\">\n <div class=\"top-inner\">\n <div class=\"brand\">\n <span class=\"cmd\"><b>gtd</b> · workflow visualizer</span>\n <span class=\"sub\" id=\"brandSub\">read-only view</span>\n </div>\n <div class=\"summary\" id=\"summary\"></div>\n <button\n class=\"theme-btn\"\n id=\"themeBtn\"\n title=\"Toggle light/dark theme\"\n aria-label=\"Toggle light/dark theme\"\n >\n ◐\n </button>\n </div>\n </header>\n\n <main class=\"wrap\" id=\"app\">\n <section class=\"hero\">\n <h1 id=\"heroTitle\">Loading workflow…</h1>\n <p class=\"lede\" id=\"heroLede\">\n Fetching <code>/workflow.json</code> from the local\n <span class=\"mono\">gtd visualize</span> server.\n </p>\n </section>\n\n <div id=\"content\" class=\"hidden\">\n <div class=\"sec-head\">\n <h2>Main flow</h2>\n <span class=\"rule\"></span>\n </div>\n <p class=\"diagram-note\" id=\"diaNote\"></p>\n <div class=\"dia-card\">\n <div class=\"dia-scroll\" id=\"diaScroll\"><div class=\"dia-msg\">Rendering diagram…</div></div>\n </div>\n\n <div class=\"sec-head\">\n <h2>Sub-machines</h2>\n <span class=\"rule\"></span>\n </div>\n <div id=\"smSection\"></div>\n\n <div class=\"sec-head\">\n <h2>State catalogue</h2>\n <span class=\"rule\"></span>\n </div>\n <div class=\"toolbar\">\n <div class=\"search\">\n <svg\n width=\"15\"\n height=\"15\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.4\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m21 21-4.3-4.3\" />\n </svg>\n <input\n id=\"search\"\n type=\"search\"\n placeholder=\"filter by name, actor, kind, file, mode, flag…\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-label=\"Filter states\"\n />\n </div>\n <div class=\"chips\" id=\"kindChips\"></div>\n <div class=\"legend\" id=\"legend\"></div>\n </div>\n <div id=\"groups\"></div>\n </div>\n </main>\n\n <div class=\"backdrop\" id=\"backdrop\"></div>\n <aside\n class=\"drawer\"\n id=\"drawer\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"State detail\"\n tabindex=\"-1\"\n ></aside>\n\n <footer class=\"wrap\" id=\"footer\"></footer>\n\n <script type=\"module\">\n // ── theme ──────────────────────────────────────────────────────────────\n const root = document.documentElement\n const prefersDark = () => matchMedia(\"(prefers-color-scheme: dark)\").matches\n const isDark = () => {\n const t = root.getAttribute(\"data-theme\")\n return t ? t === \"dark\" : prefersDark()\n }\n document.getElementById(\"themeBtn\").onclick = () => {\n const next = isDark() ? \"light\" : \"dark\"\n root.setAttribute(\"data-theme\", next)\n renderDiagram()\n }\n\n const esc = (s) =>\n String(s).replace(\n /[&<>\"']/g,\n (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" })[c],\n )\n\n // Actor metadata: color class, css-var prefix, mermaid classDef name, label.\n const ACTOR = {\n human: { cls: \"actor-human\", dot: \"human\", label: \"human\" },\n agent: { cls: \"actor-agent\", dot: \"agent\", label: \"agent\" },\n check: { cls: \"actor-check\", dot: \"check\", label: \"check\" },\n commit: { cls: \"actor-engine\", dot: \"engine\", label: \"commit\" },\n unknown: { cls: \"actor-unknown\", dot: \"unknown\", label: \"?\" },\n }\n // A commit state has no actor — treat it as the engine/commit colour.\n const actorOf = (s) => s.actor || (s.kind === \"commit\" ? \"commit\" : \"unknown\")\n const meta = (a) => ACTOR[a] || ACTOR.unknown\n\n const FLAGS = {\n initial:\n \"The one initial state — an unrecognized HEAD (any non-gtd history) resolves here.\",\n reviewEntry:\n \"This state is a dedicated CLI entry point (e.g. `gtd review <ref>`) that starts a brand-new process.\",\n fixEntry:\n \"This state is the `gtd fix` entry point — a red suite drops into the fixing loop, a green suite is a no-op.\",\n reviewWindow:\n \"While resting here, gtd opens the review checkout window: HEAD is rewound to the review base so the whole diff surfaces as ordinary uncommitted changes.\",\n reviewBase:\n \"Anchors the incremental review diff base — its most-recent in-process commit is where the next reviewing turn and checkout window start.\",\n answerGate:\n \"Edge gate: a step is refused while any open question in the steering file is not answered with exactly one tick.\",\n requireProgress:\n \"Edge gate: refuses a work-free turn that only deletes the instructions file (unless it held a NOTHING ACTIONABLE sentinel).\",\n }\n\n // ── state ──────────────────────────────────────────────────────────────\n let MODEL = null\n let STATES = []\n let byName = {}\n let DIAGRAM_SRC = \"\"\n let mermaidMod = null\n let renderSeq = 0\n\n const actorFilter = new Set([\"human\", \"agent\", \"check\", \"commit\", \"unknown\"])\n let kindFilter = \"all\"\n\n // ── boot ─────────────────────────────────────────────────────────────\n try {\n const res = await fetch(\"/workflow.json\")\n if (!res.ok) throw new Error(\"HTTP \" + res.status)\n MODEL = await res.json()\n } catch (e) {\n document.getElementById(\"heroTitle\").textContent = \"Could not load workflow\"\n document.getElementById(\"heroLede\").innerHTML =\n \"Fetching <code>/workflow.json</code> failed: \" +\n esc(e.message) +\n '. This page must be served by <span class=\"mono\">gtd visualize</span>.'\n throw e\n }\n\n STATES = MODEL.states || []\n byName = Object.fromEntries(STATES.map((s) => [s.name, s]))\n\n // ── header summary + hero ───────────────────────────────────────────\n const counts = { human: 0, agent: 0, check: 0, commit: 0, unknown: 0 }\n STATES.forEach((s) => {\n counts[actorOf(s)]++\n })\n const nGroups = (MODEL.groups || []).length\n const cnt = (a) =>\n counts[a] ? `<span><span class=\"s-num\">${counts[a]}</span> ${meta(a).label}</span>` : \"\"\n document.getElementById(\"summary\").innerHTML = [\n `<span><span class=\"s-num\">${STATES.length}</span> states</span>`,\n `<span><span class=\"s-num\">${nGroups}</span> sub-machine${nGroups === 1 ? \"\" : \"s\"}</span>`,\n `<span class=\"sep\">·</span>`,\n cnt(\"human\"),\n cnt(\"agent\"),\n cnt(\"check\"),\n cnt(\"commit\"),\n cnt(\"unknown\"),\n ]\n .filter(Boolean)\n .join(\" \")\n\n document.getElementById(\"heroTitle\").innerHTML =\n `A workflow of <span class=\"mono\">${STATES.length}</span> states`\n document.getElementById(\"heroLede\").innerHTML =\n `Rendered live from <code>/workflow.json</code>. The initial state is ` +\n `<code>${esc(MODEL.initial)}</code>. Click any state — in the diagram, a sub-machine chip, ` +\n `or the catalogue — to open its full definition, walk its outgoing edges, and see what routes into it.`\n document.getElementById(\"content\").classList.remove(\"hidden\")\n\n document.getElementById(\"footer\").innerHTML =\n `Read-only view served by <code>gtd visualize</code>. Node shape = content kind ` +\n `(<span style=\"font-family:var(--mono)\">([message])</span> · ` +\n `<span style=\"font-family:var(--mono)\">[prompt]</span> · ` +\n `<span style=\"font-family:var(--mono)\">{{script}}</span> · ` +\n `<span style=\"font-family:var(--mono)\">[[commit]]</span>); colour = actor.`\n\n // ── pill helper ────────────────────────────────────────────────────\n const pill = (a) =>\n `<span class=\"pill ${meta(a).cls}\"><span class=\"dot\"></span>${meta(a).label}</span>`\n\n // ── mermaid source generation ──────────────────────────────────────\n // Safe, stable node ids (state names can contain hyphens etc.).\n const idFor = (() => {\n const map = new Map()\n return (name) => {\n if (!map.has(name)) map.set(name, \"n\" + map.size)\n return map.get(name)\n }\n })()\n const shapeFor = (kind, label) => {\n const l = `\"${label.replace(/\"/g, \"&quot;\")}\"`\n switch (kind) {\n case \"message\":\n return `([\"${label.replace(/\"/g, \"&quot;\")}\"])`\n case \"prompt\":\n return `[${l}]`\n case \"script\":\n return `{{${l}}}`\n case \"commit\":\n return `[[${l}]]`\n default:\n return `[${l}]`\n }\n }\n const mlabel = (s) => `\"${String(s).replace(/\"/g, \"&quot;\").replace(/\\|/g, \"&#124;\")}\"`\n\n function buildMermaid() {\n const lines = [\"flowchart TD\"]\n const rendered = new Set()\n const nodeLine = (s) => ` ${idFor(s.name)}${shapeFor(s.kind, s.name)}:::${actorOf(s)}`\n\n // Nodes grouped by their innermost sub-machine.\n const groups = MODEL.groups || []\n const byGroupName = {}\n STATES.forEach((s) => {\n if (s.group) (byGroupName[s.group] ||= []).push(s)\n })\n // Emit one subgraph per group (in declared order), containing only the\n // states whose innermost group is this one.\n groups.forEach((g, i) => {\n const members = byGroupName[g.name] || []\n if (!members.length) return\n lines.push(` subgraph sg${i}[${mlabel(g.name)}]`)\n members.forEach((s) => {\n lines.push(\" \" + nodeLine(s))\n rendered.add(s.name)\n })\n lines.push(\" end\")\n })\n // Ungrouped states sit outside any subgraph.\n STATES.forEach((s) => {\n if (!rendered.has(s.name)) {\n lines.push(nodeLine(s))\n rendered.add(s.name)\n }\n })\n\n // Edges: one per `on` entry, labelled with the pattern.\n STATES.forEach((s) => {\n ;(s.on || []).forEach((e) => {\n if (!byName[e.to]) return\n lines.push(` ${idFor(s.name)} -->|${mlabel(e.pattern)}| ${idFor(e.to)}`)\n })\n // Retry redirect (dashed) — informative, mirrors the \"routes in from\" list.\n if (s.retry && byName[s.retry.otherwise]) {\n lines.push(\n ` ${idFor(s.name)} -.->|${mlabel(\"retry ×\" + s.retry.max)}| ${idFor(s.retry.otherwise)}`,\n )\n }\n })\n\n lines.push(\n \"classDef human fill:#fbeccd,stroke:#d99413,color:#5a3d0a;\",\n \"classDef agent fill:#e5e2fb,stroke:#6d5ce0,color:#3a2c85;\",\n \"classDef check fill:#d3f0ef,stroke:#16a39d,color:#0c524f;\",\n \"classDef commit fill:#fbdfe4,stroke:#d84a68,color:#8a1f38;\",\n \"classDef unknown fill:#eef1f7,stroke:#c2ccdb,color:#55607a,stroke-dasharray:4 3;\",\n )\n return lines.join(\"\\n\")\n }\n\n DIAGRAM_SRC = buildMermaid()\n document.getElementById(\"diaNote\").innerHTML =\n `Every state as a node — shape is the content kind, colour is the actor. Each edge is an ` +\n `<span class=\"mono\">on</span> pattern (first match wins); dashed edges are ` +\n `<span class=\"mono\">retry</span> redirects. Sub-machines are drawn as labelled boxes. ` +\n `Scrolls horizontally if wide.`\n\n // ── diagram render (theme-aware, offline-tolerant) ─────────────────\n async function loadMermaid() {\n if (mermaidMod) return mermaidMod\n const m = await import(\"https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs\")\n mermaidMod = m.default\n return mermaidMod\n }\n async function renderDiagram() {\n const host = document.getElementById(\"diaScroll\")\n const seq = ++renderSeq\n let mermaid\n try {\n mermaid = await loadMermaid()\n } catch (e) {\n host.innerHTML =\n `<div class=\"dia-msg\">Could not load the Mermaid renderer (offline?). ` +\n `The workflow is still fully browsable below. Diagram source:` +\n `<pre class=\"dia-src\">${esc(DIAGRAM_SRC)}</pre></div>`\n return\n }\n if (seq !== renderSeq) return\n try {\n const dark = isDark()\n mermaid.initialize({\n startOnLoad: false,\n theme: \"base\",\n securityLevel: \"strict\",\n flowchart: { useMaxWidth: false, htmlLabels: true },\n themeVariables: {\n fontFamily: \"ui-monospace, Menlo, monospace\",\n fontSize: \"12.5px\",\n lineColor: dark ? \"#6b7690\" : \"#8a93a6\",\n primaryBorderColor: dark ? \"#3a465c\" : \"#c2ccdb\",\n tertiaryColor: dark ? \"#1c2434\" : \"#f0f3f8\",\n clusterBkg: dark ? \"#1c2434\" : \"#f0f3f8\",\n clusterBorder: dark ? \"#3a465c\" : \"#c2ccdb\",\n titleColor: dark ? \"#e6ebf4\" : \"#1a2233\",\n },\n })\n const { svg } = await mermaid.render(\"gtdflow\" + seq, DIAGRAM_SRC)\n if (seq !== renderSeq) return\n host.innerHTML = `<div class=\"mflow\">${svg}</div>`\n } catch (e) {\n host.innerHTML =\n `<div class=\"dia-msg\">Diagram render failed: ${esc(e.message)}. ` +\n `Diagram source:<pre class=\"dia-src\">${esc(DIAGRAM_SRC)}</pre></div>`\n }\n }\n renderDiagram()\n\n // ── sub-machines panel ──────────────────────────────────────────────\n ;(function renderSubmachines() {\n const el = document.getElementById(\"smSection\")\n const groups = MODEL.groups || []\n if (!groups.length) {\n el.innerHTML = `<p class=\"muted-note\">This workflow declares no sub-machines.</p>`\n return\n }\n const grid = document.createElement(\"div\")\n grid.className = \"sm-grid\"\n groups.forEach((g) => {\n const card = document.createElement(\"div\")\n card.className = \"sm-def\"\n const chips = (g.states || [])\n .map((n) => {\n const s = byName[n]\n const a = s ? actorOf(s) : \"unknown\"\n return `<button class=\"goto\" data-actor=\"${a}\" data-to=\"${esc(n)}\">${esc(n)}</button>`\n })\n .join(\"\")\n card.innerHTML =\n `<div class=\"h\"><span class=\"nm\">${esc(g.name)}</span>` +\n `<span class=\"pm\">instantiates <b>${esc(g.submachine)}</b> · ${(g.states || []).length} states</span></div>` +\n `<div class=\"body\">${chips}</div>`\n grid.appendChild(card)\n })\n el.innerHTML = \"\"\n el.appendChild(grid)\n el.querySelectorAll(\".goto\").forEach((b) => (b.onclick = () => openDrawer(b.dataset.to)))\n })()\n\n // ── vars ─────────────────────────────────────────────────────────────\n ;(function renderVars() {\n const vars = MODEL.vars || {}\n const keys = Object.keys(vars)\n if (!keys.length) return\n const el = document.getElementById(\"smSection\")\n const box = document.createElement(\"div\")\n box.innerHTML =\n `<div class=\"sec-head\" style=\"margin-top:34px\"><h2>Workflow variables</h2><span class=\"rule\"></span></div>` +\n `<div class=\"vars-grid\">${keys\n .map((k) => `<span class=\"var-chip\"><b>${esc(k)}</b> = ${esc(vars[k])}</span>`)\n .join(\"\")}</div>`\n el.appendChild(box)\n })()\n\n // ── kind chips + actor legend ──────────────────────────────────────\n const kindChipsEl = document.getElementById(\"kindChips\")\n const presentKinds = [\"all\", ...Array.from(new Set(STATES.map((s) => s.kind)))]\n presentKinds.forEach((k) => {\n const b = document.createElement(\"button\")\n b.className = \"chip-btn\" + (k === \"all\" ? \" on\" : \"\")\n b.textContent = k\n b.dataset.kind = k\n b.onclick = () => {\n kindFilter = k\n ;[...kindChipsEl.children].forEach((c) =>\n c.classList.toggle(\"on\", c.dataset.kind === kindFilter),\n )\n renderCatalogue()\n }\n kindChipsEl.appendChild(b)\n })\n\n const legendEl = document.getElementById(\"legend\")\n ;[\"human\", \"agent\", \"check\", \"commit\", \"unknown\"].forEach((a) => {\n if (!counts[a]) return\n const el = document.createElement(\"button\")\n el.className = \"li\"\n el.dataset.actor = a\n el.setAttribute(\"aria-pressed\", \"true\")\n el.innerHTML = `<span class=\"dot\" style=\"background:var(--${meta(a).dot}-dot)\"></span>${meta(a).label}`\n el.onclick = () => {\n const on = actorFilter.has(a)\n on ? actorFilter.delete(a) : actorFilter.add(a)\n el.classList.toggle(\"off\", on)\n el.setAttribute(\"aria-pressed\", String(!on))\n renderCatalogue()\n }\n legendEl.appendChild(el)\n })\n\n // ── catalogue ──────────────────────────────────────────────────────\n const groupsEl = document.getElementById(\"groups\")\n function stateMatches(s, q) {\n if (!actorFilter.has(actorOf(s))) return false\n if (kindFilter !== \"all\" && s.kind !== kindFilter) return false\n if (!q) return true\n const hay = [\n s.name,\n s.actor,\n s.kind,\n s.file,\n s.mode,\n s.model,\n s.memory,\n (s.flags || []).join(\" \"),\n ]\n .filter(Boolean)\n .join(\" \")\n .toLowerCase()\n return hay.includes(q)\n }\n function cardFor(s) {\n const a = actorOf(s)\n const card = document.createElement(\"button\")\n card.className = \"card\"\n card.dataset.actor = a\n const flags = (s.flags || [])\n .filter((f) => f !== \"initial\")\n .map((f) => `<span class=\"flag\">${esc(f)}</span>`)\n .join(\"\")\n const retry = s.retry ? `<span class=\"flag\">retry ${s.retry.max}</span>` : \"\"\n const initTick = s.initial ? `<div class=\"init-tick\">initial</div>` : \"\"\n card.innerHTML =\n `${initTick}<div class=\"name\">${esc(s.name)}</div>` +\n `<div class=\"row\">${pill(a)}<span class=\"kind\">${esc(s.kind)}</span>${retry}${flags}</div>`\n card.onclick = () => openDrawer(s.name)\n return card\n }\n function renderCatalogue() {\n const q = document.getElementById(\"search\").value.trim().toLowerCase()\n groupsEl.innerHTML = \"\"\n const sections = []\n ;(MODEL.groups || []).forEach((g) => {\n sections.push({\n label: g.name,\n flow: g.submachine,\n items: STATES.filter((s) => s.group === g.name),\n })\n })\n const ungrouped = STATES.filter((s) => !s.group)\n if (ungrouped.length) sections.push({ label: \"Ungrouped\", flow: \"\", items: ungrouped })\n\n let shown = 0\n sections.forEach((sec) => {\n const items = sec.items.filter((s) => stateMatches(s, q))\n if (!items.length) return\n shown += items.length\n const wrap = document.createElement(\"div\")\n wrap.className = \"group\"\n wrap.innerHTML =\n `<div class=\"group-head\">${sec.flow ? `<span class=\"flow-tag\">${esc(sec.flow)}</span>` : \"\"}` +\n `<h3>${esc(sec.label)}</h3></div>`\n const grid = document.createElement(\"div\")\n grid.className = \"grid\"\n items.forEach((s) => grid.appendChild(cardFor(s)))\n wrap.appendChild(grid)\n groupsEl.appendChild(wrap)\n })\n if (!shown)\n groupsEl.innerHTML = `<p class=\"none\" style=\"padding:20px 0\">No states match this filter.</p>`\n }\n document.getElementById(\"search\").addEventListener(\"input\", renderCatalogue)\n renderCatalogue()\n\n // ── drawer ─────────────────────────────────────────────────────────\n const drawer = document.getElementById(\"drawer\")\n const backdrop = document.getElementById(\"backdrop\")\n let lastFocus = null\n\n function openDrawer(name) {\n const s = byName[name]\n if (!s) return\n const a = actorOf(s)\n if (!drawer.classList.contains(\"open\")) lastFocus = document.activeElement\n\n const kv = []\n kv.push([\"actor\", pill(a)])\n kv.push([\"kind\", `<span class=\"kind\">${esc(s.kind)}</span>`])\n kv.push([\"model\", s.model ? esc(s.model) : '<span class=\"none\">—</span>'])\n kv.push([\"memory\", s.memory ? esc(s.memory) : '<span class=\"none\">—</span>'])\n kv.push([\"file\", s.file ? esc(s.file) : '<span class=\"none\">—</span>'])\n kv.push([\"mode\", s.mode ? esc(s.mode) : '<span class=\"none\">—</span>'])\n if (s.retry)\n kv.push([\n \"retry\",\n `max ${s.retry.max} → <span style=\"color:var(--accent)\">${esc(s.retry.otherwise)}</span>`,\n ])\n if (s.group) kv.push([\"group\", esc(s.group)])\n\n const flags = s.flags || []\n const flagBlock = flags.length\n ? `<div class=\"field\"><div class=\"k\">flags & gates</div><div class=\"flag-list\">${flags\n .map(\n (f) =>\n `<div class=\"flag-item\"><span class=\"fn\">${esc(f)}</span><span class=\"fd\">${esc(FLAGS[f] || \"\")}</span></div>`,\n )\n .join(\"\")}</div></div>`\n : \"\"\n\n const edges = s.on || []\n const edgesBlock = edges.length\n ? `<div class=\"field\"><div class=\"k\">outgoing edges — on (first match wins)</div><div class=\"edges\">${edges\n .map((e) => {\n const t = byName[e.to]\n const ta = t ? actorOf(t) : \"unknown\"\n return (\n `<div class=\"edge\"><div class=\"er\"><span class=\"pat\">${esc(e.pattern)}</span><span class=\"arr\">→</span>` +\n `<button class=\"goto\" data-actor=\"${ta}\" data-to=\"${esc(e.to)}\">${esc(e.to)}</button></div>` +\n `${e.describe ? `<div class=\"em\">${esc(e.describe)}</div>` : \"\"}</div>`\n )\n })\n .join(\"\")}</div></div>`\n : `<div class=\"field\"><div class=\"k\">outgoing edges</div><p class=\"none\">${\n s.kind === \"commit\"\n ? \"Commit state — no outgoing edges. Entering it ends the process in one squash.\"\n : \"No outgoing edges declared.\"\n }</p></div>`\n\n const retryOut =\n s.retry && byName[s.retry.otherwise]\n ? `<div class=\"field\"><div class=\"k\">retry redirect</div><div class=\"edge\"><div class=\"er\">` +\n `<span class=\"pat\">after ${esc(s.retry.max)} tries</span><span class=\"arr\">→</span>` +\n `<button class=\"goto\" data-actor=\"${actorOf(byName[s.retry.otherwise])}\" data-to=\"${esc(s.retry.otherwise)}\">${esc(s.retry.otherwise)}</button>` +\n `</div></div></div>`\n : \"\"\n\n const inc = s.incoming || []\n const incBlock =\n `<div class=\"field\"><div class=\"k\">routes in from (${inc.length})</div>` +\n (inc.length\n ? `<div class=\"incoming\">${inc\n .map((i) => {\n const f = byName[i.from]\n return `<button class=\"goto\" data-actor=\"${f ? actorOf(f) : \"unknown\"}\" data-to=\"${esc(i.from)}\" title=\"${esc(i.pattern)}\">${esc(i.from)}</button>`\n })\n .join(\"\")}</div>`\n : `<p class=\"none\">Reached only as an entry point (a CLI command enters it directly).</p>`) +\n `</div>`\n\n drawer.style.setProperty(\"--card-accent\", `var(--${meta(a).dot}-dot)`)\n drawer.innerHTML =\n `<div class=\"drawer-head\" style=\"--card-accent:var(--${meta(a).dot}-dot)\">` +\n `<div class=\"top\"><h3>${esc(s.name)}</h3>` +\n `<button class=\"close-x\" id=\"closeX\" aria-label=\"Close detail\">✕</button></div>` +\n `<div class=\"meta-row\">${pill(a)}<span class=\"kind\">${esc(s.kind)}</span>` +\n `${s.initial ? '<span class=\"flag\">initial</span>' : \"\"}` +\n `${s.retry ? `<span class=\"flag\">retry ${s.retry.max}</span>` : \"\"}` +\n `${flags\n .filter((f) => f !== \"initial\")\n .map((f) => `<span class=\"flag\">${esc(f)}</span>`)\n .join(\"\")}</div></div>` +\n `<div class=\"drawer-body\">` +\n `<div class=\"field\"><div class=\"k\">definition</div><dl class=\"kv\">${kv\n .map(([k, v]) => `<dt>${esc(k)}</dt><dd>${v}</dd>`)\n .join(\"\")}</dl></div>` +\n flagBlock +\n edgesBlock +\n retryOut +\n incBlock +\n `</div>`\n\n drawer\n .querySelectorAll(\".goto\")\n .forEach((b) => (b.onclick = () => openDrawer(b.dataset.to)))\n drawer.querySelector(\"#closeX\").onclick = closeDrawer\n\n backdrop.classList.add(\"open\")\n drawer.classList.add(\"open\")\n drawer.querySelector(\".drawer-body\").scrollTop = 0\n drawer.querySelector(\"#closeX\").focus()\n }\n function closeDrawer() {\n backdrop.classList.remove(\"open\")\n drawer.classList.remove(\"open\")\n if (lastFocus && lastFocus.focus) lastFocus.focus()\n }\n backdrop.onclick = closeDrawer\n document.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Escape\" && drawer.classList.contains(\"open\")) closeDrawer()\n })\n\n // expose for interactions\n window.__openState = openDrawer\n <\/script>\n </body>\n</html>\n";
204893
+ //#endregion
204894
+ //#region src/Visualize.ts
204895
+ const FLAG_KEYS = [
204896
+ "reviewWindow",
204897
+ "reviewBase",
204898
+ "reviewEntry",
204899
+ "fixEntry",
204900
+ "requireProgress",
204901
+ "answerGate"
204902
+ ];
204903
+ const flagsOf = (def) => FLAG_KEYS.filter((k) => def[k] === true);
204904
+ const edgeToViz = ([pattern, to, describe]) => describe !== void 0 ? {
204905
+ pattern,
204906
+ to,
204907
+ describe
204908
+ } : {
204909
+ pattern,
204910
+ to
204911
+ };
204912
+ /** Drop keys whose value is `undefined` (so `exactOptionalPropertyTypes` optionals stay absent, not `undefined`). */
204913
+ const stripUndefined = (o) => {
204914
+ for (const key of Object.keys(o)) if (o[key] === void 0) delete o[key];
204915
+ return o;
204916
+ };
204917
+ /** Describe one compiled state for the viewer (optional fields omitted when unset). */
204918
+ const toVizState = (name, def, group, incoming) => stripUndefined({
204919
+ name,
204920
+ actor: def.actor,
204921
+ kind: contentKindOf(def) ?? "unknown",
204922
+ initial: def.initial === true ? true : void 0,
204923
+ model: def.model,
204924
+ memory: def.memory,
204925
+ file: def.file,
204926
+ mode: def.mode,
204927
+ retry: def.retry,
204928
+ flags: flagsOf(def),
204929
+ on: (def.on ?? []).map(edgeToViz),
204930
+ incoming,
204931
+ group
204932
+ });
204933
+ /**
204934
+ * Build the viewer's JSON description from the active COMPILED workflow plus its
204935
+ * RAW value (the pre-expansion `submachines:`/`use:` form — `rawWorkflow` from
204936
+ * `ConfigService`), which is the only place the sub-machine grouping survives
204937
+ * (`compileWorkflowConfig` flattens it away). `vars` is shown for reference.
204938
+ */
204939
+ const buildVizModel = (workflow, rawWorkflow, vars) => {
204940
+ const groups = collectGroups(rawWorkflow);
204941
+ const sizeOf = new Map(groups.map((g) => [g.name, g.states.length]));
204942
+ const groupOf = /* @__PURE__ */ new Map();
204943
+ for (const g of groups) for (const s of g.states) {
204944
+ const cur = groupOf.get(s);
204945
+ if (cur === void 0 || sizeOf.get(g.name) < sizeOf.get(cur)) groupOf.set(s, g.name);
204946
+ }
204947
+ const incoming = /* @__PURE__ */ new Map();
204948
+ const addIncoming = (target, from, pattern) => {
204949
+ const list = incoming.get(target) ?? [];
204950
+ list.push({
204951
+ from,
204952
+ pattern
204953
+ });
204954
+ incoming.set(target, list);
204955
+ };
204956
+ for (const [name, def] of Object.entries(workflow.states)) {
204957
+ for (const [pattern, to] of def.on ?? []) addIncoming(to, name, pattern);
204958
+ if (def.retry) addIncoming(def.retry.otherwise, name, `retry ×${def.retry.max}`);
204491
204959
  }
204492
- return lines.join("\n") + "\n";
204960
+ return {
204961
+ states: Object.entries(workflow.states).map(([name, def]) => toVizState(name, def, groupOf.get(name), incoming.get(name) ?? [])),
204962
+ initial: initialStateOf(workflow),
204963
+ groups: groups.map((g) => ({
204964
+ name: g.name,
204965
+ submachine: g.submachine,
204966
+ states: [...g.states]
204967
+ })),
204968
+ vars
204969
+ };
204970
+ };
204971
+ /**
204972
+ * Route one request (pure): `/` (or `/index.html`) serves the viewer page,
204973
+ * `/workflow.json` serves the model, anything else is a 404. Kept separate from
204974
+ * the server so it unit-tests without a socket.
204975
+ */
204976
+ const handleVizRequest = (pathname, model) => {
204977
+ if (pathname === "/" || pathname === "/index.html") return {
204978
+ status: 200,
204979
+ contentType: "text/html; charset=utf-8",
204980
+ body: visualize_default
204981
+ };
204982
+ if (pathname === "/workflow.json") return {
204983
+ status: 200,
204984
+ contentType: "application/json; charset=utf-8",
204985
+ body: JSON.stringify(model)
204986
+ };
204987
+ return {
204988
+ status: 404,
204989
+ contentType: "text/plain; charset=utf-8",
204990
+ body: "not found"
204991
+ };
204992
+ };
204993
+ /**
204994
+ * Start the viewer's HTTP server, resolving once it is listening with the
204995
+ * chosen URL (a `port` of `0` picks a free port). Rejects if the port is in
204996
+ * use. The caller owns the returned `server` (closes it on shutdown).
204997
+ */
204998
+ const startVizServer = (model, port, host = "127.0.0.1") => new Promise((resolve, reject) => {
204999
+ const server = createServer((req, res) => {
205000
+ const { pathname } = new URL(req.url ?? "/", "http://localhost");
205001
+ const { status, contentType, body } = handleVizRequest(pathname, model);
205002
+ res.writeHead(status, { "content-type": contentType });
205003
+ res.end(body);
205004
+ });
205005
+ server.once("error", reject);
205006
+ server.listen(port, host, () => {
205007
+ const address = server.address();
205008
+ const chosen = typeof address === "object" && address !== null ? address.port : port;
205009
+ resolve({
205010
+ server,
205011
+ url: `http://${host}:${chosen}`
205012
+ });
205013
+ });
205014
+ });
205015
+ /** Best-effort open a URL in the default browser (macOS `open`, Windows `start`, else `xdg-open`); failures are ignored — the URL is always printed too. */
205016
+ const openInBrowser = (url) => {
205017
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
205018
+ try {
205019
+ const child = spawn(cmd, [url], {
205020
+ stdio: "ignore",
205021
+ detached: true,
205022
+ shell: process.platform === "win32"
205023
+ });
205024
+ child.on("error", () => {});
205025
+ child.unref();
205026
+ } catch {}
204493
205027
  };
204494
205028
  //#endregion
204495
205029
  //#region src/SteeringMode.ts
@@ -204633,6 +205167,11 @@ Commands:
204633
205167
  state (fixEntry: true) that goes straight into repairing the
204634
205168
  current failing tests. Requires a clean tree resting at the
204635
205169
  workflow's initial state
205170
+ abandon End the process currently underway without completing it:
205171
+ close any open review checkout window, then rewind HEAD to
205172
+ the commit the process started from, keeping everything it
205173
+ produced as uncommitted changes. A no-op when no process is
205174
+ underway
204636
205175
  next Print the resolved rest's rendered script/prompt/message
204637
205176
  (no mutation)
204638
205177
  status Print the resolved rest's state/actor and which declared
@@ -204640,14 +205179,17 @@ Commands:
204640
205179
  validate Format and validate the steering file the resolved rest
204641
205180
  declares, with its mode's commands (its file:/mode:);
204642
205181
  exits non-zero with the findings when it is invalid
204643
- mermaid Print the active workflow's shape as Mermaid
204644
- stateDiagram-v2 source (no mutation)
204645
205182
  lsp Start the LSP server for .gtd/ steering files (stdio)
205183
+ visualize Serve an interactive diagram of the active workflow on a
205184
+ local web server (--port <n>, --no-open; --json prints the
205185
+ model and exits)
204646
205186
  version Print version and exit
204647
205187
  help Print this help and exit
204648
205188
 
204649
205189
  Options:
204650
205190
  --json Output structured JSON instead of plain text
205191
+ --port=<n> (gtd visualize only) port to serve on (default: a free port)
205192
+ --no-open (gtd visualize only) do not open the browser
204651
205193
  --cost=<n> (gtd step only) record the invocation's token cost
204652
205194
  --model=<name> (gtd step only, with --cost) tag that cost's model
204653
205195
  --version, -v Print version and exit
@@ -204893,7 +205435,7 @@ const runReviewCommand = (argv, json, write) => gen(function* () {
204893
205435
  const commitish = args[0];
204894
205436
  const git = yield* GitService;
204895
205437
  const rest = yield* resolveRest();
204896
- if (rest.state !== initialStateOf(rest.def)) return yield* fail$2(/* @__PURE__ */ new Error(`gtd review: a process is already underway (resting at "${rest.state}") — finish or abandon it before starting a review`));
205438
+ if (rest.state !== initialStateOf(rest.def)) return yield* fail$2(/* @__PURE__ */ new Error(`gtd review: a process is already underway (resting at "${rest.state}") — finish it, or run \`gtd abandon\`, before starting a review`));
204897
205439
  if ((yield* pendingChanges(git)).length > 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd review: the working tree must be clean before starting a review"));
204898
205440
  const entryState = reviewEntryStateOf(rest.def);
204899
205441
  if (entryState === void 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd review: the active workflow declares no review entry state"));
@@ -204930,7 +205472,7 @@ const runFixCommand = (argv, json, write) => gen(function* () {
204930
205472
  yield* rejectExtraArgs("fix", argv);
204931
205473
  const git = yield* GitService;
204932
205474
  const rest = yield* resolveRest();
204933
- if (rest.state !== initialStateOf(rest.def)) return yield* fail$2(/* @__PURE__ */ new Error(`gtd fix: a process is already underway (resting at "${rest.state}") — finish or abandon it before starting a fix`));
205475
+ if (rest.state !== initialStateOf(rest.def)) return yield* fail$2(/* @__PURE__ */ new Error(`gtd fix: a process is already underway (resting at "${rest.state}") — finish it, or run \`gtd abandon\`, before starting a fix`));
204934
205476
  if ((yield* pendingChanges(git)).length > 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd fix: the working tree must be clean before starting a fix"));
204935
205477
  const entryState = fixEntryStateOf(rest.def);
204936
205478
  if (entryState === void 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd fix: the active workflow declares no fix entry state"));
@@ -204942,6 +205484,53 @@ const runFixCommand = (argv, json, write) => gen(function* () {
204942
205484
  }) + "\n");
204943
205485
  else write(`committed: ${subject}\n`);
204944
205486
  });
205487
+ const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
205488
+ /**
205489
+ * `gtd abandon`: end the process currently underway WITHOUT completing it,
205490
+ * returning the machine to the workflow's initial state — the recovery path out
205491
+ * of a process nobody is going to finish (the refusals of `gtd review`/`gtd fix`
205492
+ * name it: "finish or abandon it before starting a review").
205493
+ *
205494
+ * NOTHING is discarded. The shared bracket in `makeProgram` has already closed
205495
+ * any open review checkout window (so HEAD is the real head), and abandon then
205496
+ * `git reset --mixed`es HEAD to the commit the process started from
205497
+ * (`computeProcessRun`'s `startParentHash` — the same boundary a squash resets
205498
+ * to). Every turn commit the process wrote is dropped, and everything they
205499
+ * carried — the code, the `.gtd/` steering files — stays in the working tree as
205500
+ * uncommitted changes for the human to keep, re-commit, or discard with
205501
+ * ordinary git.
205502
+ *
205503
+ * Idempotent: resting at the initial state (a plain non-gtd branch, or a
205504
+ * just-squashed cycle) is a no-op SUCCESS, not a refusal — a recovery command
205505
+ * that fails when there is nothing to recover is a worse tool. The one refusal
205506
+ * is a process whose first commit is the repository's own root commit: there is
205507
+ * no earlier commit to rewind to.
205508
+ */
205509
+ const runAbandonCommand = (argv, json, write) => gen(function* () {
205510
+ yield* rejectExtraArgs("abandon", argv);
205511
+ const git = yield* GitService;
205512
+ const rest = yield* resolveRest();
205513
+ const initial = initialStateOf(rest.def);
205514
+ if (rest.state === initial) {
205515
+ if (json) write(JSON.stringify({
205516
+ state: initial,
205517
+ abandoned: false
205518
+ }) + "\n");
205519
+ else write(`no gtd process is underway (resting at "${initial}") — nothing to abandon\n`);
205520
+ return;
205521
+ }
205522
+ const run = yield* computeProcessRun(git, rest.def);
205523
+ if (run.startParentHash === EMPTY_TREE) return yield* fail$2(/* @__PURE__ */ new Error(`gtd abandon: the process underway (resting at "${rest.state}") starts at the repository's first commit — there is no earlier commit to rewind to`));
205524
+ yield* git.mixedResetTo(run.startParentHash);
205525
+ const subject = yield* git.lastCommitSubject();
205526
+ if (json) write(JSON.stringify({
205527
+ state: initial,
205528
+ abandoned: true,
205529
+ from: rest.state,
205530
+ head: run.startParentHash
205531
+ }) + "\n");
205532
+ else write(`abandoned the process resting at "${rest.state}" — HEAD is back at ${run.startParentHash.slice(0, 7)} ("${subject}"), resting at "${initial}".\nEverything the process produced is kept as uncommitted changes (\`git status\`); discard them with \`git checkout -- . && git clean -fd .gtd\` for a clean tree.\n`);
205533
+ });
204945
205534
  /**
204946
205535
  * The self-validation instruction gtd APPENDS to a `prompt` rest that declares
204947
205536
  * both `file:` and `mode:` — i.e. a state whose actor hands over a steering
@@ -205289,29 +205878,91 @@ const runStatusCommand = (argv, json, write) => gen(function* () {
205289
205878
  if (json) writeStatusJson(write, rest, statusChanges, model, memory, file, context.processCost, context.processCostByModel);
205290
205879
  else writeStatusPlain(write, rest, statusChanges, model, memory, file, context.processCost, context.processCostByModel);
205291
205880
  });
205881
+ /** Parse a `--port` value: an integer 0–65535, or `undefined` if invalid/absent. */
205882
+ const parsePort = (value) => {
205883
+ const n = Number(value);
205884
+ return value !== void 0 && Number.isInteger(n) && n >= 0 && n <= 65535 ? n : void 0;
205885
+ };
205886
+ /** Resolve a `--port`/`--port=<n>` argument to its raw value + the index consumed, or `null` if `arg` is not a port option. */
205887
+ const portArgValue = (arg, rest, i) => {
205888
+ if (arg === "--port") return {
205889
+ value: rest[i + 1],
205890
+ nextIndex: i + 1
205891
+ };
205892
+ if (arg.startsWith("--port=")) return {
205893
+ value: arg.slice(7),
205894
+ nextIndex: i
205895
+ };
205896
+ return null;
205897
+ };
205898
+ /**
205899
+ * Parse `gtd visualize`'s own options from the argv tail (the subcommand token
205900
+ * already dropped): `--port <n>`/`--port=<n>`, `--no-open`, and a passthrough
205901
+ * `--json`. Returns the options, or an `{ error }` message for an invalid port,
205902
+ * an unknown `--` option, or an unexpected positional.
205903
+ */
205904
+ const parseVisualizeOptions = (rest) => {
205905
+ let port = 0;
205906
+ let open = true;
205907
+ for (let i = 0; i < rest.length; i++) {
205908
+ const arg = rest[i];
205909
+ const portArg = portArgValue(arg, rest, i);
205910
+ if (portArg !== null) {
205911
+ const parsed = parsePort(portArg.value);
205912
+ if (parsed === void 0) return { error: `gtd visualize: --port must be an integer 0–65535 (got '${portArg.value ?? ""}')` };
205913
+ port = parsed;
205914
+ i = portArg.nextIndex;
205915
+ continue;
205916
+ }
205917
+ if (arg === "--json") continue;
205918
+ if (arg === "--no-open") open = false;
205919
+ else if (arg.startsWith("--")) return { error: `gtd: unknown option '${arg}' — see \`gtd --help\`` };
205920
+ else return { error: `gtd visualize: unexpected argument '${arg}'` };
205921
+ }
205922
+ return {
205923
+ port,
205924
+ open
205925
+ };
205926
+ };
205292
205927
  /**
205293
- * `gtd mermaid`: pure emitter of the active workflow's SHAPE (not the
205294
- * resolved rest) as Mermaid `stateDiagram-v2` source — see `src/Mermaid.ts`.
205295
- * Needs only `ConfigService` (no HEAD resolution, no rendering), but is
205296
- * dispatched alongside `next`/`status` since it still depends on the active
205297
- * `.gtdrc` — unlike `format`/`lsp`, which need neither git nor config — so it
205298
- * goes through the same repository-root guard and auto-init. Rejects
205299
- * `--json`: there is no structured shape to emit beyond the Mermaid source
205300
- * itself.
205301
- */
205302
- const runMermaidCommand = (argv, json, write) => gen(function* () {
205303
- if (json) return yield* fail$2(/* @__PURE__ */ new Error("gtd mermaid does not accept --json"));
205304
- yield* rejectExtraArgs("mermaid", argv);
205305
- write(renderMermaid((yield* (yield* ConfigService).load).workflow));
205928
+ * `gtd visualize`: serve an interactive diagram of the ACTIVE workflow on a
205929
+ * local HTTP server (see `src/Visualize.ts`). Dispatched early (like `lsp`/
205930
+ * `init`) — it reads the config but never touches git, HEAD, or the review
205931
+ * window — and parses its OWN options (`--port`, `--no-open`), since the global
205932
+ * unknown-option check does not know them. `--json` prints the model and exits
205933
+ * without starting a server (the testable path).
205934
+ */
205935
+ const runVisualizeCommand = (argv, json, write) => gen(function* () {
205936
+ const tokens = argv.slice(2);
205937
+ const subIdx = tokens.indexOf("visualize");
205938
+ const rest = subIdx >= 0 ? [...tokens.slice(0, subIdx), ...tokens.slice(subIdx + 1)] : tokens;
205939
+ const opts = parseVisualizeOptions(rest);
205940
+ if ("error" in opts) return yield* fail$2(new Error(opts.error));
205941
+ const config = yield* (yield* ConfigService).load;
205942
+ const model = buildVizModel(config.workflow, config.rawWorkflow, {
205943
+ ...config.workflowVars,
205944
+ ...config.rcVars
205945
+ });
205946
+ if (json) {
205947
+ write(JSON.stringify(model, null, 2) + "\n");
205948
+ return;
205949
+ }
205950
+ const { server, url } = yield* tryPromise({
205951
+ try: () => startVizServer(model, opts.port),
205952
+ catch: (e) => /* @__PURE__ */ new Error(`gtd visualize: could not start server: ${e instanceof Error ? e.message : String(e)}`)
205953
+ });
205954
+ write(`gtd visualize running at ${url} — Ctrl-C to stop\n`);
205955
+ if (opts.open) openInBrowser(url);
205956
+ yield* never.pipe(ensuring$2(sync$1(() => server.close())));
205306
205957
  });
205307
205958
  const KNOWN_SUBCOMMANDS = [
205308
205959
  "step",
205309
205960
  "review",
205310
205961
  "fix",
205962
+ "abandon",
205311
205963
  "next",
205312
205964
  "status",
205313
- "validate",
205314
- "mermaid"
205965
+ "validate"
205315
205966
  ];
205316
205967
  /**
205317
205968
  * `--version`/`-v`/`version` or `--help`/`-h`/`help`: short-circuits before any
@@ -205380,10 +206031,10 @@ const dispatchKnownSubcommand = (sub, argv, json, write, cost, model) => {
205380
206031
  case "step": return runStepCommand(argv, json, write, cost, model);
205381
206032
  case "review": return runReviewCommand(argv, json, write);
205382
206033
  case "fix": return runFixCommand(argv, json, write);
206034
+ case "abandon": return runAbandonCommand(argv, json, write);
205383
206035
  case "next": return runNextCommand(json, write);
205384
206036
  case "status": return runStatusCommand(argv, json, write);
205385
206037
  case "validate": return runValidateCommand(argv, json, write);
205386
- case "mermaid": return runMermaidCommand(argv, json, write);
205387
206038
  }
205388
206039
  };
205389
206040
  /**
@@ -205395,7 +206046,7 @@ const dispatchKnownSubcommand = (sub, argv, json, write, cost, model) => {
205395
206046
  * captures stdout via the `write` callback.
205396
206047
  *
205397
206048
  * v3 command surface: `step <actor>` / `review <commitish>` / `fix` / `next` /
205398
- * `status` / `validate` / `mermaid` (see `src/Edge.ts` and
206049
+ * `status` / `validate` (see `src/Edge.ts` and
205399
206050
  * `docs/design/pattern-machine-plan.md` §3),
205400
206051
  * plus `lsp` and `init` — both dispatched before the config-reading
205401
206052
  * path since neither needs to touch the config. Bare `gtd` or an
@@ -205410,6 +206061,7 @@ function makeProgram(opts = {}) {
205410
206061
  const positional = argv.slice(2).find((a) => !a.startsWith("--"));
205411
206062
  return gen(function* () {
205412
206063
  if (runVersionOrHelp(argv, write)) return;
206064
+ if (positional === "visualize") return yield* runVisualizeCommand(argv, json, write);
205413
206065
  const unknownOption = argv.slice(2).find((a) => a.startsWith("--") && a !== "--json" && a !== "--cost" && a !== "--model" && !a.startsWith(COST_FLAG) && !a.startsWith(MODEL_FLAG));
205414
206066
  if (unknownOption !== void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd: unknown option '${unknownOption}' — see \`gtd --help\``));
205415
206067
  const { cost, model } = yield* parseStepFlags(argv, positional);