@pmelab/gtd 6.14.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -19
- package/dist/gtd.bundle.mjs +850 -322
- package/docs/cli.md +69 -72
- package/docs/configuration.md +120 -57
- package/docs/development.md +2 -1
- package/docs/upgrading.md +23 -21
- package/package.json +1 -1
- package/schema.json +180 -0
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -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
|
|
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(
|
|
190167
|
-
const modes = mergeModes(compileModesMap(
|
|
190168
|
-
const rawStates =
|
|
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));
|
|
@@ -190185,6 +190524,90 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true) =
|
|
|
190185
190524
|
};
|
|
190186
190525
|
};
|
|
190187
190526
|
//#endregion
|
|
190527
|
+
//#region src/workflows/unified.yaml
|
|
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";
|
|
190529
|
+
//#endregion
|
|
190530
|
+
//#region src/workflows/templates.ts
|
|
190531
|
+
/**
|
|
190532
|
+
* The `$schema` URL editors resolve for `.gtdrc.json` completion/validation —
|
|
190533
|
+
* written as the first key of every `gtd init` config (see `renderInitScaffold`).
|
|
190534
|
+
*/
|
|
190535
|
+
const SCHEMA_URL = "https://raw.githubusercontent.com/pmelab/gtd/main/schema.json";
|
|
190536
|
+
/**
|
|
190537
|
+
* The default `vars:` `gtd init` seeds into the scaffolded `.gtdrc.json` — the
|
|
190538
|
+
* one variable a fresh project almost always changes. It mirrors the bundled
|
|
190539
|
+
* workflow's OWN `testCommand` default (`npm test`), surfaced at the top level
|
|
190540
|
+
* as a ready-to-edit override (the top-level `vars:` layer wins over the
|
|
190541
|
+
* workflow's own — see `src/Edge.ts`'s `resolveVars`). Everything else the
|
|
190542
|
+
* workflow needs already ships inside the built-in default, so there is nothing
|
|
190543
|
+
* else to seed.
|
|
190544
|
+
*/
|
|
190545
|
+
const INIT_VARS = { testCommand: "npm test" };
|
|
190546
|
+
/**
|
|
190547
|
+
* The top-level `modes:` block `gtd init` seeds into the scaffolded
|
|
190548
|
+
* `.gtdrc.json` as a ready-to-edit suggestion: a `format:` command for each
|
|
190549
|
+
* built-in steering-file mode the bundled default uses (`qa` for the plan /
|
|
190550
|
+
* open-questions files, `review` for REVIEW.md), so a fresh project's steering
|
|
190551
|
+
* files are auto-formatted with Prettier before gtd validates them. Only
|
|
190552
|
+
* `format:` is declared — gtd's built-in `qa`/`review` validators still do the
|
|
190553
|
+
* validating (the two halves layer independently — see STATES.md §12 /
|
|
190554
|
+
* docs/configuration.md `modes:`). gtd ships no formatter, so this is the one
|
|
190555
|
+
* place a default is suggested; a project edits or drops it freely (swap
|
|
190556
|
+
* Prettier for dprint, point at a script, delete the key).
|
|
190557
|
+
*/
|
|
190558
|
+
const MODES_SUGGESTION = {
|
|
190559
|
+
qa: { format: "npx prettier --write <%= it.file %>" },
|
|
190560
|
+
review: { format: "npx prettier --write <%= it.file %>" }
|
|
190561
|
+
};
|
|
190562
|
+
/**
|
|
190563
|
+
* The single bundled workflow template — the raw YAML text of `unified.yaml`,
|
|
190564
|
+
* imported as a string (tsdown's `.yaml` text loader / the vitest `rawMd`
|
|
190565
|
+
* transform — see tsdown.config.ts / tests/vitest.rawMd.ts), so this module
|
|
190566
|
+
* never touches the filesystem: it works identically in the dev checkout,
|
|
190567
|
+
* under `vitest`, and inside the single-file `dist/gtd.bundle.mjs` build. gtd
|
|
190568
|
+
* ships this as its BUILT-IN DEFAULT — a repo with no `workflow:` configured
|
|
190569
|
+
* runs it directly (see `src/Config.ts`'s `toOperations`), so `gtd init` no
|
|
190570
|
+
* longer writes the workflow into the config at all.
|
|
190571
|
+
*/
|
|
190572
|
+
const UNIFIED_WORKFLOW = unified_default;
|
|
190573
|
+
/**
|
|
190574
|
+
* The bundled default, compiled once through the exact same
|
|
190575
|
+
* `compileWorkflowConfig` a user's `.gtdrc` `workflow:` key goes through — no
|
|
190576
|
+
* privileged code path. `src/Config.ts` (and the in-memory test layer) fall
|
|
190577
|
+
* back to this whenever no `workflow:` key is configured; `compileTemplate`
|
|
190578
|
+
* exposes it fresh for tests. `configDir` is `"."` and
|
|
190579
|
+
* never consulted: no template content value starts with `./`/`../`.
|
|
190580
|
+
*/
|
|
190581
|
+
const DEFAULT_WORKFLOW = compileWorkflowConfig((0, import_dist$1.parse)(UNIFIED_WORKFLOW), ".");
|
|
190582
|
+
/** The compiled built-in default workflow definition — the fallback when no `workflow:` is configured. */
|
|
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);
|
|
190592
|
+
/** The built-in default workflow's own declared `vars:` defaults (layer 1 of the merged `it.vars`). */
|
|
190593
|
+
const defaultWorkflowVars = DEFAULT_WORKFLOW.vars;
|
|
190594
|
+
/**
|
|
190595
|
+
* Render what `gtd init` writes: a minimal `.gtdrc.json` seeding the default
|
|
190596
|
+
* variables (`vars.testCommand`) and a ready-to-edit Prettier formatting
|
|
190597
|
+
* suggestion (`modes:`). It carries NO `workflow:` key — gtd ships the unified
|
|
190598
|
+
* workflow as its built-in default and runs it whenever none is configured
|
|
190599
|
+
* (see `src/Config.ts`). A project that wants to customize the machine itself
|
|
190600
|
+
* adds a `workflow:` key (materialize the default with `renderInitConfig` as a
|
|
190601
|
+
* starting point); everyday tuning is just editing the seeded `vars:`/`modes:`.
|
|
190602
|
+
*/
|
|
190603
|
+
const renderInitScaffold = () => {
|
|
190604
|
+
return { config: JSON.stringify({
|
|
190605
|
+
$schema: SCHEMA_URL,
|
|
190606
|
+
vars: INIT_VARS,
|
|
190607
|
+
modes: MODES_SUGGESTION
|
|
190608
|
+
}, null, 2) + "\n" };
|
|
190609
|
+
};
|
|
190610
|
+
//#endregion
|
|
190188
190611
|
//#region src/Cwd.ts
|
|
190189
190612
|
var Cwd = class Cwd extends Tag("Cwd")() {
|
|
190190
190613
|
static layer = (root) => succeed$2(Cwd, { root });
|
|
@@ -190259,6 +190682,117 @@ const modesJsonSchema = {
|
|
|
190259
190682
|
}
|
|
190260
190683
|
}
|
|
190261
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
|
+
};
|
|
190262
190796
|
/** The whole `workflow:` value — see `PatternConfig.ts`'s module docstring for the authoritative schema. */
|
|
190263
190797
|
const workflowJsonSchema = {
|
|
190264
190798
|
type: "object",
|
|
@@ -190268,116 +190802,67 @@ const workflowJsonSchema = {
|
|
|
190268
190802
|
properties: {
|
|
190269
190803
|
vars: varsJsonSchema,
|
|
190270
190804
|
modes: modesJsonSchema,
|
|
190271
|
-
|
|
190805
|
+
submachines: {
|
|
190272
190806
|
type: "object",
|
|
190273
|
-
description: "
|
|
190274
|
-
minProperties: 1,
|
|
190807
|
+
description: "Optional reusable sub-machines, invoked by `use:` and expanded into concrete states at load time (see src/Submachines.ts).",
|
|
190275
190808
|
additionalProperties: {
|
|
190276
190809
|
type: "object",
|
|
190277
|
-
description: "
|
|
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.",
|
|
190278
190811
|
additionalProperties: false,
|
|
190812
|
+
required: ["states"],
|
|
190279
190813
|
properties: {
|
|
190280
|
-
|
|
190281
|
-
type: "
|
|
190282
|
-
|
|
190283
|
-
|
|
190284
|
-
script: {
|
|
190285
|
-
type: "string",
|
|
190286
|
-
description: "Content kind: a shell script (Eta template). The loop driver executes it verbatim via bash, then steps this state's actor."
|
|
190287
|
-
},
|
|
190288
|
-
prompt: {
|
|
190289
|
-
type: "string",
|
|
190290
|
-
description: "Content kind: an agent prompt (Eta template), emitted by `gtd next`."
|
|
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."
|
|
190291
190818
|
},
|
|
190292
|
-
|
|
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: {
|
|
190293
190837
|
type: "string",
|
|
190294
|
-
description: "
|
|
190838
|
+
description: "Name of a declared sub-machine."
|
|
190295
190839
|
},
|
|
190296
|
-
|
|
190840
|
+
name: {
|
|
190297
190841
|
type: "string",
|
|
190298
|
-
description: "
|
|
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."
|
|
190299
190843
|
},
|
|
190300
|
-
|
|
190844
|
+
as: {
|
|
190301
190845
|
type: "object",
|
|
190302
|
-
description: "
|
|
190303
|
-
additionalProperties: {
|
|
190304
|
-
type: "string",
|
|
190305
|
-
description: "The target state name."
|
|
190306
|
-
}, {
|
|
190307
|
-
type: "object",
|
|
190308
|
-
additionalProperties: false,
|
|
190309
|
-
required: ["to"],
|
|
190310
|
-
properties: {
|
|
190311
|
-
to: {
|
|
190312
|
-
type: "string",
|
|
190313
|
-
description: "The target state name."
|
|
190314
|
-
},
|
|
190315
|
-
describe: {
|
|
190316
|
-
type: "string",
|
|
190317
|
-
description: "Human-readable sentence describing where this change routes; surfaced verbatim (never Eta-rendered) to templates as it.edges[].describe."
|
|
190318
|
-
}
|
|
190319
|
-
}
|
|
190320
|
-
}] }
|
|
190846
|
+
description: "Rename map: local state name -> concrete state name (identity when omitted).",
|
|
190847
|
+
additionalProperties: { type: "string" }
|
|
190321
190848
|
},
|
|
190322
|
-
|
|
190323
|
-
type: "boolean",
|
|
190324
|
-
description: "Exactly one state in the workflow must declare initial: true (and it must not be a commit state)."
|
|
190325
|
-
},
|
|
190326
|
-
retry: {
|
|
190849
|
+
with: {
|
|
190327
190850
|
type: "object",
|
|
190328
|
-
description: "
|
|
190329
|
-
additionalProperties: false,
|
|
190330
|
-
required: ["max", "otherwise"],
|
|
190331
|
-
properties: {
|
|
190332
|
-
max: {
|
|
190333
|
-
type: "integer",
|
|
190334
|
-
minimum: 0,
|
|
190335
|
-
description: "Entries allowed this process before redirecting."
|
|
190336
|
-
},
|
|
190337
|
-
otherwise: {
|
|
190338
|
-
type: "string",
|
|
190339
|
-
description: "Defined state to redirect to once over the cap."
|
|
190340
|
-
}
|
|
190341
|
-
}
|
|
190342
|
-
},
|
|
190343
|
-
model: {
|
|
190344
|
-
type: "string",
|
|
190345
|
-
description: "Opaque harness hint passed through `gtd next --json`/`gtd status --json` (e.g. \"smart\"). Never interpreted by gtd. Forbidden on a commit state."
|
|
190346
|
-
},
|
|
190347
|
-
memory: {
|
|
190348
|
-
type: "string",
|
|
190349
|
-
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."
|
|
190350
|
-
},
|
|
190351
|
-
file: {
|
|
190352
|
-
type: "string",
|
|
190353
|
-
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."
|
|
190354
|
-
},
|
|
190355
|
-
mode: {
|
|
190356
|
-
type: "string",
|
|
190357
|
-
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."
|
|
190358
|
-
},
|
|
190359
|
-
reviewWindow: {
|
|
190360
|
-
type: "boolean",
|
|
190361
|
-
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."
|
|
190362
|
-
},
|
|
190363
|
-
reviewBase: {
|
|
190364
|
-
type: "boolean",
|
|
190365
|
-
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."
|
|
190366
|
-
},
|
|
190367
|
-
reviewEntry: {
|
|
190368
|
-
type: "boolean",
|
|
190369
|
-
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."
|
|
190370
|
-
},
|
|
190371
|
-
fixEntry: {
|
|
190372
|
-
type: "boolean",
|
|
190373
|
-
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."
|
|
190851
|
+
description: "Bindings for the sub-machine's `$param`s."
|
|
190374
190852
|
},
|
|
190375
|
-
|
|
190376
|
-
type: "
|
|
190377
|
-
description: "
|
|
190853
|
+
set: {
|
|
190854
|
+
type: "object",
|
|
190855
|
+
description: "Extra fields merged onto a renamed local state, keyed by local name.",
|
|
190856
|
+
additionalProperties: { type: "object" }
|
|
190378
190857
|
}
|
|
190379
190858
|
}
|
|
190380
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
|
|
190381
190866
|
}
|
|
190382
190867
|
}
|
|
190383
190868
|
};
|
|
@@ -190558,31 +191043,41 @@ const compileRcVars = (raw) => {
|
|
|
190558
191043
|
return vars;
|
|
190559
191044
|
};
|
|
190560
191045
|
/**
|
|
190561
|
-
*
|
|
190562
|
-
*
|
|
190563
|
-
*
|
|
190564
|
-
*
|
|
190565
|
-
|
|
190566
|
-
|
|
190567
|
-
|
|
190568
|
-
*
|
|
190569
|
-
*
|
|
190570
|
-
*
|
|
190571
|
-
*
|
|
190572
|
-
*
|
|
190573
|
-
*
|
|
190574
|
-
* `gtd init`), or (via `compileWorkflowConfig`/`compileRcVars`) on any invalid
|
|
190575
|
-
* workflow/vars.
|
|
191046
|
+
* Compile the decoded config's `workflow:` key (or gtd's built-in bundled
|
|
191047
|
+
* default, when absent) plus its top-level `vars:`/`modes:` keys into
|
|
191048
|
+
* `ConfigOperations`. A custom `workflow:`'s `./`/`../` content file references
|
|
191049
|
+
* were already inlined per declaring file by `loadMerged`, so the compiler is
|
|
191050
|
+
* invoked with `inlineFileRefs: false` and `root` is passed only as an (unused)
|
|
191051
|
+
* `configDir` placeholder.
|
|
191052
|
+
*
|
|
191053
|
+
* When no `workflow:` key is configured anywhere in the cwd→home chain, the
|
|
191054
|
+
* built-in default (`defaultWorkflowDefinition`, pre-compiled and validated
|
|
191055
|
+
* once at module load) is used. Layering the top-level `modes:` over it can
|
|
191056
|
+
* only ADD mode names (never invalidate a `mode:` reference), so it needs no
|
|
191057
|
+
* re-validation. Throws (via `compileWorkflowConfig`/`compileRcVars`) only on
|
|
191058
|
+
* an invalid CUSTOM workflow/vars; the built-in default never throws here.
|
|
190576
191059
|
*/
|
|
190577
191060
|
const toOperations = (decoded, root) => {
|
|
190578
191061
|
const rcVars = compileRcVars(decoded.vars);
|
|
190579
191062
|
const rcModes = compileRcModes(decoded.modes);
|
|
190580
|
-
if (decoded.workflow === void 0)
|
|
191063
|
+
if (decoded.workflow === void 0) {
|
|
191064
|
+
const modes = mergeModes(defaultWorkflowDefinition.modes, rcModes);
|
|
191065
|
+
return {
|
|
191066
|
+
workflow: modes !== void 0 ? {
|
|
191067
|
+
...defaultWorkflowDefinition,
|
|
191068
|
+
modes
|
|
191069
|
+
} : defaultWorkflowDefinition,
|
|
191070
|
+
workflowVars: defaultWorkflowVars,
|
|
191071
|
+
rcVars,
|
|
191072
|
+
rawWorkflow: defaultWorkflowRaw
|
|
191073
|
+
};
|
|
191074
|
+
}
|
|
190581
191075
|
const { definition, vars: workflowVars } = compileWorkflowConfig(decoded.workflow, root, rcModes, false);
|
|
190582
191076
|
return {
|
|
190583
191077
|
workflow: definition,
|
|
190584
191078
|
workflowVars,
|
|
190585
|
-
rcVars
|
|
191079
|
+
rcVars,
|
|
191080
|
+
rawWorkflow: decoded.workflow
|
|
190586
191081
|
};
|
|
190587
191082
|
};
|
|
190588
191083
|
const formatSchemaError = (e) => {
|
|
@@ -191557,96 +192052,6 @@ var WorktreeReader = class WorktreeReader extends Tag("WorktreeReader")() {
|
|
|
191557
192052
|
}));
|
|
191558
192053
|
};
|
|
191559
192054
|
//#endregion
|
|
191560
|
-
//#region src/workflows/unified.yaml
|
|
191561
|
-
var unified_default = "# The unified gtd workflow template — written verbatim into `.gtdrc.json`\n# (nested under a `workflow:` key) by `gtd init` (no argument). gtd ships no\n# default workflow: a repo picks this bundled template with `gtd init`, and a\n# state command run with no workflow configured fails (see src/Config.ts).\n# 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. `gtd init` DERIVES the scaffolded\n# `.gtdrc.json` from this inline source (`renderInitScaffold`): each agent\n# state's `prompt:` is written out as an editable `gtd-prompts/<state>.md`\n# file; human `message:`/check `script:`/the `done` `commit:` body stay inline.\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";
|
|
191562
|
-
//#endregion
|
|
191563
|
-
//#region src/workflows/templates.ts
|
|
191564
|
-
/**
|
|
191565
|
-
* The `$schema` URL editors resolve for `.gtdrc.json` completion/validation —
|
|
191566
|
-
* written as the first key of every `gtd init` config (see `renderInitConfig`).
|
|
191567
|
-
*/
|
|
191568
|
-
const SCHEMA_URL = "https://raw.githubusercontent.com/pmelab/gtd/main/schema.json";
|
|
191569
|
-
/**
|
|
191570
|
-
* The repo-root-relative directory `gtd init` writes each agent state's prompt
|
|
191571
|
-
* into as a standalone, editable Markdown file (see `renderInitScaffold`). The
|
|
191572
|
-
* scaffolded `.gtdrc.json` references these via `./gtd-prompts/<state>.md`
|
|
191573
|
-
* content-value file references, which `compileWorkflowConfig` inlines at load
|
|
191574
|
-
* time (`resolveContent` in `src/PatternConfig.ts`) — so editing a prompt file
|
|
191575
|
-
* changes the workflow with no config edit.
|
|
191576
|
-
*/
|
|
191577
|
-
const PROMPTS_DIR = "gtd-prompts";
|
|
191578
|
-
/**
|
|
191579
|
-
* The top-level `modes:` block `gtd init` SEEDS into the scaffolded
|
|
191580
|
-
* `.gtdrc.json` as a ready-to-edit suggestion: a `format:` command for each
|
|
191581
|
-
* built-in steering-file mode the bundled template uses (`qa` for the plan /
|
|
191582
|
-
* open-questions files, `review` for REVIEW.md), so a fresh project's steering
|
|
191583
|
-
* files are auto-formatted with Prettier before gtd validates them. Only
|
|
191584
|
-
* `format:` is declared — gtd's built-in `qa`/`review` validators still do the
|
|
191585
|
-
* validating (the two halves layer independently — see STATES.md §12 /
|
|
191586
|
-
* docs/configuration.md `modes:`). gtd ships no formatter, so this is the one
|
|
191587
|
-
* place a default is suggested; a project edits or drops it freely (swap
|
|
191588
|
-
* Prettier for dprint, point at a script, delete the key).
|
|
191589
|
-
*
|
|
191590
|
-
* Seeded only by `renderInitScaffold` (the real `gtd init` write path), NOT by
|
|
191591
|
-
* `renderInitConfig`: the latter is reused as a hermetic test fixture (the
|
|
191592
|
-
* `Given the workflow` step) that must not depend on Prettier being installed
|
|
191593
|
-
* or spawn a subprocess at every steering-file gate.
|
|
191594
|
-
*/
|
|
191595
|
-
const MODES_SUGGESTION = {
|
|
191596
|
-
qa: { format: "npx prettier --write <%= it.file %>" },
|
|
191597
|
-
review: { format: "npx prettier --write <%= it.file %>" }
|
|
191598
|
-
};
|
|
191599
|
-
/**
|
|
191600
|
-
* The single bundled workflow template `gtd init` scaffolds — the raw YAML text
|
|
191601
|
-
* of `unified.yaml`, imported as a string (tsdown's `.yaml` text loader / the
|
|
191602
|
-
* vitest `rawMd` transform — see tsdown.config.ts / tests/vitest.rawMd.ts), so
|
|
191603
|
-
* this module never touches the filesystem: it works identically in the dev
|
|
191604
|
-
* checkout, under `vitest`, and inside the single-file `dist/gtd.bundle.mjs`
|
|
191605
|
-
* build. gtd ships NO default workflow and no longer offers a choice of
|
|
191606
|
-
* templates — `gtd init` takes no argument.
|
|
191607
|
-
*/
|
|
191608
|
-
const UNIFIED_WORKFLOW = unified_default;
|
|
191609
|
-
/**
|
|
191610
|
-
* Render everything `gtd init` writes. Like `renderInitConfig`, but each agent
|
|
191611
|
-
* state's inline `prompt:` is EXTRACTED into a standalone
|
|
191612
|
-
* `gtd-prompts/<state>.md` file and the config value is rewritten to a
|
|
191613
|
-
* `./gtd-prompts/<state>.md` file reference — `compileWorkflowConfig` inlines
|
|
191614
|
-
* it back at load time (`resolveContent`), so the workflow behaves identically
|
|
191615
|
-
* to the fully-inline `renderInitConfig` form while the prompts stay editable
|
|
191616
|
-
* as ordinary Markdown. Only `prompt:` content is externalized: human
|
|
191617
|
-
* `message:` blocks, check `script:` bodies, and the `done` `commit:` template
|
|
191618
|
-
* remain inline in the config (they are workflow mechanics, not prompts). The
|
|
191619
|
-
* bundled YAML template itself stays fully inline (it ships inside the
|
|
191620
|
-
* single-file bundle); this function is the only place the split happens, at
|
|
191621
|
-
* scaffold time.
|
|
191622
|
-
*
|
|
191623
|
-
* It also seeds a top-level `modes:` block (`MODES_SUGGESTION`) — a ready-to-
|
|
191624
|
-
* edit Prettier `format:` for the built-in `qa`/`review` steering-file modes —
|
|
191625
|
-
* so a fresh project auto-formats its steering files out of the box. This lives
|
|
191626
|
-
* here rather than in `renderInitConfig` so the latter stays a hermetic test
|
|
191627
|
-
* fixture (see `MODES_SUGGESTION`).
|
|
191628
|
-
*/
|
|
191629
|
-
const renderInitScaffold = () => {
|
|
191630
|
-
const workflow = (0, import_dist$1.parse)(UNIFIED_WORKFLOW);
|
|
191631
|
-
const prompts = [];
|
|
191632
|
-
for (const [stateName, state] of Object.entries(workflow.states ?? {})) if (typeof state.prompt === "string") {
|
|
191633
|
-
const path = `${PROMPTS_DIR}/${stateName}.md`;
|
|
191634
|
-
prompts.push({
|
|
191635
|
-
path,
|
|
191636
|
-
content: state.prompt
|
|
191637
|
-
});
|
|
191638
|
-
state.prompt = `./${path}`;
|
|
191639
|
-
}
|
|
191640
|
-
return {
|
|
191641
|
-
config: JSON.stringify({
|
|
191642
|
-
$schema: SCHEMA_URL,
|
|
191643
|
-
modes: MODES_SUGGESTION,
|
|
191644
|
-
workflow
|
|
191645
|
-
}, null, 2) + "\n",
|
|
191646
|
-
prompts
|
|
191647
|
-
};
|
|
191648
|
-
};
|
|
191649
|
-
//#endregion
|
|
191650
192055
|
//#region node_modules/eta/dist/index.mjs
|
|
191651
192056
|
var EtaError = class extends Error {
|
|
191652
192057
|
constructor(message) {
|
|
@@ -204428,74 +204833,142 @@ const startLspServer = () => gen(function* () {
|
|
|
204428
204833
|
});
|
|
204429
204834
|
});
|
|
204430
204835
|
//#endregion
|
|
204431
|
-
//#region src/
|
|
204432
|
-
/**
|
|
204433
|
-
|
|
204434
|
-
|
|
204435
|
-
|
|
204436
|
-
|
|
204437
|
-
|
|
204438
|
-
|
|
204439
|
-
|
|
204440
|
-
|
|
204441
|
-
|
|
204442
|
-
|
|
204443
|
-
|
|
204444
|
-
|
|
204445
|
-
|
|
204446
|
-
|
|
204447
|
-
|
|
204448
|
-
|
|
204449
|
-
|
|
204450
|
-
|
|
204451
|
-
|
|
204452
|
-
|
|
204453
|
-
|
|
204454
|
-
|
|
204455
|
-
|
|
204456
|
-
|
|
204457
|
-
|
|
204458
|
-
|
|
204459
|
-
|
|
204460
|
-
|
|
204461
|
-
|
|
204462
|
-
|
|
204463
|
-
|
|
204464
|
-
|
|
204465
|
-
|
|
204466
|
-
|
|
204467
|
-
|
|
204468
|
-
|
|
204469
|
-
|
|
204470
|
-
|
|
204471
|
-
|
|
204472
|
-
|
|
204473
|
-
|
|
204474
|
-
*
|
|
204475
|
-
*
|
|
204476
|
-
*
|
|
204477
|
-
|
|
204478
|
-
|
|
204479
|
-
|
|
204480
|
-
const
|
|
204481
|
-
const
|
|
204482
|
-
|
|
204483
|
-
|
|
204484
|
-
|
|
204485
|
-
|
|
204486
|
-
|
|
204487
|
-
|
|
204488
|
-
|
|
204489
|
-
|
|
204490
|
-
|
|
204491
|
-
|
|
204492
|
-
|
|
204493
|
-
|
|
204494
|
-
|
|
204495
|
-
|
|
204496
|
-
|
|
204836
|
+
//#region src/visualize.html
|
|
204837
|
+
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) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\", \"'\": \"'\" })[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, \""\")}\"`\n switch (kind) {\n case \"message\":\n return `([\"${label.replace(/\"/g, \""\")}\"])`\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, \""\").replace(/\\|/g, \"|\")}\"`\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";
|
|
204838
|
+
//#endregion
|
|
204839
|
+
//#region src/Visualize.ts
|
|
204840
|
+
const FLAG_KEYS = [
|
|
204841
|
+
"reviewWindow",
|
|
204842
|
+
"reviewBase",
|
|
204843
|
+
"reviewEntry",
|
|
204844
|
+
"fixEntry",
|
|
204845
|
+
"requireProgress",
|
|
204846
|
+
"answerGate"
|
|
204847
|
+
];
|
|
204848
|
+
const flagsOf = (def) => FLAG_KEYS.filter((k) => def[k] === true);
|
|
204849
|
+
const edgeToViz = ([pattern, to, describe]) => describe !== void 0 ? {
|
|
204850
|
+
pattern,
|
|
204851
|
+
to,
|
|
204852
|
+
describe
|
|
204853
|
+
} : {
|
|
204854
|
+
pattern,
|
|
204855
|
+
to
|
|
204856
|
+
};
|
|
204857
|
+
/** Drop keys whose value is `undefined` (so `exactOptionalPropertyTypes` optionals stay absent, not `undefined`). */
|
|
204858
|
+
const stripUndefined = (o) => {
|
|
204859
|
+
for (const key of Object.keys(o)) if (o[key] === void 0) delete o[key];
|
|
204860
|
+
return o;
|
|
204861
|
+
};
|
|
204862
|
+
/** Describe one compiled state for the viewer (optional fields omitted when unset). */
|
|
204863
|
+
const toVizState = (name, def, group, incoming) => stripUndefined({
|
|
204864
|
+
name,
|
|
204865
|
+
actor: def.actor,
|
|
204866
|
+
kind: contentKindOf(def) ?? "unknown",
|
|
204867
|
+
initial: def.initial === true ? true : void 0,
|
|
204868
|
+
model: def.model,
|
|
204869
|
+
memory: def.memory,
|
|
204870
|
+
file: def.file,
|
|
204871
|
+
mode: def.mode,
|
|
204872
|
+
retry: def.retry,
|
|
204873
|
+
flags: flagsOf(def),
|
|
204874
|
+
on: (def.on ?? []).map(edgeToViz),
|
|
204875
|
+
incoming,
|
|
204876
|
+
group
|
|
204877
|
+
});
|
|
204878
|
+
/**
|
|
204879
|
+
* Build the viewer's JSON description from the active COMPILED workflow plus its
|
|
204880
|
+
* RAW value (the pre-expansion `submachines:`/`use:` form — `rawWorkflow` from
|
|
204881
|
+
* `ConfigService`), which is the only place the sub-machine grouping survives
|
|
204882
|
+
* (`compileWorkflowConfig` flattens it away). `vars` is shown for reference.
|
|
204883
|
+
*/
|
|
204884
|
+
const buildVizModel = (workflow, rawWorkflow, vars) => {
|
|
204885
|
+
const groups = collectGroups(rawWorkflow);
|
|
204886
|
+
const sizeOf = new Map(groups.map((g) => [g.name, g.states.length]));
|
|
204887
|
+
const groupOf = /* @__PURE__ */ new Map();
|
|
204888
|
+
for (const g of groups) for (const s of g.states) {
|
|
204889
|
+
const cur = groupOf.get(s);
|
|
204890
|
+
if (cur === void 0 || sizeOf.get(g.name) < sizeOf.get(cur)) groupOf.set(s, g.name);
|
|
204891
|
+
}
|
|
204892
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
204893
|
+
const addIncoming = (target, from, pattern) => {
|
|
204894
|
+
const list = incoming.get(target) ?? [];
|
|
204895
|
+
list.push({
|
|
204896
|
+
from,
|
|
204897
|
+
pattern
|
|
204898
|
+
});
|
|
204899
|
+
incoming.set(target, list);
|
|
204900
|
+
};
|
|
204901
|
+
for (const [name, def] of Object.entries(workflow.states)) {
|
|
204902
|
+
for (const [pattern, to] of def.on ?? []) addIncoming(to, name, pattern);
|
|
204903
|
+
if (def.retry) addIncoming(def.retry.otherwise, name, `retry ×${def.retry.max}`);
|
|
204497
204904
|
}
|
|
204498
|
-
return
|
|
204905
|
+
return {
|
|
204906
|
+
states: Object.entries(workflow.states).map(([name, def]) => toVizState(name, def, groupOf.get(name), incoming.get(name) ?? [])),
|
|
204907
|
+
initial: initialStateOf(workflow),
|
|
204908
|
+
groups: groups.map((g) => ({
|
|
204909
|
+
name: g.name,
|
|
204910
|
+
submachine: g.submachine,
|
|
204911
|
+
states: [...g.states]
|
|
204912
|
+
})),
|
|
204913
|
+
vars
|
|
204914
|
+
};
|
|
204915
|
+
};
|
|
204916
|
+
/**
|
|
204917
|
+
* Route one request (pure): `/` (or `/index.html`) serves the viewer page,
|
|
204918
|
+
* `/workflow.json` serves the model, anything else is a 404. Kept separate from
|
|
204919
|
+
* the server so it unit-tests without a socket.
|
|
204920
|
+
*/
|
|
204921
|
+
const handleVizRequest = (pathname, model) => {
|
|
204922
|
+
if (pathname === "/" || pathname === "/index.html") return {
|
|
204923
|
+
status: 200,
|
|
204924
|
+
contentType: "text/html; charset=utf-8",
|
|
204925
|
+
body: visualize_default
|
|
204926
|
+
};
|
|
204927
|
+
if (pathname === "/workflow.json") return {
|
|
204928
|
+
status: 200,
|
|
204929
|
+
contentType: "application/json; charset=utf-8",
|
|
204930
|
+
body: JSON.stringify(model)
|
|
204931
|
+
};
|
|
204932
|
+
return {
|
|
204933
|
+
status: 404,
|
|
204934
|
+
contentType: "text/plain; charset=utf-8",
|
|
204935
|
+
body: "not found"
|
|
204936
|
+
};
|
|
204937
|
+
};
|
|
204938
|
+
/**
|
|
204939
|
+
* Start the viewer's HTTP server, resolving once it is listening with the
|
|
204940
|
+
* chosen URL (a `port` of `0` picks a free port). Rejects if the port is in
|
|
204941
|
+
* use. The caller owns the returned `server` (closes it on shutdown).
|
|
204942
|
+
*/
|
|
204943
|
+
const startVizServer = (model, port, host = "127.0.0.1") => new Promise((resolve, reject) => {
|
|
204944
|
+
const server = createServer((req, res) => {
|
|
204945
|
+
const { pathname } = new URL(req.url ?? "/", "http://localhost");
|
|
204946
|
+
const { status, contentType, body } = handleVizRequest(pathname, model);
|
|
204947
|
+
res.writeHead(status, { "content-type": contentType });
|
|
204948
|
+
res.end(body);
|
|
204949
|
+
});
|
|
204950
|
+
server.once("error", reject);
|
|
204951
|
+
server.listen(port, host, () => {
|
|
204952
|
+
const address = server.address();
|
|
204953
|
+
const chosen = typeof address === "object" && address !== null ? address.port : port;
|
|
204954
|
+
resolve({
|
|
204955
|
+
server,
|
|
204956
|
+
url: `http://${host}:${chosen}`
|
|
204957
|
+
});
|
|
204958
|
+
});
|
|
204959
|
+
});
|
|
204960
|
+
/** 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. */
|
|
204961
|
+
const openInBrowser = (url) => {
|
|
204962
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
204963
|
+
try {
|
|
204964
|
+
const child = spawn(cmd, [url], {
|
|
204965
|
+
stdio: "ignore",
|
|
204966
|
+
detached: true,
|
|
204967
|
+
shell: process.platform === "win32"
|
|
204968
|
+
});
|
|
204969
|
+
child.on("error", () => {});
|
|
204970
|
+
child.unref();
|
|
204971
|
+
} catch {}
|
|
204499
204972
|
};
|
|
204500
204973
|
//#endregion
|
|
204501
204974
|
//#region src/SteeringMode.ts
|
|
@@ -204616,11 +205089,13 @@ const GTD_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
|
204616
205089
|
const HELP_TEXT = `Usage: gtd [command] [options]
|
|
204617
205090
|
|
|
204618
205091
|
Commands:
|
|
204619
|
-
init Scaffold a .gtdrc.json for this repo
|
|
204620
|
-
|
|
204621
|
-
|
|
205092
|
+
init Scaffold a minimal .gtdrc.json for this repo, seeding the
|
|
205093
|
+
default variables you are most likely to change (the test
|
|
205094
|
+
command) and a Prettier formatting suggestion. gtd runs its
|
|
205095
|
+
built-in workflow by default, so no workflow is written —
|
|
205096
|
+
add a workflow: key only to customize the machine itself.
|
|
204622
205097
|
Takes no argument. Run once per repo; refuses if a gtd
|
|
204623
|
-
config already exists. Leaves the
|
|
205098
|
+
config already exists. Leaves the file uncommitted for you
|
|
204624
205099
|
to review and commit
|
|
204625
205100
|
step <actor> Authenticate as <actor>, match the resolved rest's
|
|
204626
205101
|
declared patterns against the pending changes, and commit
|
|
@@ -204644,14 +205119,17 @@ Commands:
|
|
|
204644
205119
|
validate Format and validate the steering file the resolved rest
|
|
204645
205120
|
declares, with its mode's commands (its file:/mode:);
|
|
204646
205121
|
exits non-zero with the findings when it is invalid
|
|
204647
|
-
mermaid Print the active workflow's shape as Mermaid
|
|
204648
|
-
stateDiagram-v2 source (no mutation)
|
|
204649
205122
|
lsp Start the LSP server for .gtd/ steering files (stdio)
|
|
205123
|
+
visualize Serve an interactive diagram of the active workflow on a
|
|
205124
|
+
local web server (--port <n>, --no-open; --json prints the
|
|
205125
|
+
model and exits)
|
|
204650
205126
|
version Print version and exit
|
|
204651
205127
|
help Print this help and exit
|
|
204652
205128
|
|
|
204653
205129
|
Options:
|
|
204654
205130
|
--json Output structured JSON instead of plain text
|
|
205131
|
+
--port=<n> (gtd visualize only) port to serve on (default: a free port)
|
|
205132
|
+
--no-open (gtd visualize only) do not open the browser
|
|
204655
205133
|
--cost=<n> (gtd step only) record the invocation's token cost
|
|
204656
205134
|
--model=<name> (gtd step only, with --cost) tag that cost's model
|
|
204657
205135
|
--version, -v Print version and exit
|
|
@@ -204671,7 +205149,7 @@ const isEnveloped = (error) => typeof error === "object" && error !== null && er
|
|
|
204671
205149
|
* The stderr line for a CLI error (see `main.ts`): a `gtd: ` prefix UNLESS the
|
|
204672
205150
|
* message already carries one. Most gtd errors are authored with a
|
|
204673
205151
|
* `gtd:`/`gtd <cmd>:` prefix of their own (e.g. `gtd init: …`,
|
|
204674
|
-
* `gtd:
|
|
205152
|
+
* `gtd: unknown option …`), so a blind prepend produced a doubled
|
|
204675
205153
|
* `gtd: gtd: …`.
|
|
204676
205154
|
*/
|
|
204677
205155
|
const cliErrorLine = (error) => {
|
|
@@ -204751,20 +205229,19 @@ const runLspCommand = (argv, json) => gen(function* () {
|
|
|
204751
205229
|
yield* startLspServer();
|
|
204752
205230
|
});
|
|
204753
205231
|
/**
|
|
204754
|
-
* `gtd init`: scaffold a `.gtdrc.json`
|
|
204755
|
-
*
|
|
204756
|
-
*
|
|
204757
|
-
*
|
|
204758
|
-
*
|
|
204759
|
-
*
|
|
204760
|
-
*
|
|
204761
|
-
* the
|
|
204762
|
-
*
|
|
204763
|
-
*
|
|
204764
|
-
*
|
|
204765
|
-
*
|
|
204766
|
-
*
|
|
204767
|
-
* would otherwise capture).
|
|
205232
|
+
* `gtd init`: scaffold a MINIMAL `.gtdrc.json` seeding the default variables a
|
|
205233
|
+
* fresh project is most likely to change — the test command (`vars.testCommand`)
|
|
205234
|
+
* and a ready-to-edit Prettier formatting suggestion (`modes:`). It writes NO
|
|
205235
|
+
* `workflow:` key: gtd ships the unified workflow as its built-in default and
|
|
205236
|
+
* runs it whenever none is configured (see `src/Config.ts`), so there is
|
|
205237
|
+
* nothing to scaffold there — a project customizes the machine itself only by
|
|
205238
|
+
* adding a `workflow:` key. Takes NO argument. Dispatched like `lsp` — BEFORE
|
|
205239
|
+
* the closeReviewWindow/dispatch/openReviewWindow block — because it needs no
|
|
205240
|
+
* `ConfigService` and no review window. It still runs the repo-root guard (it
|
|
205241
|
+
* writes `.gtdrc.json` at the root) and refuses to clobber an existing config.
|
|
205242
|
+
* The file is left UNCOMMITTED, so the message warns to commit it before the
|
|
205243
|
+
* first `gtd step` (an uncommitted config counts as a pending change the
|
|
205244
|
+
* initial state's `* **` edge would otherwise capture).
|
|
204768
205245
|
*/
|
|
204769
205246
|
const runInitCommand = (argv, json, write) => gen(function* () {
|
|
204770
205247
|
const args = commandArgs(argv);
|
|
@@ -204777,21 +205254,11 @@ const runInitCommand = (argv, json, write) => gen(function* () {
|
|
|
204777
205254
|
const toError = (e) => e instanceof Error ? e : new Error(String(e));
|
|
204778
205255
|
const scaffold = renderInitScaffold();
|
|
204779
205256
|
yield* fs.writeFileString(join(root, ".gtdrc.json"), scaffold.config).pipe(mapError$1(toError));
|
|
204780
|
-
for (const prompt of scaffold.prompts) {
|
|
204781
|
-
const full = join(root, prompt.path);
|
|
204782
|
-
yield* fs.makeDirectory(dirname(full), { recursive: true }).pipe(mapError$1(toError));
|
|
204783
|
-
yield* fs.writeFileString(full, prompt.content).pipe(mapError$1(toError));
|
|
204784
|
-
}
|
|
204785
205257
|
if (json) write(JSON.stringify({
|
|
204786
205258
|
written: ".gtdrc.json",
|
|
204787
|
-
workflow: "unified",
|
|
204788
|
-
prompts: scaffold.prompts.map((p) => p.path),
|
|
204789
205259
|
inRepo
|
|
204790
205260
|
}) + "\n");
|
|
204791
|
-
else
|
|
204792
|
-
const promptCount = scaffold.prompts.length;
|
|
204793
|
-
write(`Wrote .gtdrc.json with the bundled unified workflow, and its ${promptCount} agent prompt${promptCount === 1 ? "" : "s"} as editable Markdown under ${PROMPTS_DIR}/ (referenced from the config).\n\n` + (inRepo ? "Review and commit them before starting: an uncommitted .gtdrc.json (or prompt\nfile) counts as a pending change, so the initial state would capture it on the\nfirst step. Once committed, run `gtd step human` to begin.\n" : "This directory is not a git repository, so there is nothing to commit here. The\nconfig applies to any gtd repository nested below it — gtd discovers it by\nwalking up from the repository root. Run `gtd step human` from such a repo to\nbegin.\n"));
|
|
204794
|
-
}
|
|
205261
|
+
else write("Wrote .gtdrc.json seeding the default variables (the test command) and a\nPrettier formatting suggestion. gtd runs its built-in workflow by default — add\na workflow: key only if you want to customize the machine itself.\n\n" + (inRepo ? "Review and commit it before starting: an uncommitted .gtdrc.json counts as a\npending change, so the initial state would capture it on the first step. Once\ncommitted, run `gtd step human` to begin.\n" : "This directory is not a git repository, so there is nothing to commit here. The\nconfig applies to any gtd repository nested below it — gtd discovers it by\nwalking up from the repository root. Run `gtd step human` from such a repo to\nbegin.\n"));
|
|
204795
205262
|
});
|
|
204796
205263
|
/**
|
|
204797
205264
|
* Resolve HEAD's rest, the current process run, and the template context for
|
|
@@ -205304,20 +205771,82 @@ const runStatusCommand = (argv, json, write) => gen(function* () {
|
|
|
205304
205771
|
if (json) writeStatusJson(write, rest, statusChanges, model, memory, file, context.processCost, context.processCostByModel);
|
|
205305
205772
|
else writeStatusPlain(write, rest, statusChanges, model, memory, file, context.processCost, context.processCostByModel);
|
|
205306
205773
|
});
|
|
205774
|
+
/** Parse a `--port` value: an integer 0–65535, or `undefined` if invalid/absent. */
|
|
205775
|
+
const parsePort = (value) => {
|
|
205776
|
+
const n = Number(value);
|
|
205777
|
+
return value !== void 0 && Number.isInteger(n) && n >= 0 && n <= 65535 ? n : void 0;
|
|
205778
|
+
};
|
|
205779
|
+
/** Resolve a `--port`/`--port=<n>` argument to its raw value + the index consumed, or `null` if `arg` is not a port option. */
|
|
205780
|
+
const portArgValue = (arg, rest, i) => {
|
|
205781
|
+
if (arg === "--port") return {
|
|
205782
|
+
value: rest[i + 1],
|
|
205783
|
+
nextIndex: i + 1
|
|
205784
|
+
};
|
|
205785
|
+
if (arg.startsWith("--port=")) return {
|
|
205786
|
+
value: arg.slice(7),
|
|
205787
|
+
nextIndex: i
|
|
205788
|
+
};
|
|
205789
|
+
return null;
|
|
205790
|
+
};
|
|
205791
|
+
/**
|
|
205792
|
+
* Parse `gtd visualize`'s own options from the argv tail (the subcommand token
|
|
205793
|
+
* already dropped): `--port <n>`/`--port=<n>`, `--no-open`, and a passthrough
|
|
205794
|
+
* `--json`. Returns the options, or an `{ error }` message for an invalid port,
|
|
205795
|
+
* an unknown `--` option, or an unexpected positional.
|
|
205796
|
+
*/
|
|
205797
|
+
const parseVisualizeOptions = (rest) => {
|
|
205798
|
+
let port = 0;
|
|
205799
|
+
let open = true;
|
|
205800
|
+
for (let i = 0; i < rest.length; i++) {
|
|
205801
|
+
const arg = rest[i];
|
|
205802
|
+
const portArg = portArgValue(arg, rest, i);
|
|
205803
|
+
if (portArg !== null) {
|
|
205804
|
+
const parsed = parsePort(portArg.value);
|
|
205805
|
+
if (parsed === void 0) return { error: `gtd visualize: --port must be an integer 0–65535 (got '${portArg.value ?? ""}')` };
|
|
205806
|
+
port = parsed;
|
|
205807
|
+
i = portArg.nextIndex;
|
|
205808
|
+
continue;
|
|
205809
|
+
}
|
|
205810
|
+
if (arg === "--json") continue;
|
|
205811
|
+
if (arg === "--no-open") open = false;
|
|
205812
|
+
else if (arg.startsWith("--")) return { error: `gtd: unknown option '${arg}' — see \`gtd --help\`` };
|
|
205813
|
+
else return { error: `gtd visualize: unexpected argument '${arg}'` };
|
|
205814
|
+
}
|
|
205815
|
+
return {
|
|
205816
|
+
port,
|
|
205817
|
+
open
|
|
205818
|
+
};
|
|
205819
|
+
};
|
|
205307
205820
|
/**
|
|
205308
|
-
* `gtd
|
|
205309
|
-
*
|
|
205310
|
-
*
|
|
205311
|
-
*
|
|
205312
|
-
*
|
|
205313
|
-
*
|
|
205314
|
-
|
|
205315
|
-
*
|
|
205316
|
-
|
|
205317
|
-
const
|
|
205318
|
-
|
|
205319
|
-
|
|
205320
|
-
|
|
205821
|
+
* `gtd visualize`: serve an interactive diagram of the ACTIVE workflow on a
|
|
205822
|
+
* local HTTP server (see `src/Visualize.ts`). Dispatched early (like `lsp`/
|
|
205823
|
+
* `init`) — it reads the config but never touches git, HEAD, or the review
|
|
205824
|
+
* window — and parses its OWN options (`--port`, `--no-open`), since the global
|
|
205825
|
+
* unknown-option check does not know them. `--json` prints the model and exits
|
|
205826
|
+
* without starting a server (the testable path).
|
|
205827
|
+
*/
|
|
205828
|
+
const runVisualizeCommand = (argv, json, write) => gen(function* () {
|
|
205829
|
+
const tokens = argv.slice(2);
|
|
205830
|
+
const subIdx = tokens.indexOf("visualize");
|
|
205831
|
+
const rest = subIdx >= 0 ? [...tokens.slice(0, subIdx), ...tokens.slice(subIdx + 1)] : tokens;
|
|
205832
|
+
const opts = parseVisualizeOptions(rest);
|
|
205833
|
+
if ("error" in opts) return yield* fail$2(new Error(opts.error));
|
|
205834
|
+
const config = yield* (yield* ConfigService).load;
|
|
205835
|
+
const model = buildVizModel(config.workflow, config.rawWorkflow, {
|
|
205836
|
+
...config.workflowVars,
|
|
205837
|
+
...config.rcVars
|
|
205838
|
+
});
|
|
205839
|
+
if (json) {
|
|
205840
|
+
write(JSON.stringify(model, null, 2) + "\n");
|
|
205841
|
+
return;
|
|
205842
|
+
}
|
|
205843
|
+
const { server, url } = yield* tryPromise({
|
|
205844
|
+
try: () => startVizServer(model, opts.port),
|
|
205845
|
+
catch: (e) => /* @__PURE__ */ new Error(`gtd visualize: could not start server: ${e instanceof Error ? e.message : String(e)}`)
|
|
205846
|
+
});
|
|
205847
|
+
write(`gtd visualize running at ${url} — Ctrl-C to stop\n`);
|
|
205848
|
+
if (opts.open) openInBrowser(url);
|
|
205849
|
+
yield* never.pipe(ensuring$2(sync$1(() => server.close())));
|
|
205321
205850
|
});
|
|
205322
205851
|
const KNOWN_SUBCOMMANDS = [
|
|
205323
205852
|
"step",
|
|
@@ -205325,8 +205854,7 @@ const KNOWN_SUBCOMMANDS = [
|
|
|
205325
205854
|
"fix",
|
|
205326
205855
|
"next",
|
|
205327
205856
|
"status",
|
|
205328
|
-
"validate"
|
|
205329
|
-
"mermaid"
|
|
205857
|
+
"validate"
|
|
205330
205858
|
];
|
|
205331
205859
|
/**
|
|
205332
205860
|
* `--version`/`-v`/`version` or `--help`/`-h`/`help`: short-circuits before any
|
|
@@ -205398,7 +205926,6 @@ const dispatchKnownSubcommand = (sub, argv, json, write, cost, model) => {
|
|
|
205398
205926
|
case "next": return runNextCommand(json, write);
|
|
205399
205927
|
case "status": return runStatusCommand(argv, json, write);
|
|
205400
205928
|
case "validate": return runValidateCommand(argv, json, write);
|
|
205401
|
-
case "mermaid": return runMermaidCommand(argv, json, write);
|
|
205402
205929
|
}
|
|
205403
205930
|
};
|
|
205404
205931
|
/**
|
|
@@ -205410,10 +205937,10 @@ const dispatchKnownSubcommand = (sub, argv, json, write, cost, model) => {
|
|
|
205410
205937
|
* captures stdout via the `write` callback.
|
|
205411
205938
|
*
|
|
205412
205939
|
* v3 command surface: `step <actor>` / `review <commitish>` / `fix` / `next` /
|
|
205413
|
-
* `status` / `validate`
|
|
205940
|
+
* `status` / `validate` (see `src/Edge.ts` and
|
|
205414
205941
|
* `docs/design/pattern-machine-plan.md` §3),
|
|
205415
205942
|
* plus `lsp` and `init` — both dispatched before the config-reading
|
|
205416
|
-
* path since
|
|
205943
|
+
* path since neither needs to touch the config. Bare `gtd` or an
|
|
205417
205944
|
* unknown subcommand is a usage error. Shared setup (argv parsing, the
|
|
205418
205945
|
* repo-root guard) lives here; each subcommand's own logic is a named
|
|
205419
205946
|
* `run*Command` function above.
|
|
@@ -205425,6 +205952,7 @@ function makeProgram(opts = {}) {
|
|
|
205425
205952
|
const positional = argv.slice(2).find((a) => !a.startsWith("--"));
|
|
205426
205953
|
return gen(function* () {
|
|
205427
205954
|
if (runVersionOrHelp(argv, write)) return;
|
|
205955
|
+
if (positional === "visualize") return yield* runVisualizeCommand(argv, json, write);
|
|
205428
205956
|
const unknownOption = argv.slice(2).find((a) => a.startsWith("--") && a !== "--json" && a !== "--cost" && a !== "--model" && !a.startsWith(COST_FLAG) && !a.startsWith(MODEL_FLAG));
|
|
205429
205957
|
if (unknownOption !== void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd: unknown option '${unknownOption}' — see \`gtd --help\``));
|
|
205430
205958
|
const { cost, model } = yield* parseStepFlags(argv, positional);
|