@pmelab/gtd 9.7.0 → 9.8.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 +160 -142
- package/dist/gtd.bundle.mjs +361 -290
- package/package.json +1 -1
- package/schema.json +6 -6
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -189739,6 +189739,17 @@ const STATE_FIELDS = {
|
|
|
189739
189739
|
rest: "rendered",
|
|
189740
189740
|
doc: "Opaque harness hint stamped onto every one of this machine's own `prompt` states (e.g. \"smart\"), passed through `gtd next --json`/`gtd status --json`. The ONLY place a model may be declared — a state carrying its own `model:` is a config error. Never interpreted by gtd. A machine declaring this with no `prompt` state is a config error."
|
|
189741
189741
|
},
|
|
189742
|
+
system: {
|
|
189743
|
+
kind: "text",
|
|
189744
|
+
surface: "def",
|
|
189745
|
+
authored: "machine",
|
|
189746
|
+
nonEmpty: true,
|
|
189747
|
+
commit: "forbidden",
|
|
189748
|
+
fileRef: true,
|
|
189749
|
+
rest: "rendered",
|
|
189750
|
+
viz: "field",
|
|
189751
|
+
doc: "The agent harness system prompt stamped onto every one of this machine's own `prompt` states — emitted verbatim in `gtd next --json`/`--sh` for the driver to pass to its agent CLI, never interpreted by gtd. Machine-level only: a machine's `prompt` states share one resumed session, and a system prompt must be identical across every call of it. A `./` or `../` value is inlined from the declaring config file's directory. A machine declaring this with no `prompt` state is a config error."
|
|
189752
|
+
},
|
|
189742
189753
|
/**
|
|
189743
189754
|
* An OPAQUE, human-readable display NAME for the state — gtd never
|
|
189744
189755
|
* interprets this string, it only passes it through verbatim (`gtd next
|
|
@@ -189766,21 +189777,25 @@ const STATE_FIELDS = {
|
|
|
189766
189777
|
* Optional — THE steering file this state is about: the file a human/
|
|
189767
189778
|
* editor should look at while the machine rests here. An Eta template
|
|
189768
189779
|
* (rendered through the same `it.vars`-carrying context as content and
|
|
189769
|
-
* `model`) that must render non-empty.
|
|
189770
|
-
*
|
|
189771
|
-
*
|
|
189772
|
-
*
|
|
189773
|
-
*
|
|
189780
|
+
* `model`) that must render non-empty. Names a path RELATIVE to `.gtd/` —
|
|
189781
|
+
* the compiler prepends the directory (see `PatternConfig.ts`'s `stateFile`
|
|
189782
|
+
* compiler); a `..` segment, an absolute path, or an already-prefixed
|
|
189783
|
+
* `.gtd/...` declaration are all load-time errors, never silently
|
|
189784
|
+
* rewritten. Forbidden on a commit state (never at rest — see
|
|
189785
|
+
* `validateDefinition`). Multiple states may share one `file:` (and, in the
|
|
189786
|
+
* bundled default, do). The engine never reads a path out of this string
|
|
189787
|
+
* itself — only the LSP (`src/Lsp.ts`) interprets it, to map rendered paths
|
|
189788
|
+
* to `mode`.
|
|
189774
189789
|
*/
|
|
189775
189790
|
file: {
|
|
189776
|
-
kind: "
|
|
189791
|
+
kind: "stateFile",
|
|
189777
189792
|
surface: "def",
|
|
189778
189793
|
authored: "state",
|
|
189779
189794
|
nonEmpty: true,
|
|
189780
189795
|
commit: "forbidden",
|
|
189781
189796
|
viz: "field",
|
|
189782
189797
|
rest: "rendered",
|
|
189783
|
-
doc: "The state's steering file: an Eta template naming the file a human/editor should look at while the machine rests here. Forbidden on a commit state."
|
|
189798
|
+
doc: "The state's steering file: an Eta template naming the file a human/editor should look at while the machine rests here, RELATIVE to \".gtd/\" — the compiler prepends that directory automatically. A \"..\" segment, an absolute path, or an already-prefixed \".gtd/\" are rejected, not rewritten. Forbidden on a commit state."
|
|
189784
189799
|
},
|
|
189785
189800
|
/**
|
|
189786
189801
|
* Optional, requires `file:`. The associated file's FORMAT — the NAME of a
|
|
@@ -189937,6 +189952,8 @@ const STATE_FIELDS = {
|
|
|
189937
189952
|
const STATE_FIELD_ENTRIES = Object.entries(STATE_FIELDS);
|
|
189938
189953
|
/** The four content-kind field names, in declaration order — the "exactly one of" set `validateContentKind`/`compileContent` iterate. */
|
|
189939
189954
|
const CONTENT_FIELDS = STATE_FIELD_ENTRIES.filter(([, spec]) => spec.kind === "content").map(([key]) => key);
|
|
189955
|
+
/** Every machine-authored field, in table order — the machine tier's `CONTENT_FIELDS`. */
|
|
189956
|
+
const MACHINE_FIELD_ENTRIES = STATE_FIELD_ENTRIES.filter(([, spec]) => spec.authored === "machine");
|
|
189940
189957
|
/** True when a state is a commit (final, squash) state. */
|
|
189941
189958
|
const isCommitState = (state) => state.commit !== void 0;
|
|
189942
189959
|
/**
|
|
@@ -190001,64 +190018,16 @@ const validateFieldRules = (name, state, key, spec) => {
|
|
|
190001
190018
|
/** Every mode name `def` declares in `modes:` (empty when it declares none) — the whole vocabulary a state's `mode:` may name, per this module (see `StateMode`'s doc comment for where the registry names come from). */
|
|
190002
190019
|
const knownModes = (def) => Object.keys(def.modes ?? {});
|
|
190003
190020
|
/**
|
|
190004
|
-
*
|
|
190005
|
-
*
|
|
190006
|
-
*
|
|
190007
|
-
*
|
|
190008
|
-
*
|
|
190009
|
-
*
|
|
190010
|
-
|
|
190011
|
-
|
|
190012
|
-
|
|
190013
|
-
|
|
190014
|
-
* `src/Edge.ts`'s `renderStateDir` — the one tolerated rewrite of a
|
|
190015
|
-
* `stateDir` declaration, covering the two conventional spellings a
|
|
190016
|
-
* hand-written template or `.gtdrc` value commonly carries. Exported as one
|
|
190017
|
-
* function rather than duplicated at the edge so the two call sites can never
|
|
190018
|
-
* drift into tolerating different affixes. Anything beyond these two is a
|
|
190019
|
-
* spelling `stateDirError` rejects rather than rewrites — the strip is
|
|
190020
|
-
* tolerance, not full canonicalization; it does not touch an internal `.` or
|
|
190021
|
-
* empty segment.
|
|
190022
|
-
*/
|
|
190023
|
-
const canonicalStateDir = (value) => value.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
190024
|
-
/**
|
|
190025
|
-
* The one place the engine interprets a `stateDir` VALUE (never the var
|
|
190026
|
-
* NAME) — pure and total, so both the edge (on the rendered, normalized
|
|
190027
|
-
* value, before any consumer sees it — see `src/Edge.ts`'s
|
|
190028
|
-
* `renderStateDirOrFail`) and a test can call it directly. Returns the
|
|
190029
|
-
* error message when `value` cannot name a usable plumbing directory,
|
|
190030
|
-
* `undefined` when it can.
|
|
190031
|
-
*
|
|
190032
|
-
* Rejects, in this order: blank/whitespace-only; the repo root (`.`, `./`,
|
|
190033
|
-
* `/`, or empty once a leading `./` and trailing `/` are stripped); an
|
|
190034
|
-
* absolute path (a leading `/` after that stripping); any `..` segment (an
|
|
190035
|
-
* escape); any other `.` or empty path segment (a non-canonical spelling —
|
|
190036
|
-
* `a/./state`, `a//state`, `a/.` — rejected rather than silently
|
|
190037
|
-
* canonicalized, since every downstream consumer compares `it.stateDir`
|
|
190038
|
-
* rather than re-normalizing it). The `..` escape check runs BEFORE the
|
|
190039
|
-
* segment-canonicality check so a value carrying BOTH is always reported as
|
|
190040
|
-
* an escape, never as a spelling to rewrite — `a/.././state` (segments `a`,
|
|
190041
|
-
* `..`, `.`, `state`) would, under the reverse order, suggest the canonical
|
|
190042
|
-
* spelling `"a/../state"`, a rewrite that still escapes the repo. `..` is
|
|
190043
|
-
* deliberately not in the canonicality predicate — it is an escape, not a
|
|
190044
|
-
* spelling. Deliberately does its own stripping rather than requiring an
|
|
190045
|
-
* already-normalized `value` — a caller may hand it either the raw
|
|
190046
|
-
* declaration or the edge's normalized form and get the same verdict, which
|
|
190047
|
-
* is what lets a unit test exercise `"./"` directly.
|
|
190048
|
-
*/
|
|
190049
|
-
const stateDirError = (value) => {
|
|
190050
|
-
const invalid = () => `"stateDir": must name a directory inside the repository, not the repo root, an absolute path, or a path outside it (got ${JSON.stringify(value)})`;
|
|
190051
|
-
if (value.trim() === "") return invalid();
|
|
190052
|
-
const stripped = canonicalStateDir(value);
|
|
190053
|
-
if (stripped === "" || stripped === ".") return invalid();
|
|
190054
|
-
if (stripped.startsWith("/")) return invalid();
|
|
190055
|
-
const segments = stripped.split("/");
|
|
190056
|
-
if (segments.includes("..")) return invalid();
|
|
190057
|
-
if (segments.some((segment) => segment === "." || segment === "")) {
|
|
190058
|
-
const canonical = segments.filter((segment) => segment !== "." && segment !== "").join("/");
|
|
190059
|
-
return `"stateDir": ${JSON.stringify(stripped)} is not a canonical path — write it as ${JSON.stringify(canonical)}`;
|
|
190060
|
-
}
|
|
190061
|
-
};
|
|
190021
|
+
* The engine's own plumbing directory — gtd's scratch/bookkeeping directory
|
|
190022
|
+
* (the review window's revert pathspec, the step guards' code-vs-plumbing
|
|
190023
|
+
* test) and the prefix `PatternConfig.ts`'s `stateFile` compiler prepends onto
|
|
190024
|
+
* every state's `file:` declaration. A plain const, not a workflow-level
|
|
190025
|
+
* declaration: there is nothing left to relocate it to, and the three sites
|
|
190026
|
+
* that must agree on it — the compiler that prepends, the validator that
|
|
190027
|
+
* re-checks, and the runtime guard that tests the prefix — all import this
|
|
190028
|
+
* one value rather than re-deriving a prefix string of their own.
|
|
190029
|
+
*/
|
|
190030
|
+
const STATE_DIR = ".gtd";
|
|
190062
190031
|
/** Which content kind a state declares, or `undefined` if none (a validation error). */
|
|
190063
190032
|
const contentKindOf = (state) => {
|
|
190064
190033
|
if (state.script !== void 0) return "script";
|
|
@@ -190497,18 +190466,6 @@ const validateKnownMode = (def, name, state) => {
|
|
|
190497
190466
|
return [`state "${name}": "mode" must name a mode this workflow knows (${known.length > 0 ? known.join(", ") : "none declared"}) (got "${state.mode}")`];
|
|
190498
190467
|
};
|
|
190499
190468
|
/**
|
|
190500
|
-
* `stateDir`, when DECLARED, must be non-blank — the same
|
|
190501
|
-
* blank/whitespace-only check `validateReviewBaseTemplate` runs for a string
|
|
190502
|
-
* `reviewBase` template. Deliberately checks nothing else: `stateDirError`'s
|
|
190503
|
-
* repo-root/absolute/escape rules apply to the RENDERED value, which this
|
|
190504
|
-
* load-time pass can't see (the bundled declaration is itself a template,
|
|
190505
|
-
* e.g. `<%= it.vars.stateDir %>`, whose var can arrive at runtime via a
|
|
190506
|
-
* `GTD_STATEDIR` override no load-time check could ever observe — see
|
|
190507
|
-
* `renderStateDirOrFail` in `src/Edge.ts` for the one site that does own
|
|
190508
|
-
* that rule).
|
|
190509
|
-
*/
|
|
190510
|
-
const validateStateDir = (def) => def.stateDir !== void 0 && def.stateDir.trim() === "" ? [`"stateDir" template must not be blank`] : [];
|
|
190511
|
-
/**
|
|
190512
190469
|
* When `reviewBase` is a STRING (the template form — see `StateDef.reviewBase`),
|
|
190513
190470
|
* its source must be non-blank: a literal `reviewBase: ""` (or whitespace-only)
|
|
190514
190471
|
* is an authoring error, distinct from the runtime concern of the RENDERED
|
|
@@ -190520,6 +190477,18 @@ const validateReviewBaseTemplate = (name, state) => {
|
|
|
190520
190477
|
if (typeof state.reviewBase !== "string") return [];
|
|
190521
190478
|
return state.reviewBase.trim() === "" ? [`state "${name}": "reviewBase" template must not be blank`] : [];
|
|
190522
190479
|
};
|
|
190480
|
+
/**
|
|
190481
|
+
* A compiled `file:` must sit under `STATE_DIR` (`.gtd`) — `PatternConfig.ts`'s
|
|
190482
|
+
* `stateFile` compiler already guarantees this for anything it compiles by
|
|
190483
|
+
* prepending the directory itself, so this can only fire for a definition
|
|
190484
|
+
* built directly in TypeScript (a hand-authored test fixture) that skipped
|
|
190485
|
+
* the compiler entirely.
|
|
190486
|
+
*/
|
|
190487
|
+
const validateFileUnderStateDir = (name, state) => {
|
|
190488
|
+
const file = state.file;
|
|
190489
|
+
if (file === void 0) return [];
|
|
190490
|
+
return file === ".gtd" || file.startsWith(`.gtd/`) ? [] : [`state "${name}": "file" must be under "${STATE_DIR}/" (got "${file}")`];
|
|
190491
|
+
};
|
|
190523
190492
|
/** Every `on` row parses, and its target names a defined state. */
|
|
190524
190493
|
const validateOnEdges = (name, state, names) => {
|
|
190525
190494
|
const errors = [];
|
|
@@ -190586,7 +190555,8 @@ const BESPOKE = {
|
|
|
190586
190555
|
on: (_def, name, state, names) => validateOnEdges(name, state, names),
|
|
190587
190556
|
retry: (_def, name, state, names) => validateRetry(name, state, names),
|
|
190588
190557
|
mode: (def, name, state) => validateKnownMode(def, name, state),
|
|
190589
|
-
reviewBase: (_def, name, state) => validateReviewBaseTemplate(name, state)
|
|
190558
|
+
reviewBase: (_def, name, state) => validateReviewBaseTemplate(name, state),
|
|
190559
|
+
file: (_def, name, state) => validateFileUnderStateDir(name, state)
|
|
190590
190560
|
};
|
|
190591
190561
|
/**
|
|
190592
190562
|
* All per-state rule checkers, run over one state: the two group-rule
|
|
@@ -190613,12 +190583,14 @@ const validateState = (def, name, names) => {
|
|
|
190613
190583
|
* `entries.manual` itself (see `validateEntries`); every state declares
|
|
190614
190584
|
* exactly one content kind; commit states carry no `actor` and no `on`;
|
|
190615
190585
|
* non-commit states carry an `actor`; every `modes:` entry declares at least
|
|
190616
|
-
* one non-blank `format`/`validate` command; a
|
|
190617
|
-
*
|
|
190618
|
-
*
|
|
190619
|
-
*
|
|
190620
|
-
*
|
|
190621
|
-
*
|
|
190586
|
+
* one non-blank `format`/`validate` command; a compiled `file:` sits under
|
|
190587
|
+
* `STATE_DIR` (`validateFileUnderStateDir` — the compiler's `stateFile` kind
|
|
190588
|
+
* already guarantees this for anything it compiles, so this only fires for a
|
|
190589
|
+
* hand-built definition that skipped the compiler); every state is reachable
|
|
190590
|
+
* from an entry root (`def.entries` — `default` plus every `entries.manual`
|
|
190591
|
+
* state) by walking `on` targets and `retry.otherwise` redirects (checked
|
|
190592
|
+
* only when `validateEntries` itself found no problem — see
|
|
190593
|
+
* `validateReachability`).
|
|
190622
190594
|
*
|
|
190623
190595
|
* Every OTHER per-field rule (`on`/`retry` targets resolving, `retry.max`
|
|
190624
190596
|
* being a non-negative integer, a text field's non-empty/commit-forbidden
|
|
@@ -190638,13 +190610,99 @@ const validateDefinition = (def) => {
|
|
|
190638
190610
|
return [
|
|
190639
190611
|
...entriesErrors,
|
|
190640
190612
|
...validateModes(def),
|
|
190641
|
-
...validateStateDir(def),
|
|
190642
190613
|
...names.flatMap((name) => validateState(def, name, names)),
|
|
190643
190614
|
...entriesErrors.length === 0 ? validateReachability(def, names) : []
|
|
190644
190615
|
];
|
|
190645
190616
|
};
|
|
190646
190617
|
//#endregion
|
|
190647
190618
|
//#region src/Machines.ts
|
|
190619
|
+
/**
|
|
190620
|
+
* Compile-time MACHINE FLATTENING — a raw `entry:`/`machines:` config in, a
|
|
190621
|
+
* flat `{ states, entries, tree }` out. This is the pure compile-time pass
|
|
190622
|
+
* that lets a workflow be AUTHORED as a tree of reusable, parameterized
|
|
190623
|
+
* "machines" (a gate/loop written once, instantiated several times with
|
|
190624
|
+
* different bindings — dedup — or a complex cluster grouped under one name
|
|
190625
|
+
* for source comprehension — encapsulation) while the rest of the compiler
|
|
190626
|
+
* (`src/PatternConfig.ts`) and the pure engine (`src/PatternMachine.ts`) only
|
|
190627
|
+
* ever see ordinary qualified states. No git, no filesystem, no Effect, no
|
|
190628
|
+
* runtime footprint — every export here is a plain function of its
|
|
190629
|
+
* arguments, and every finding is a pushed string on the shared `errors`
|
|
190630
|
+
* accumulator the caller threads through, never a throw.
|
|
190631
|
+
*
|
|
190632
|
+
* ## The raw shape
|
|
190633
|
+
*
|
|
190634
|
+
* ```yaml
|
|
190635
|
+
* entry:
|
|
190636
|
+
* default: <machine name> # which machine is the ROOT instance
|
|
190637
|
+
* machines:
|
|
190638
|
+
* <name>:
|
|
190639
|
+
* params: [<param>, ...]? # advisory only — documents which $params a caller may bind
|
|
190640
|
+
* entry: <local or ref key> # this machine's OWN default local, resolved recursively
|
|
190641
|
+
* states:
|
|
190642
|
+
* <local>: <StateDef> # an ordinary state
|
|
190643
|
+
* <local>: { machine: <name>, with: { ... } } # a REFERENCE — instantiates <name> as a child
|
|
190644
|
+
* ```
|
|
190645
|
+
*
|
|
190646
|
+
* ## Two passes over one `Instance` tree
|
|
190647
|
+
*
|
|
190648
|
+
* **Pass 1 (instantiate)** walks depth-first from `entry.default`'s machine
|
|
190649
|
+
* at the root (`path: ""`), turning every local `machine:` reference into a
|
|
190650
|
+
* child `Instance` at `parent.path + "." + local` — recording each local as
|
|
190651
|
+
* either a `state` or a `ref` (for later target resolution) along the way.
|
|
190652
|
+
* Guards push an error and skip only the offending subtree/local: a machine
|
|
190653
|
+
* reference cycle, an unknown `machine:` name, or a local name containing
|
|
190654
|
+
* `.` (which would be indistinguishable from a qualified reference path).
|
|
190655
|
+
* Any `machines:` entry never targeted by a reference (or the root) is
|
|
190656
|
+
* reported as unreferenced once both passes complete.
|
|
190657
|
+
*
|
|
190658
|
+
* **Pass 2 (emit)** clones each instance's own states into the flat
|
|
190659
|
+
* `states` map, keyed by qualified name (`qualify`), substituting `$param`
|
|
190660
|
+
* placeholders (whole-value only — a field whose ENTIRE value is `$name`,
|
|
190661
|
+
* never a substring, so bash `$var`/Eta `<%= %>` inside a `script:` survive
|
|
190662
|
+
* untouched) and rewriting every `on` target and `retry.otherwise` through
|
|
190663
|
+
* the resolver below.
|
|
190664
|
+
*
|
|
190665
|
+
* ## Bindings carry their scope
|
|
190666
|
+
*
|
|
190667
|
+
* A bound value cannot be resolved eagerly at the reference site (it is not
|
|
190668
|
+
* yet known whether it will be used as a TARGET, needing scope-aware
|
|
190669
|
+
* resolution, or spliced whole into a `describe:`/`prompt:` sentence, needing
|
|
190670
|
+
* none) and cannot be resolved lazily purely in the callee's own scope (it
|
|
190671
|
+
* was written in the CALLER's). So a `Binding` is a value plus the instance
|
|
190672
|
+
* path it was written in (`scope`) — see `Binding`/`Bindings`. Building a
|
|
190673
|
+
* child's bindings at a reference site substitutes each `with:` value
|
|
190674
|
+
* against the caller's OWN bindings first: if it names another binding, that
|
|
190675
|
+
* binding is passed down WHOLE, scope intact, which is what makes a param
|
|
190676
|
+
* threaded grandparent → parent → child resolve in the grandparent's
|
|
190677
|
+
* namespace rather than the parent's or child's.
|
|
190678
|
+
*
|
|
190679
|
+
* ## The resolver
|
|
190680
|
+
*
|
|
190681
|
+
* Resolving a target string written in instance `I` (`resolveCore`):
|
|
190682
|
+
*
|
|
190683
|
+
* 1. `$param` → look up `I.bindings[name]`; missing or non-string is an
|
|
190684
|
+
* "unbound param" finding. Otherwise resolve `binding.value` starting
|
|
190685
|
+
* over at `binding.scope` (recursing through however many bindings were
|
|
190686
|
+
* chained).
|
|
190687
|
+
* 2. Split on the first `.`. The leading segment must name a local of `I`:
|
|
190688
|
+
* a state with no remainder qualifies directly; a reference with no
|
|
190689
|
+
* remainder resolves THAT machine's own `entry:` recursively (so a chain
|
|
190690
|
+
* of references collapses to a real state); a reference with a remainder
|
|
190691
|
+
* recurses into the child using the remainder; anything else (not a
|
|
190692
|
+
* local, or a remainder against a state) is the sideways/upward refusal.
|
|
190693
|
+
*
|
|
190694
|
+
* The root machine's own `entry:` (which becomes `FlattenedWorkflow.entries.
|
|
190695
|
+
* default`) runs through the exact same resolver seeded at the ROOT instance
|
|
190696
|
+
* — accepting either a bare state path or an instance/reference-key path
|
|
190697
|
+
* uniformly — but surfaces a distinctly worded finding (`"entry.default"
|
|
190698
|
+
* names "y", which is not a state or machine reference`) since there is no
|
|
190699
|
+
* single owning state to blame.
|
|
190700
|
+
*
|
|
190701
|
+
* The `MachineNode` tree (`FlattenedWorkflow.tree`) is a plain projection of
|
|
190702
|
+
* the `Instance` tree Pass 1 built — which concrete states each instance
|
|
190703
|
+
* directly owns — kept around for a future visualizer (Package 05's
|
|
190704
|
+
* `Visualize.ts`) to render; this module only produces the data.
|
|
190705
|
+
*/
|
|
190648
190706
|
const isPlainObject$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
190649
190707
|
const describeType$1 = (v) => {
|
|
190650
190708
|
if (v === null) return "null";
|
|
@@ -190883,29 +190941,34 @@ const emitState = (stateRaw, instance, localName, instancesByPath, machinesRaw,
|
|
|
190883
190941
|
return out;
|
|
190884
190942
|
};
|
|
190885
190943
|
/**
|
|
190886
|
-
* Resolve an instance's own
|
|
190944
|
+
* Resolve every one of an instance's own machine-authored field values
|
|
190945
|
+
* (`MACHINE_FIELD_ENTRIES` — `model`, `system`), substituting a whole-value
|
|
190887
190946
|
* `$param` at the reference site. A whole-value `$param` that resolves to the
|
|
190888
190947
|
* empty string compiles away to "field absent" (mirrors the same rule in
|
|
190889
|
-
* `emitState`) — an instance that doesn't bind a
|
|
190890
|
-
* should not stamp `model: ""`.
|
|
190948
|
+
* `emitState`) — an instance that doesn't bind a value for this machine
|
|
190949
|
+
* should not stamp e.g. `model: ""`.
|
|
190891
190950
|
*/
|
|
190892
|
-
const
|
|
190893
|
-
const
|
|
190894
|
-
|
|
190895
|
-
|
|
190896
|
-
|
|
190897
|
-
|
|
190951
|
+
const resolveInstanceMachineFields = (rawMachine, instance, errors) => {
|
|
190952
|
+
const resolved = {};
|
|
190953
|
+
for (const [key] of MACHINE_FIELD_ENTRIES) {
|
|
190954
|
+
const raw = rawMachine[key];
|
|
190955
|
+
if (raw === void 0) continue;
|
|
190956
|
+
const substituted = substituteScalar(raw, instance, `machines.${instance.machine}`, errors);
|
|
190957
|
+
if (typeof raw === "string" && PARAM_REF.test(raw) && substituted === "") continue;
|
|
190958
|
+
resolved[key] = substituted;
|
|
190959
|
+
}
|
|
190960
|
+
return resolved;
|
|
190898
190961
|
};
|
|
190899
190962
|
/** Walk the instance tree, emitting every instance's own direct states into `states` (qualified). */
|
|
190900
190963
|
const emitTree = (instance, machinesRaw, states, scopes, instancesByPath, errors) => {
|
|
190901
190964
|
const rawMachine = machinesRaw[instance.machine];
|
|
190902
190965
|
const statesRaw = rawMachine["states"];
|
|
190903
|
-
const
|
|
190966
|
+
const resolvedFields = resolveInstanceMachineFields(rawMachine, instance, errors);
|
|
190904
190967
|
for (const [localName, local] of instance.locals) {
|
|
190905
190968
|
if (local.kind !== "state") continue;
|
|
190906
190969
|
const qualified = qualify(instance.path, localName);
|
|
190907
190970
|
const emitted = emitState(statesRaw[localName], instance, localName, instancesByPath, machinesRaw, errors);
|
|
190908
|
-
if (
|
|
190971
|
+
if (typeof emitted["prompt"] === "string") Object.assign(emitted, resolvedFields);
|
|
190909
190972
|
states[qualified] = emitted;
|
|
190910
190973
|
scopes[qualified] = instance.path;
|
|
190911
190974
|
}
|
|
@@ -191643,13 +191706,13 @@ const isSeededValidateCommand = (mode, command) => command === seededValidateCom
|
|
|
191643
191706
|
* <name>:
|
|
191644
191707
|
* format: <shell command> # at least one of format/validate
|
|
191645
191708
|
* validate: <shell command>
|
|
191646
|
-
* stateDir: <string>? # optional — an Eta template naming where this workflow keeps its own plumbing; defaults to ".gtd" (see PatternMachine.stateDirOf)
|
|
191647
191709
|
* entry:
|
|
191648
191710
|
* default: <machine name> # which machine is the ROOT instance
|
|
191649
191711
|
* machines:
|
|
191650
191712
|
* <name>:
|
|
191651
191713
|
* params: [<param>, ...]? # advisory only — documents which $params a caller may bind
|
|
191652
191714
|
* model: <string>? # optional, opaque harness hint — stamped onto every one of THIS machine's own `prompt` states; a state may NOT declare its own
|
|
191715
|
+
* system: <string>? # optional, machine-level only — the agent harness system prompt stamped onto every one of THIS machine's own `prompt` states; a "./" or "../" value is inlined from the declaring config file's directory (package 02); a state may NOT declare its own
|
|
191653
191716
|
* entry: <local or ref key> # this machine's OWN default local, resolved recursively
|
|
191654
191717
|
* states:
|
|
191655
191718
|
* <local>: # an ordinary state
|
|
@@ -191662,7 +191725,7 @@ const isSeededValidateCommand = (mode, command) => command === seededValidateCom
|
|
|
191662
191725
|
* max: <number>
|
|
191663
191726
|
* otherwise: <targetState>
|
|
191664
191727
|
* label: <string> # optional, opaque display name — never on a commit state
|
|
191665
|
-
* file: <string> # optional, an Eta template naming the state's steering file — never on a commit state
|
|
191728
|
+
* file: <string> # optional, an Eta template naming the state's steering file RELATIVE to ".gtd/" (the compiler prepends it automatically) — never on a commit state
|
|
191666
191729
|
* mode: <modeName> # optional, requires "file" — a built-in (qa/review) or a `modes:` entry; never on a commit state
|
|
191667
191730
|
* entry: true # optional — an EXTRA reachability root (WorkflowEntries.manual), enterable via `gtd --entry <this state's qualified name>`; distinct from the top-level `entry:` key above
|
|
191668
191731
|
* <local>: { machine: <name>, with: { ... } } # a REFERENCE — instantiates <name> as a child, see src/Machines.ts
|
|
@@ -191705,20 +191768,15 @@ const isSeededValidateCommand = (mode, command) => command === seededValidateCom
|
|
|
191705
191768
|
* `isSeededValidateCommand` lets the edge (`src/SteeringMode.ts`) tell gtd's
|
|
191706
191769
|
* own seeding apart from a genuine user override.
|
|
191707
191770
|
*
|
|
191708
|
-
* ## `
|
|
191709
|
-
*
|
|
191710
|
-
* A
|
|
191711
|
-
*
|
|
191712
|
-
*
|
|
191713
|
-
*
|
|
191714
|
-
*
|
|
191715
|
-
*
|
|
191716
|
-
*
|
|
191717
|
-
* compiles to `undefined`, so `stateDirOf`'s `.gtd` default applies — a
|
|
191718
|
-
* workflow declaring nothing behaves exactly as before this key existed. The
|
|
191719
|
-
* bundled template renders this from its own ordinary `vars.stateDir` (the
|
|
191720
|
-
* knob a user actually overrides) — this compiler never renders it itself,
|
|
191721
|
-
* the same discipline as a state's own `file:`.
|
|
191771
|
+
* ## `file:` — resolved under `.gtd/` automatically
|
|
191772
|
+
*
|
|
191773
|
+
* A state's `file:` names a path RELATIVE to `PatternMachine.STATE_DIR`
|
|
191774
|
+
* (`.gtd`) — the `stateFile` field kind (`compileStateFile`, below) prepends
|
|
191775
|
+
* the directory after rejecting a `..` segment, an absolute path, or a
|
|
191776
|
+
* declared `.gtd/` prefix (each a load-time error, never silently rewritten).
|
|
191777
|
+
* There is no per-workflow relocation knob any more: `.gtd` is a fixed
|
|
191778
|
+
* literal every consumer (the review window's revert pathspec, the step
|
|
191779
|
+
* guards' code-vs-plumbing test) agrees on.
|
|
191722
191780
|
*
|
|
191723
191781
|
* ## File references
|
|
191724
191782
|
*
|
|
@@ -191844,37 +191902,19 @@ const mergeModes = (base, override) => {
|
|
|
191844
191902
|
};
|
|
191845
191903
|
return merged;
|
|
191846
191904
|
};
|
|
191847
|
-
/**
|
|
191848
|
-
* Compile the `stateDir:` key — the raw Eta template source for where this
|
|
191849
|
-
* workflow keeps its own plumbing (see `PatternMachine.WorkflowDefinition.stateDir`).
|
|
191850
|
-
* An absent key compiles to `undefined` — the definition then carries none,
|
|
191851
|
-
* and `stateDirOf`'s `.gtd` default applies. A non-string value pushes one
|
|
191852
|
-
* load error and drops the value, never guessed at. Never rendered here —
|
|
191853
|
-
* the pure engine carries the string verbatim, the same discipline as a
|
|
191854
|
-
* state's own `file:`.
|
|
191855
|
-
*/
|
|
191856
|
-
const compileStateDir = (raw, errors) => {
|
|
191857
|
-
if (raw === void 0) return void 0;
|
|
191858
|
-
if (typeof raw !== "string") {
|
|
191859
|
-
errors.push(`"stateDir" must be a string, got ${describeType(raw)}`);
|
|
191860
|
-
return;
|
|
191861
|
-
}
|
|
191862
|
-
return raw;
|
|
191863
|
-
};
|
|
191864
191905
|
/** Every state property a flattened state may carry — `STATE_FIELDS`'s own key set, in table order. */
|
|
191865
191906
|
const KNOWN_STATE_KEYS = new Set(Object.keys(STATE_FIELDS));
|
|
191866
191907
|
const KNOWN_TOP_KEYS = /* @__PURE__ */ new Set([
|
|
191867
191908
|
"entry",
|
|
191868
191909
|
"machines",
|
|
191869
191910
|
"vars",
|
|
191870
|
-
"modes"
|
|
191871
|
-
"stateDir"
|
|
191911
|
+
"modes"
|
|
191872
191912
|
]);
|
|
191873
191913
|
const KNOWN_MACHINE_KEYS = /* @__PURE__ */ new Set([
|
|
191874
191914
|
"params",
|
|
191875
191915
|
"entry",
|
|
191876
191916
|
"states",
|
|
191877
|
-
|
|
191917
|
+
...MACHINE_FIELD_ENTRIES.map(([key]) => key)
|
|
191878
191918
|
]);
|
|
191879
191919
|
const KNOWN_REF_KEYS = /* @__PURE__ */ new Set(["machine", "with"]);
|
|
191880
191920
|
/** A state-level key removed by the `entry:`/`machines:` rewrite (or, for `memory`, by the machine-scoped-memory restructure), naming its replacement so a stale config's error points somewhere useful instead of a bare "unknown key". The state-level `model` case is instead caught pre-flatten by `LEGACY_AUTHORED_STATE_KEY_HINTS`/`validateMachineStateKeys`, because by the time a state reaches `KNOWN_STATE_KEYS`/`compileState` its `model` may be a legitimately machine-stamped key. */
|
|
@@ -191970,32 +192010,38 @@ const validateMachineStateKeys = (machineName, machineRaw, errors) => {
|
|
|
191970
192010
|
}
|
|
191971
192011
|
};
|
|
191972
192012
|
/**
|
|
191973
|
-
*
|
|
191974
|
-
* (`validateMachineStateKeys`, above, rejects the
|
|
191975
|
-
* non-empty string, else a load error naming the
|
|
191976
|
-
* a machine stamps onto its own `prompt` states
|
|
191977
|
-
* `
|
|
191978
|
-
* then only the type guard
|
|
192013
|
+
* Every machine-authored field (`MACHINE_FIELD_ENTRIES`) — the ONLY place any
|
|
192014
|
+
* of them may be authored (`validateMachineStateKeys`, above, rejects the
|
|
192015
|
+
* state-level form): a non-empty string, else a load error naming the
|
|
192016
|
+
* machine. Validates the value a machine stamps onto its own `prompt` states
|
|
192017
|
+
* (`src/Machines.ts`'s `resolveInstanceMachineFields`/`emitTree`); the
|
|
192018
|
+
* per-state `compileText` (below) is then only the type guard
|
|
192019
|
+
* over that STAMPED value.
|
|
191979
192020
|
*/
|
|
191980
|
-
const
|
|
191981
|
-
|
|
191982
|
-
|
|
192021
|
+
const validateMachineFieldValues = (machineName, machineRaw, errors) => {
|
|
192022
|
+
for (const [key, spec] of MACHINE_FIELD_ENTRIES) {
|
|
192023
|
+
const value = machineRaw[key];
|
|
192024
|
+
if (value === void 0) continue;
|
|
192025
|
+
if (spec.nonEmpty === true && (typeof value !== "string" || value === "")) errors.push(`machines.${machineName}: "${key}" must be a non-empty string`);
|
|
192026
|
+
}
|
|
191983
192027
|
};
|
|
191984
|
-
/** Does `machineRaw` declare at least one of its OWN (non-reference) states with content kind `prompt`? Mirrors exactly which states `src/Machines.ts` stamps a machine-
|
|
192028
|
+
/** Does `machineRaw` declare at least one of its OWN (non-reference) states with content kind `prompt`? Mirrors exactly which states `src/Machines.ts` stamps a machine-authored field onto — a reference local's states belong to the CHILD machine, never this one. */
|
|
191985
192029
|
const machineHasPromptState = (machineRaw) => {
|
|
191986
192030
|
const statesRaw = machineRaw["states"];
|
|
191987
192031
|
if (!isPlainObject$1(statesRaw)) return false;
|
|
191988
192032
|
return Object.values(statesRaw).some((def) => isPlainObject$1(def) && !isRefRaw(def) && typeof def["prompt"] === "string");
|
|
191989
192033
|
};
|
|
191990
192034
|
/**
|
|
191991
|
-
* A machine declaring
|
|
191992
|
-
* its own states is a load error, not a silent no-op — see
|
|
191993
|
-
* "BOUNDARY CORRECTION" note in package 02: the
|
|
191994
|
-
* emitted state, so the declaration does nothing.
|
|
192035
|
+
* A machine declaring a machine-authored field with no `prompt`-content state
|
|
192036
|
+
* anywhere among its own states is a load error, not a silent no-op — see
|
|
192037
|
+
* this module's "BOUNDARY CORRECTION" note in package 02: the field would
|
|
192038
|
+
* never land on any emitted state, so the declaration does nothing.
|
|
191995
192039
|
*/
|
|
191996
|
-
const
|
|
191997
|
-
|
|
191998
|
-
|
|
192040
|
+
const validateMachineFieldsTakeEffect = (machineName, machineRaw, errors) => {
|
|
192041
|
+
for (const [key] of MACHINE_FIELD_ENTRIES) {
|
|
192042
|
+
if (machineRaw[key] === void 0) continue;
|
|
192043
|
+
if (!machineHasPromptState(machineRaw)) errors.push(`machine "${machineName}": declares "${key}" but has no "prompt" state — this would never take effect`);
|
|
192044
|
+
}
|
|
191999
192045
|
};
|
|
192000
192046
|
const validateMachinesShape = (raw, errors) => {
|
|
192001
192047
|
if (raw === void 0) return;
|
|
@@ -192009,8 +192055,8 @@ const validateMachinesShape = (raw, errors) => {
|
|
|
192009
192055
|
if (unknownMachineKeys.length > 0) errors.push(`machine "${machineName}": unknown key(s) ${unknownMachineKeys.join(", ")}`);
|
|
192010
192056
|
validateMachineRefs(machineName, machineRaw, errors);
|
|
192011
192057
|
validateMachineStateKeys(machineName, machineRaw, errors);
|
|
192012
|
-
|
|
192013
|
-
|
|
192058
|
+
validateMachineFieldValues(machineName, machineRaw, errors);
|
|
192059
|
+
validateMachineFieldsTakeEffect(machineName, machineRaw, errors);
|
|
192014
192060
|
}
|
|
192015
192061
|
};
|
|
192016
192062
|
const formatErrors = (errors) => `workflow config:\n${errors.map((e) => ` - ${e}`).join("\n")}`;
|
|
@@ -192042,6 +192088,16 @@ const resolveContent = (value, configDir, where, errors, fileRefs = nodeFileRefR
|
|
|
192042
192088
|
/** A local is a REFERENCE iff its raw value carries a `machine` key — the same predicate `src/Machines.ts` uses. */
|
|
192043
192089
|
const isRefRaw = (v) => isPlainObject$1(v) && typeof v["machine"] === "string";
|
|
192044
192090
|
/**
|
|
192091
|
+
* Machine-authored fields whose `./`/`../` value is a file reference
|
|
192092
|
+
* (`FieldSpec.fileRef`), derived from `MACHINE_FIELD_ENTRIES` rather than
|
|
192093
|
+
* hand-listed — the declarative escape hatch for a field whose behaviour
|
|
192094
|
+
* doesn't fit `nonEmpty`/`commit`/`requires`. Deliberately NOT every
|
|
192095
|
+
* machine-authored field: `model` is an opaque harness hint today, and
|
|
192096
|
+
* looping it here would turn `model: ./tiers/fast.txt` into a file read — a
|
|
192097
|
+
* load error for a value that used to work.
|
|
192098
|
+
*/
|
|
192099
|
+
const MACHINE_FILE_REF_FIELDS = MACHINE_FIELD_ENTRIES.filter(([, spec]) => spec.fileRef === true).map(([key]) => key);
|
|
192100
|
+
/**
|
|
192045
192101
|
* Inline every `./`/`../` content file reference in ONE raw `workflow:` value
|
|
192046
192102
|
* against `configDir` — the directory of the `.gtdrc` that DECLARED it — and
|
|
192047
192103
|
* return a copy with those references replaced by the referenced file's text.
|
|
@@ -192075,17 +192131,31 @@ const inlineStateFileRefs = (def, machineName, local, configDir, errors, fileRef
|
|
|
192075
192131
|
}
|
|
192076
192132
|
return next;
|
|
192077
192133
|
};
|
|
192078
|
-
/**
|
|
192134
|
+
/**
|
|
192135
|
+
* Inline a machine's own file-reference fields (`MACHINE_FILE_REF_FIELDS`,
|
|
192136
|
+
* e.g. `system: ./persona.md`) plus every state's content file references;
|
|
192137
|
+
* a malformed machine shape passes through untouched. The machine-level pass
|
|
192138
|
+
* runs FIRST and unconditionally — only the states loop is gated on a
|
|
192139
|
+
* well-formed `states:` — so a machine with a malformed `states:` still gets
|
|
192140
|
+
* its own `system:` reference resolved (and its missing-file error reported)
|
|
192141
|
+
* rather than skipping inlining entirely.
|
|
192142
|
+
*/
|
|
192079
192143
|
const inlineMachineFileRefs = (machineRaw, machineName, configDir, errors, fileRefs = nodeFileRefReader) => {
|
|
192080
192144
|
if (!isPlainObject$1(machineRaw)) return machineRaw;
|
|
192145
|
+
const next = { ...machineRaw };
|
|
192146
|
+
for (const key of MACHINE_FILE_REF_FIELDS) {
|
|
192147
|
+
const value = machineRaw[key];
|
|
192148
|
+
if (typeof value !== "string" || !isFileReference(value)) continue;
|
|
192149
|
+
const resolved = resolveContent(value, configDir, `machine "${machineName}" (${key})`, errors, fileRefs);
|
|
192150
|
+
if (resolved !== void 0) next[key] = resolved;
|
|
192151
|
+
}
|
|
192081
192152
|
const rawStates = machineRaw["states"];
|
|
192082
|
-
if (
|
|
192083
|
-
|
|
192084
|
-
|
|
192085
|
-
|
|
192086
|
-
|
|
192087
|
-
|
|
192088
|
-
};
|
|
192153
|
+
if (isPlainObject$1(rawStates)) {
|
|
192154
|
+
const states = {};
|
|
192155
|
+
for (const [local, def] of Object.entries(rawStates)) states[local] = isPlainObject$1(def) ? inlineStateFileRefs(def, machineName, local, configDir, errors, fileRefs) : def;
|
|
192156
|
+
next["states"] = states;
|
|
192157
|
+
}
|
|
192158
|
+
return next;
|
|
192089
192159
|
};
|
|
192090
192160
|
const inlineWorkflowFileRefs = (rawWorkflow, configDir, errors, fileRefs = nodeFileRefReader) => {
|
|
192091
192161
|
if (!isPlainObject$1(rawWorkflow)) return rawWorkflow;
|
|
@@ -192208,6 +192278,43 @@ const compileText = (raw, key, name, errors) => {
|
|
|
192208
192278
|
return value;
|
|
192209
192279
|
};
|
|
192210
192280
|
/**
|
|
192281
|
+
* `file:` — a plain string (`compileText`'s own shape), then prepended under
|
|
192282
|
+
* `STATE_DIR` after three load-time rejections: a `..` segment (an escape), a
|
|
192283
|
+
* leading `/` (an absolute path), or an already-declared `STATE_DIR` prefix
|
|
192284
|
+
* (what would otherwise silently double up into `.gtd/.gtd/...`). None of
|
|
192285
|
+
* these is a rewrite — a leading `./` or a doubled `//` is neither accepted
|
|
192286
|
+
* nor normalized, since reintroducing canonical-spelling machinery for `file:`
|
|
192287
|
+
* buys nothing when removing it is the point. A blank value is left alone
|
|
192288
|
+
* (returned as `""`, not prepended) so the field's own generic `nonEmpty` rule
|
|
192289
|
+
* (`STATE_FIELDS.file`, walked by `validateFieldRules`) still catches it —
|
|
192290
|
+
* prepending onto blank would hide the blank behind a non-empty
|
|
192291
|
+
* `"${STATE_DIR}/"` string. `undefined` (absent, or the type mismatch
|
|
192292
|
+
* `compileText` already reported) short-circuits with nothing left to check.
|
|
192293
|
+
*
|
|
192294
|
+
* A templated `file:` (e.g. `file: <%= it.vars.x %>`) is checked against its
|
|
192295
|
+
* SOURCE, not its rendered form — a var supplying `../REVIEW.md` at runtime
|
|
192296
|
+
* passes every one of these checks and renders outside `.gtd/` at the edge.
|
|
192297
|
+
* Accepted, not guarded: see this module's own `file:` section for the
|
|
192298
|
+
* rationale.
|
|
192299
|
+
*/
|
|
192300
|
+
const compileStateFile = (raw, key, name, ctx) => {
|
|
192301
|
+
const value = compileText(raw, key, name, ctx.errors);
|
|
192302
|
+
if (value === void 0 || value === "") return value;
|
|
192303
|
+
if (value.split("/").includes("..")) {
|
|
192304
|
+
ctx.errors.push(`state "${name}": "${key}" must not contain a ".." segment (got "${value}")`);
|
|
192305
|
+
return;
|
|
192306
|
+
}
|
|
192307
|
+
if (value.startsWith("/")) {
|
|
192308
|
+
ctx.errors.push(`state "${name}": "${key}" must not be an absolute path (a leading "/") (got "${value}")`);
|
|
192309
|
+
return;
|
|
192310
|
+
}
|
|
192311
|
+
if (value === ".gtd" || value.startsWith(`.gtd/`)) {
|
|
192312
|
+
ctx.errors.push(`state "${name}": "${key}" is resolved under "${STATE_DIR}/" automatically — drop the "${STATE_DIR}/" prefix (got "${value}")`);
|
|
192313
|
+
return;
|
|
192314
|
+
}
|
|
192315
|
+
return `${STATE_DIR}/${value}`;
|
|
192316
|
+
};
|
|
192317
|
+
/**
|
|
192211
192318
|
* A boolean state flag (`reviewWindow`/`requireProgress`/`answerGate`/
|
|
192212
192319
|
* `entry`): `true` only when the raw value is the literal `true`; a
|
|
192213
192320
|
* non-boolean is a config error; `false` (or absent) compiles away to
|
|
@@ -192269,6 +192376,7 @@ const compileContentRef = (raw, key, name, ctx) => {
|
|
|
192269
192376
|
const COMPILE = {
|
|
192270
192377
|
actor: (raw, key, name, ctx) => compileText(raw, key, name, ctx.errors),
|
|
192271
192378
|
text: (raw, key, name, ctx) => compileText(raw, key, name, ctx.errors),
|
|
192379
|
+
stateFile: compileStateFile,
|
|
192272
192380
|
mode: (raw, key, name, ctx) => compileText(raw, key, name, ctx.errors),
|
|
192273
192381
|
content: compileContentRef,
|
|
192274
192382
|
flag: (raw, key, name, ctx) => compileBooleanFlag(raw, key, name, ctx.errors),
|
|
@@ -192379,7 +192487,6 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
|
|
|
192379
192487
|
const vars = compileVarsMap(raw.vars, errors);
|
|
192380
192488
|
const seeded = Object.fromEntries(builtInModeNames().map((name) => [name, { validate: seededValidateCommand(name) }]));
|
|
192381
192489
|
const modes = mergeModes(mergeModes(seeded, compileModesMap(raw.modes, errors)), rcModes);
|
|
192382
|
-
const stateDir = compileStateDir(raw.stateDir, errors);
|
|
192383
192490
|
validateMachinesShape(raw.machines, errors);
|
|
192384
192491
|
const flattened = flattenMachines(raw, errors);
|
|
192385
192492
|
if (flattened.entries === void 0) throw new Error(formatErrors(errors));
|
|
@@ -192396,8 +192503,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
|
|
|
192396
192503
|
default: flattened.entries.default,
|
|
192397
192504
|
manual
|
|
192398
192505
|
},
|
|
192399
|
-
modes
|
|
192400
|
-
...stateDir !== void 0 ? { stateDir } : {}
|
|
192506
|
+
modes
|
|
192401
192507
|
};
|
|
192402
192508
|
assertScopesCoverStates(Object.keys(states), flattened.scopes, errors);
|
|
192403
192509
|
const definitionErrors = validateDefinition(definition);
|
|
@@ -192412,7 +192518,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
|
|
|
192412
192518
|
};
|
|
192413
192519
|
//#endregion
|
|
192414
192520
|
//#region src/workflows/unified.yaml
|
|
192415
|
-
var unified_default = "# The unified gtd workflow template — gtd's BUILT-IN DEFAULT workflow. When no\n# `workflow:` key is configured anywhere in the cwd→home config chain,\n# src/Config.ts runs this template directly, so a state command works with no\n# configuration at all. `gtd init` seeds only the settings a project tunes\n# (vars.testCommand + a modes: formatting suggestion), never the workflow\n# itself. See the README's \"How it works\" and \"Driving the loop\" sections for\n# the full walkthrough.\n#\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/templates.ts does the compiling). Every content string below\n# is INLINE (no `./`-relative file references): the bundled template ships\n# inside a single-file build (`dist/gtd.bundle.mjs`), so its content must\n# already be resolved at BUILD time. `renderInitConfig` (src/workflows/\n# templates.ts) can MATERIALIZE this raw source into a `.gtdrc.json`\n# `workflow:` value — the way to eject/customize the machine, and the hermetic\n# test fixture — but `gtd init` itself no longer writes the workflow.\n#\n# ── Authored with MACHINES (entry:/machines:) ───────────────────────────────\n#\n# This template is written as a tree of reusable, parameterized MACHINES (see\n# src/Machines.ts) — a gate/loop written once and instantiated several times\n# with different `with:` bindings (dedup), or a complex cluster grouped under\n# one name for source comprehension (encapsulation). Every reference below is\n# an ordinary `{ machine: <name>, with: { ... } }` local, expanded at\n# compile-time into concrete, qualified states before the pure engine ever\n# sees the definition — no runtime footprint.\n#\n# MACHINE BOUNDARIES LINE UP WITH CONVERSATIONAL IDENTITY: a machine that holds\n# an identity (a planner or a coder persona) declares its own `model:` ONCE, at\n# the machine level (stamped onto every one of its own `prompt` states — see\n# src/Machines.ts's `resolveInstanceModel`), instead of repeating `model:` on\n# every state; no state anywhere declares `model:` or `memory:` directly any\n# more (the memory key is computed from the machine tree — see\n# src/PatternMachine.ts's `memoryScopeAt` / src/Edge.ts's `memoryKeyFor`).\n#\n# * entryGate (dedup ×2) — the green-baseline gate (check + human blocked)\n# guarding the single start (`start-gate`) and\n# the review (`review-gate`) entries. Its check\n# script IS the shared `&suiteCheck` anchor\n# (also aliased by `fix-precheck`, outside this\n# machine) — every entry blocks on any red tree,\n# full stop; there is no baseline-aware second\n# run any more, since `unwind` (below) already\n# put the tree back to the baseline before this\n# gate ever runs. Identity-free.\n# * healthGate (dedup ×2) — the check/escalate health pair shared by the\n# build tail's own tail (`build.health.*`) and\n# the per-package build's own tail\n# (`packages.item.health.*`) — each caller's OWN\n# `fix` state (a coder-identity action) lives\n# beside this machine, not inside it, since\n# fixing IS a coder action and this machine has\n# no identity of its own. Identity-free.\n# * questionGate (dedup ×2) — the check/answer pair shared by the design and\n# architecture phases' own open-questions gate\n# (`design.gate.*` / `architecture.gate.*`): a\n# shared probe script decides whether the\n# phase's steering file still has unanswered\n# questions, and only then rests the process at\n# a human answer gate. Identity-free.\n# * designPlan (encap) — the triage phase's one conversation: group the\n# diff that started the process into ordered,\n# classified concerns and resolve the PRODUCT\n# ones' open questions (`design.*`: triage,\n# gate.check, gate.answer). ▸ planner.\n# * archPlan (encap) — the technical phase's one conversation: a COLD\n# read of the settled concerns, resolving the\n# TECHNICAL open questions, then a mechanical\n# decomposition into package files\n# (`architecture.*`: author, gate.check,\n# gate.answer, decompose). A separate machine\n# from `designPlan` (not one fused machine) —\n# its own memory scope, since there may be a\n# handover between the user-facing and the\n# technical phase. ▸ planner.\n# * humanReview (encap) — the shared review tail, nested INSIDE\n# `buildTail` (`build.review.*`: reviewing →\n# await-review → deciding → collecting) — see\n# \"Why humanReview is nested, not a root sibling\"\n# below. ▸ planner — `collecting` only judges\n# whether the round is actionable; there is no\n# coder-identity follow-through inside this\n# machine any more (see the root's own\n# `re-unwind` state).\n# * specReview (encap) — the per-package agentic review, a SINGLE\n# state (`packages.item.spec.review`) — a\n# machine whose entire job is to be a separate\n# mind from the implementer. ▸ planner — fixing\n# the reviewer's own findings is a coder action,\n# so it lives in `packages.item.fix-spec`\n# instead.\n# * packageLoop (encap) — the per-package build queue\n# (`packages.picking`). Identity-free — the\n# actual per-package build identity lives in its\n# own `item` child (packageItem, below).\n# * packageItem (encap) — the per-package build identity (`packages.\n# item.*`: building, fix-suite, fix-spec,\n# closing), plus its own `health` and `spec`\n# children. ▸ coder.\n# * buildTail (encap) — the process's build identity (`build.*`:\n# squashing, fix), plus its own `health` child\n# AND its own `review` child\n# (humanReview, above). No monolithic \"build the\n# whole plan\" state any more — every package is\n# built inside `packages.item`, and the queue\n# drains straight into `build.review`. ▸ coder.\n#\n# ── Why humanReview is nested, not a root sibling ───────────────────────────\n#\n# `humanReview` sits UNDER `buildTail` (`build.review`) rather than beside it\n# at the root, because a machine's memory scope is its dotted instance path\n# (see src/PatternMachine.ts's `memoryScopeAt`): entering a DESCENDANT scope\n# never breaks the parent's unbroken run, but entering a sibling does. On the\n# ORDINARY path (packages -> build.review -> squashing) that buys nothing —\n# `build`'s own scope has no prior turn to resume, since every package is\n# built inside `packages.item` instead. The one concrete case where it\n# matters is `gtd --entry fix-precheck`: a red suite drops into `build.fix`,\n# which opens `build`'s own scope, and the whole run from there —\n# `build.fix` -> `build.health.check` -> `build.review.*` -> `build.squashing`\n# — stays inside that one subtree, so `build.squashing` drafts the finale\n# from the SAME session that made the fixes, instead of a root-level sibling\n# forcing it to re-derive the range from scratch on every pass through the\n# tail. That one benefit is worth keeping over hoisting `humanReview` back to\n# a root sibling, which would otherwise be a repo-wide rename with no\n# behavioural gain. (The OTHER coder turn a feedback round used to resume —\n# the implementer's own follow-through on review feedback — is retired\n# outright: a hand-edit made during review is now re-planned from scratch via\n# the root's own `re-unwind` state, never built upon, so there is no\n# continuity to preserve on that path any more.)\n# The reviewer itself is unaffected either way: `build.review.*` is its own\n# scope regardless of where it's nested, so it never resumes the builder's\n# session.\n#\n# ── One flow: triage into concerns, then packages, then the shared tail ────\n#\n# ANY change to the tree starts the process — a hand-edit to real code, a\n# scratch note, anything at all — because it's a SKETCH, not finished work:\n# there is no fork on which steering file the human happened to create;\n# `idle` has exactly one outgoing edge, into `unwind`, which reverts that\n# whole diff back out of the working tree before planning starts (its intent\n# survives in history for `design.triage` to read; the squash finale means it\n# never reaches the final commit either way). `unwind` itself has exactly one\n# outgoing edge, into `start-gate`.\n#\n# EVERY entry gates on a green baseline before proceeding, and all three now\n# keep the exact same plain rule: block on any red tree, full stop. A red run\n# at any of them halts at a human blocked gate (fix the failures and re-run,\n# exactly like the escalate states). `start-gate` needs no baseline-aware\n# second run any more — by the time it runs, `unwind` has already put the\n# tree back to the baseline, so ONE suite run there IS the baseline verdict.\n#\n# Once green, `design.triage` reads the diff that started the process (never\n# rendered into the prompt — it names the start commit and reads the range up\n# to the unwind commit itself) and groups it into an ORDERED list of\n# CONCERNS, each one leaving the suite green on its own, classifying each as\n# PRODUCT or TECHNICAL. It raises open questions for the product ones only,\n# and folds EVERYTHING the start diff added into the concerns — a scratch\n# sketch and a real hand-edit alike, since `unwind` already emptied the tree\n# of both; nothing is left to delete. `design.gate` is a shared check+answer\n# pair: its check probes `requirementsFile` for unanswered questions and only\n# rests the process at a human answer gate when some remain — a\n# question-free phase skips the human stop entirely.\n#\n# Once the product questions are settled, `architecture.author` picks up as a\n# COLD reader (a separate machine, a separate memory scope — it reads\n# `requirementsFile` in full rather than resuming design's conversation),\n# develops the *how* for each settled concern, and raises only TECHNICAL open\n# questions — routed through the same shared `questionGate` shape\n# (`architecture.gate`) over `architectureFile`. Once those are settled,\n# `architecture.decompose` is a purely mechanical write-out: one package file\n# per concern, in the settled order — no merge/split judgement, since that\n# already happened in triage's grouping.\n#\n# From there the per-package build queue (`packages.*`) and the shared review\n# + squash tail (`build.*`) are UNCHANGED from before this restructure — see\n# their own machine doc comments below for the per-package build/review/\n# health mechanics.\n#\n# A second entry, `gtd --entry review-gate.check`, jumps there directly and,\n# once green, to `build.review.reviewing` to review a foreign branch through\n# the same shared tail. A third, `gtd --entry fix-precheck`, jumps there: a\n# red suite drops straight into the shared `build.fix` loop and out through the\n# review + squash tail; a green suite is a no-op back to `idle`. Both states\n# declare `entry: true` — otherwise unreachable from `entries.default`.\n#\n# All entries converge at `build.review.reviewing` -> `build.review.await-review`.\n# Ticking a REVIEW.md box means only \"I reviewed this hunk\"; a COMMENT (not an\n# unticked box) is what asks for changes. Every human step at\n# `build.review.await-review` goes to `build.review.deciding`, which CAPTURES\n# the raw material (never interprets it) and routes on the step's shape —\n# THREE edges out of the review tail, not one:\n# * the SIGN-OFF edge: no comment at all (no REVIEW.md note, no code edit)\n# -> the diff is a bare REVIEW.md deletion ->\n# build.squashing -> done (the whole process collapses into one commit).\n# * the NON-ACTIONABLE edge: some comment exists, but `build.review.collecting`\n# judges it non-actionable (the human left only an approving remark, e.g. a\n# compliment with no code edit and no substantive note) -> CONSUMES the raw\n# capture (deletes it, the only change this turn) -> straight to\n# build.squashing too, WITHOUT spending a design lap producing nothing, and\n# with nothing left over to leak into the finale. The outcome is encoded by\n# which paths `collecting`'s own diff touches, never by a sentinel a file\n# pattern can't tell apart from real content — and never by a CLEAN turn\n# either, which stays the ordinary fruitless-dispatch attempt (no `C` row\n# here), not a verdict.\n# * the ACTIONABLE edge: `collecting` judges the round actionable (a note in\n# REVIEW.md, or a hand-edit to code) -> marks the raw capture (a real\n# edit) -> the root's own `re-unwind` state reverts the human's\n# review-round commit out of the working tree, scoped to real code (never\n# the review file, already gone, and never `stateDir`) -> `design.triage`\n# re-derives the work from that history as intent only — the human's\n# hand-edit is a SKETCH to be re-planned, never built upon. This is a full\n# development lap (triage -> gate -> architecture -> gate -> packages ->\n# build.review again), not a quick fix-and-re-review; there is no\n# baseline check on the way back in (`re-unwind` is not an entry, so\n# `entryGate` never runs on this path).\n# One dead end never commits: the review-doc guard (src/StepGuards.ts) REFUSES\n# a deleted REVIEW.md — restore it, or leave a note (or edit code) to request\n# changes. A tick only records that a hunk was read; it never gates a land.\n#\n# The shared health pair (`build.health.check`/`.escalate`, plus its own\n# `build.fix`) no longer serves a feedback loop at all — with the implementer's\n# own coder-identity follow-through retired, the only edge into it is\n# `gtd --entry fix-precheck`'s red branch; they are not dead code, just\n# reachable from one place instead of two. The\n# per-package loop keeps its OWN health pair (`packages.item.health.check`/\n# `.escalate`, plus `packages.item.fix-suite`) because its green edge targets\n# the per-package `packages.item.spec.review` gate instead.\n#\n# Every `on` pattern's path below is written as the matching var's Eta\n# template (e.g. `\"A <%= it.vars.feedbackFile %>\"`), not a literal `.gtd/...`\n# path — repointing a var reroutes the machine along with every template that\n# reads/writes it (see the README's \"Variables\" section).\n\n# The definition-level declaration `stateDirOf` (src/PatternMachine.ts) reads —\n# a DIFFERENT thing from the `vars.stateDir` below despite sharing a name\n# (see that var's own comment). Rendered from it here so the two stay in\n# lockstep: relocating the var relocates what the engine treats as plumbing.\nstateDir: <%= it.vars.stateDir %>\n\nvars:\n testCommand: npm test\n # Where gtd keeps its own scratch/bookkeeping (the check scripts' temp output\n # file, review-deciding's state-file exclusion). Independent of the per-file\n # vars below — relocating a file wholesale means setting both. An ordinary\n # var — the knob a user overrides — distinct from the top-level `stateDir:`\n # declaration above, which renders its value from this one; that is why the\n # two share a name.\n stateDir: .gtd\n # The only per-file var gtd never writes and never reads — the human's own\n # sketch pad, named so the one \"open steering file\" keybinding has an\n # answer at `idle` too.\n todoFile: .gtd/TODO.md\n requirementsFile: .gtd/REQUIREMENTS.md\n architectureFile: .gtd/ARCHITECTURE.md\n # Stamped (never authored, never read for its content) by design.gate.check\n # / architecture.gate.check — its mere presence is the routing signal that\n # the current phase's steering file still has unanswered questions.\n questionsFile: .gtd/QUESTIONS.md\n packagesDir: .gtd/packages\n nextFile: .gtd/NEXT.md\n # Written by packages.item.building when a package's acceptance criteria are\n # already met (an earlier package's fix turn pulled the work in); read by\n # the spec reviewer as the evidence a clean turn there is a real approval,\n # not a lazy/crashed one; removed by packages.item.closing.\n satisfiedFile: .gtd/SATISFIED.md\n reviewFile: .gtd/REVIEW.md\n reviewRawFile: .gtd/REVIEW_RAW.md\n feedbackFile: .gtd/FEEDBACK.md\n specFeedbackFile: .gtd/SPEC_FEEDBACK.md\n commitMsgFile: .gtd/COMMIT_MSG.md\n # The two opaque model tiers every agent state draws from — repoint them here\n # (or via a top-level `.gtdrc` `vars:` key / a `GTD_PLANNERMODEL` env var)\n # to change every state on that tier at once. `plannerModel` (default \"smart\")\n # is the heavier tier for the one-shot triage/design/review turns;\n # `coderModel` (default \"base\") is the tier for the build/fix turns.\n plannerModel: smart\n coderModel: base\n # The fixed diff base for `gtd --entry review-gate.check --var\n # reviewBase=<commitish>` — a plain non-gtd branch reviewed against that\n # commitish rather than the process start. Declared here (rather than left\n # undeclared) so `--var reviewBase=...` passes the CLI's declared-name\n # check; the default empty string renders blank, which the entry command\n # refuses (a review entry always requires an explicit base).\n reviewBase: \"\"\n # gtd's own voice for the files it generates — a specialisation of the\n # \"Spartan\" output style from alexgreensh/attention-span\n # (https://github.com/alexgreensh/attention-span), licensed AGPL-3.0,\n # written from a reading of that project's version 0.6. Rewritten in gtd's\n # own words for deliverables rather than chat replies; no upstream text\n # ships. A point-in-time derivation with no refresh mechanism — it will go\n # stale and nothing will notice.\n #\n # Keep/adapt/drop ledger — seventeen items reconciled: eight upstream rules\n # carried, five added, three dropped, rationale kept.\n #\n # Carried (six rules below cover eight upstream rules — two of them each\n # carry two upstream rules):\n # - the answer-first rule below carries both upstream's answer-first\n # rule and its cut-ruthlessly rule\n # - the never-trim rule below carries both upstream's\n # cut-elaboration-never-a-warning rule and its\n # numbers-and-thresholds-are-exact rule\n # - the deliverable rule carries upstream's answer-versus-deliverable\n # rule, minus its \"can't tell which? treat it as an answer\" tie-break —\n # gtd has no ambiguity to break, every injection site authors a file\n # - the blunt-and-imperative rule, the plain-words rule, and the bold\n # rule map one-to-one to upstream's equivalents\n #\n # Added and sharpened:\n # - five rules with no upstream equivalent: ship the artifact bare,\n # compressing is not dropping, one idea per block, flag risk in one\n # blunt line, never narrate the work\n # - two carried rules sharpened past upstream: the bold rule gains a\n # bold-only-reading test, and the plain-words rule gains a five-word\n # cap on defining an unavoidable term\n #\n # Dropped, each for its own reason:\n # - the arrow-point paragraph format: four of the seven voice-bearing\n # states are machine-parsed and their grammars require literal\n # `- [ ]` rows\n # - the go-deeper escape hatch: a state's own prompt is the request and\n # there is no channel to ask through\n # - the confirm-before-acting rule: every prompt is an instruction and\n # the actor's only output channel is the file\n styleBlock: |-\n This block opens with why density matters, not with rule one: the reader\n has a finite attention budget, and burying the line that mattered fails as\n hard as cutting it. This is motivation, not a rule — it commands nothing\n and never outranks never-trim.\n\n This file is a deliverable, not a chat reply. Its size follows the work —\n write what the task demands, and let padding, not length, be the target of\n any cut.\n\n Lead with the answer or the decision immediately. Skip the throat-clearing\n opener, and never circle back to restate the point once it has landed.\n\n Write in flat, commanding sentences — commit to the claim rather than\n hedging it.\n\n Favor everyday vocabulary over technical shorthand; where a term is\n unavoidable, define it in five words or fewer on first use.\n\n In markdown, bold carries the load: bold the claim, not the sentence around\n it. The test: reading only the bold text yields the full point and every\n risk, or the bolding is wrong.\n\n Above every rule in this block: never trim a risk, a number, a threshold,\n or a scoped condition to save space. A claim that only holds under some\n condition ships with that condition. It outranks every other rule here.\n\n Ship the artifact bare. Output only what was asked for — no lead-in, no\n \"here's the…\", no sign-off wrapped around it. A generated file consumed\n as-is turns a wrapper into a defect.\n\n Compressing is not dropping. A task with three load-bearing parts ships all\n three, each shorter — reporting two of three findings has failed the brief,\n not fulfilled it.\n\n One idea per block; break when the idea shifts. Blocks may run any length,\n but one unbroken paragraph is a defect even when short.\n\n Flag risk and uncertainty in one blunt line. This is never-trim's\n constructive twin: that rule forbids deleting a caveat, this one forbids\n dissolving it into hedged prose.\n\n Never narrate the work — do it. Skip lines like \"I will now list the\n concerns\"; a file has no reason to describe its own composition.\n styleFormatContract: |-\n This file is machine-read. Its format contract outranks every style rule\n above it: keep every `##`/`###` heading this state specifies, every\n `- [ ]` checkbox row, and every required marker line exactly as specified.\n\n Do not replace a checkbox list with prose paragraphs. Do not renumber or\n rename a required heading. Do not drop a marker line. A parser reads these\n literally, and a violation refuses the turn outright — this is not a\n polite request, it is the one thing standing between this output and a\n failed parse.\n\n The voice rules above still govern the prose that sits between these\n structural elements. They do not govern the elements themselves.\n\nentry:\n default: unified\n\nmachines:\n # ── DEDUP: the green-baseline entry gate (identity-free) ──────────────────\n # Runs the suite before any work starts — both `start-gate` and\n # `review-gate` reach this one check, and its script IS the shared\n # `&suiteCheck` anchor (defined once, right here, and aliased by\n # `fix-precheck` below); the `on` rules turn the result into \"halt at\n # blocked\" vs \"proceed to $onGreen\". Kept SEPARATE from `healthGate` (below)\n # even though both are a check+something-else shape: this gate runs BEFORE\n # planning, so it must NOT sweep any steering file `healthGate` does.\n entryGate:\n params: [onGreen, blockedMessage, blockedDescribe, checkLabel, blockedLabel, reviewBase]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: &suiteCheck |\n #!/usr/bin/env sh\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p \"<%~ it.stateDir %>\"\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain POSIX sh.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n mkdir -p \"$(dirname \"$feedback\")\"\n <%~ it.vars.testCommand %> > <%~ it.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.stateDir %>/.check-output ]; then\n mv <%~ it.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.stateDir %>/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n # A repeat identical failure would otherwise leave FEEDBACK.md\n # byte-identical to its committed copy — no diff, so the check would\n # look GREEN. Stamp each red run with the current HEAD (which always\n # advances between checks) so a still-red re-run always re-registers\n # as an M/A edit and takes the red edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$feedback\"\n else\n rm -f <%~ it.stateDir %>/.check-output\n rm -f \"$feedback\"\n fi\n # `entry: true` on the shared local marks both `entryGate` instances\n # (start-gate/review-gate) as manual entries — only `review-gate.check`\n # actually needs the reachability root (it is otherwise unreachable\n # from `entries.default`), but the shared dedup makes marking one mark\n # both. Accepted as correct: both ARE the workflow's entry gates.\n entry: true\n # `reviewBase: $reviewBase` — only `review-gate` binds a real\n # template (`<%= it.vars.reviewBase %>`), fixing the whole process's\n # diff base to whatever `gtd --entry review-gate.check --var\n # reviewBase=<commitish>` supplies. `start-gate` binds the literal\n # empty string, which compiles away to \"no base\" here — it is reached\n # from `idle`, never manually entered with a base.\n reviewBase: $reviewBase\n on:\n \"A <%= it.vars.feedbackFile %>\": blocked\n \"M <%= it.vars.feedbackFile %>\": blocked\n \"D <%= it.vars.feedbackFile %>\": $onGreen\n \"C\": $onGreen\n blocked:\n actor: human\n label: $blockedLabel\n file: <%= it.vars.feedbackFile %>\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $blockedDescribe\n\n # ── DEDUP: the check/escalate health pair (identity-free) ─────────────────\n # Runs the suite; a red run records the configured feedbackFile and routes to\n # the caller's OWN fixer (`$onRed` — a coder-identity action that lives\n # beside this machine, not inside it, since fixing IS a coder action and this\n # machine has no identity); a green run cleans it up and routes to\n # `$onGreen`. Escalation redirects here after the caller's fixer has retried\n # `max` times without success. Instantiated for the build tail's own tail\n # (`build.health.*`, caller's fixer is `build.fix`) and the per-package\n # build's own tail (`packages.item.health.*`, caller's fixer is\n # `packages.item.fix-suite`).\n healthGate:\n params: [onGreen, onRed, escalateDescribe, checkLabel, escalateLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # This script ALSO sweeps the spent raw review capture\n # (`reviewRawFile`) — a mechanic `entryGate.check` must NOT run, since\n # it executes before planning, while a fresh sketch legitimately still\n # exists.\n script: |\n #!/usr/bin/env sh\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p \"<%~ it.stateDir %>\"\n # Hoist every path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a\n # line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain POSIX sh.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n mkdir -p \"$(dirname \"$feedback\")\"\n # Sweep the raw review capture — no path from `deciding`/`collecting`\n # reaches this gate any more (its only inbound edge is\n # `gtd --entry fix-precheck`'s red branch); this is a leftover\n # sweeper for a capture an earlier, abandoned process may have left\n # committed on that path. A no-op when it is already gone or never\n # existed.\n reviewRaw=\"<%~ it.vars.reviewRawFile %>\"\n rm -f \"$reviewRaw\"\n <%~ it.vars.testCommand %> > <%~ it.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.stateDir %>/.check-output ]; then\n mv <%~ it.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.stateDir %>/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n # A repeat identical failure would otherwise leave FEEDBACK.md\n # byte-identical to its committed copy — no diff, so the check would\n # look GREEN. Stamp each red run with the current HEAD (which always\n # advances between checks) so a still-red re-run always re-registers\n # as an M/A edit and takes the red edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$feedback\"\n else\n rm -f <%~ it.stateDir %>/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A <%= it.vars.feedbackFile %>\": $onRed\n \"M <%= it.vars.feedbackFile %>\": $onRed\n \"* **\": $onGreen\n \"C\": $onGreen\n escalate:\n actor: human\n label: $escalateLabel\n file: <%= it.vars.feedbackFile %>\n message: |\n The agent could not get the check to pass after repeated attempts.\n `<%= it.vars.feedbackFile %>` holds the last failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $escalateDescribe\n\n # ── DEDUP: the open-questions check/answer pair (identity-free) ───────────\n # Shared by design and architecture's own gate (`design.gate`/\n # `architecture.gate`): `check`'s script probes whichever of\n # requirementsFile/architectureFile currently exists (see the script's own\n # comment) with `gtd check qa --open-questions` and stamps/removes\n # `questionsFile` on the result; only when it's present does the process\n # rest at `answer`, a human gate. A question-free phase never rests here at\n # all — `check`'s own `on` rows skip straight to `$onNone`. Takes NO `file:`\n # PARAM for the probe script itself: machine `$param` substitution is\n # whole-value only, so a path cannot be spliced into a shared script body —\n # `file` is instead the PARAM `answer` renders as its own `file:` (the\n # human-facing gate), a different use entirely from the probe.\n questionGate:\n params: [file, message, onNone, onRevise, checkLabel, answerLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: |\n #!/usr/bin/env sh\n # gtd check turn — probe the current phase's steering file for\n # unanswered questions; an answered/empty result removes the marker\n # (questionsFile), an unanswered one stamps it fresh. Design and\n # architecture share this one probe: whichever of\n # requirementsFile/architectureFile exists ON DISK right now IS this\n # phase's steering file — unambiguous because architecture.author\n # deletes requirementsFile in the same turn it writes\n # architectureFile, so exactly one of the two exists here.\n set +e\n mkdir -p \"<%~ it.stateDir %>\"\n # Hoist every path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a\n # line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain POSIX sh.\n questions=\"<%~ it.vars.questionsFile %>\"\n mkdir -p \"$(dirname \"$questions\")\"\n requirements=\"<%~ it.vars.requirementsFile %>\"\n architecture=\"<%~ it.vars.architectureFile %>\"\n file=\"$requirements\"\n [ -f \"$file\" ] || file=\"$architecture\"\n gtd check qa \"$file\" --open-questions > /dev/null 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n printf 'open questions remain in %s\\n' \"$file\" > \"$questions\"\n # A repeat identical result would otherwise leave the marker\n # byte-identical to its committed copy — no diff, so the check\n # would look answered. Stamp each round with the current HEAD\n # (which always advances between checks) so a still-open re-run\n # always re-registers as an M/A edit and takes the answer edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$questions\"\n else\n rm -f \"$questions\"\n fi\n on:\n \"A <%= it.vars.questionsFile %>\": answer\n \"M <%= it.vars.questionsFile %>\": answer\n \"D <%= it.vars.questionsFile %>\": $onNone\n \"C\": $onNone\n answer:\n actor: human\n label: $answerLabel\n file: $file\n mode: qa\n answerGate: true\n message: $message\n on:\n \"* **\":\n to: $onRevise\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace\n `_your answer_` for your own) to send it back for the agent to\n fold your answers in. Delete a question to skip it, or delete\n the whole `## Open Questions` section to accept the plan as-is.\n\n # ── ENCAPSULATION: the triage phase's one conversation ─────────────────────\n # Groups the diff that started the process into ordered, classified\n # concerns and resolves the PRODUCT ones' open questions — one machine\n # identity across the whole back-and-forth, so the agent explores the\n # codebase once and the human's answer rationale survives every return lap.\n # `triage`'s prompt reads THREE laps off the requirements file's own\n # content, never off which edge landed here (the resting state is the same\n # either way): FIRST (the file is absent), RETURN (a ticked/answered\n # question waiting to be folded), and REVIEW LOOP-BACK (concerns already\n # present, nothing ticked-but-unfolded — `re-unwind` put them there, not an\n # answer). The loop-back lap also runs the suite itself and files any\n # breakage it finds as its own FIRST concern: `re-unwind` sits between\n # `build.review.deciding` and here with no `entryGate` in between, so a red\n # the revert causes is otherwise ambient. Left for\n # `packages.item.health.check` to trip over instead, it would spend one of\n # `packages.item.fix-suite`'s retry cap — process-pooled across every\n # package, the same pattern `specReview.review`'s own retry comment\n # documents — on breakage the package itself never caused.\n # ▸ planner identity — `model:` declared once here, stamped onto `triage`\n # (its only prompt state; `gate.answer` is a human turn and carries no\n # prompt).\n designPlan:\n model: <%= it.vars.plannerModel %>\n params: [onArchitecture]\n entry: triage\n states:\n triage:\n actor: agent\n label: Triaging the change\n file: <%= it.vars.requirementsFile %>\n mode: qa\n # An agent that deletes the assembled review input without folding it\n # in is the original \"captured then discarded\" bug, now against THIS\n # state's own file — the retired `build.addressing` state was the\n # guard's only user before this restructure; this is its new one.\n requireProgress: true\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.requirementsFile %>`; develop it. Do\n not create other files to hold notes or output.\n\n `<%= it.vars.todoFile %>` is the likely home of the sketch that\n started this process. Despite living beside this workflow's own\n state files, it is the HUMAN'S INPUT — the sketch to be folded into\n the concerns below like any other part of the start diff. It is\n never a state file to preserve, and never gtd bookkeeping to ignore.\n\n This process started at commit `<%= it.startCommit %>`. The change\n that started it was reverted out of the working tree by the\n `unwind` beat right after it landed, so `git diff <%= it.startCommit %>`\n (against the working tree) is now empty — its content survives only\n in history. Find the entry commit yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\n (the process's very first turn, landed immediately after\n `<%= it.startCommit %>`, before any later baseline-repair commits) —\n then `git show` that commit to see everything that started this\n process: a hand-edit to real code, a scratch note in some file, or\n both.\n\n On the FIRST lap, `<%= it.vars.requirementsFile %>` does not exist\n yet (or holds whatever the human's own change happened to leave\n there). Build it from that start diff:\n\n 1. Group the diff into an ORDERED list of CONCERNS — each one a\n coherent piece of work that can leave the test suite GREEN on\n its own, in the order you'd want them built. If a concern would\n red-light tests that a later concern is meant to fix, that is\n not a valid split: merge the two into one concern.\n 2. Classify each concern as PRODUCT (a user-facing/requirements\n decision) or TECHNICAL (an implementation decision).\n 3. For every PRODUCT concern with an open point you cannot settle\n yourself, add a question under a `## Open Questions` heading near\n the top: one `### <question>` sub-heading each, followed by a\n checkbox list of candidate answers — aim for TWO concrete options\n plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Raise questions ONLY for product concerns here — a technical\n concern's open points wait for the next phase. Never tick a box\n yourself — the human ticks exactly one per question.\n 4. Fold EVERYTHING the entry commit added into the concerns above —\n a scratch note and a real code edit are both just a SKETCH of\n the intent, a spec to be finished, never work to be preserved;\n `unwind` already reverted both out of the tree, so there is\n nothing left to delete. Describe each piece under the concern it\n belongs to.\n\n On a RETURN lap the human has ticked one option per question (or\n replaced `_your answer_` with their own text) in\n `<%= it.vars.requirementsFile %>`. For each answered question: fold\n the chosen answer into its concern's prose, then MOVE the question\n down into a `## Answered Questions` section as `### <question>`\n followed by the resolved answer in plain PROSE (drop the\n checkboxes). Never re-raise a question the human deleted; if the\n human deleted the whole `## Open Questions` section, treat every\n product concern as accepted and surface no new questions unless\n something is genuinely still blocking. Omit `## Open Questions`\n entirely once there are none left.\n\n On a REVIEW LOOP-BACK lap, `<%= it.vars.requirementsFile %>`\n already holds concerns and none of them have a ticked-but-unfolded\n answer waiting — that shape means a completed REVIEW ROUND put\n this material here, not a question you asked. Develop those\n existing concerns further with whatever the round raised; never\n rediscover or regroup them from a cold read the way the FIRST lap\n does.\n\n The human's own edit from that review round has been reverted out\n of the working tree the same way the entry commit is — read it\n from history, never the working tree: run\n `git show <%= it.reviewBase %>` to see it. (On a FIRST lap that\n same hash IS the process's own diff base, `<%= it.startCommit %>`,\n so this branch does not apply: `<%= it.vars.requirementsFile %>`\n does not exist yet for a review round to have happened against.)\n\n Whatever this round touches, every decision already settled under\n `## Answered Questions` stays settled on this lap too — never\n re-open one. A genuinely open PRODUCT point in the round's own\n material may still raise a fresh question under\n `## Open Questions`: the product gate sits on this path exactly as\n it does on a first lap, and will ask it.\n\n Before grouping anything on this lap, run\n `<%= it.vars.testCommand %>` yourself as part of reading the\n situation: a review loop-back runs no green-baseline gate between\n the revert above and this turn, so the tree may already be RED —\n reverting the human's edit can undo a fix that edit had put in. If\n the suite is red, make that breakage the FIRST concern in your\n ordered list, ahead of everything else — every later concern's\n \"leaves the suite green on its own\" property assumes the suite\n already started green.\n\n Leave `<%= it.vars.requirementsFile %>` uncommitted and finish your\n turn.\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: <%= it.vars.requirementsFile %>\n onNone: $onArchitecture\n onRevise: triage\n checkLabel: Checking for open questions\n answerLabel: Awaiting your product answers\n message: |\n `<%= it.vars.requirementsFile %>` holds the concerns under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n # ── ENCAPSULATION: the technical phase's one conversation ──────────────────\n # A separate machine from `designPlan` — its own memory scope, since there\n # may be a handover between the user-facing and the technical phase: this\n # phase does NOT resume design's conversation, it reads\n # `<%= it.vars.requirementsFile %>` cold. Resolves the TECHNICAL open\n # questions, then mechanically decomposes the settled concerns into package\n # files (no merge/split judgement — that already happened in triage's\n # grouping). ▸ planner identity — `model:` declared once here, stamped onto\n # `author` and `decompose` (its own prompt states; `gate.answer` is a human\n # turn and carries no prompt).\n archPlan:\n model: <%= it.vars.plannerModel %>\n params: [onPackages]\n entry: author\n states:\n author:\n actor: agent\n label: Refining the technical plan\n file: <%= it.vars.architectureFile %>\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state files this\n turn touches are `<%= it.vars.architectureFile %>` (write it) and\n `<%= it.vars.requirementsFile %>` (delete it once its content is\n folded in). Do not create other files to hold notes or output.\n\n You do NOT resume the design conversation — this is a separate\n machine with its own memory, a COLD read every time. Read\n `<%= it.vars.requirementsFile %>` (the settled, ordered, classified\n concerns) in full. Treat every product decision recorded there —\n plain prose or a resolved `## Answered Questions` entry — as\n SETTLED; never re-open it.\n\n On the FIRST lap, develop `<%= it.vars.architectureFile %>` from\n those concerns: for each one, in the same order, work out the *how*\n — file/module structure, data models, library/tech-stack choices,\n error-handling strategy — building on the *what* already settled.\n For every open TECHNICAL point you cannot settle yourself, add a\n question under a `## Open Questions` heading exactly as the\n requirements phase does: one `### <question>` each, followed by a\n checkbox list — TWO concrete options plus a final free-text slot,\n ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself. On a RETURN lap the human has ticked one\n option per question (or replaced `_your answer_` with their own\n text): fold each chosen answer into the plan prose, then MOVE the\n question into a `## Answered Questions` section as `### <question>`\n + the resolved answer in plain PROSE (drop the checkboxes). Never\n re-raise a deleted question; treat a deleted `## Open Questions`\n section as acceptance. Omit `## Open Questions` once there are none\n left.\n\n Once `<%= it.vars.architectureFile %>` is written, delete\n `<%= it.vars.requirementsFile %>` — its content has been folded in\n and it must not linger. Leave everything uncommitted and finish\n your turn.\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: <%= it.vars.architectureFile %>\n onNone: decompose\n onRevise: author\n checkLabel: Checking for open questions\n answerLabel: Awaiting your technical answers\n message: |\n `<%= it.vars.architectureFile %>` holds the technical plan under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n decompose:\n actor: agent\n label: Decomposing into packages\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are an autonomous coding agent. This workflow steers itself through\n its own state files — treat them as its private scratchpad, never as\n project code or documentation. The only state files this turn touches\n are the package files it writes under `<%= it.vars.packagesDir %>/` and\n `<%= it.vars.architectureFile %>`, which it deletes. Do not create other\n files to hold notes or output.\n\n If you wrote `<%= it.vars.architectureFile %>` earlier in this\n conversation, work from what you already settled; otherwise read it\n (the converged technical plan) yourself. It already lists an ORDERED\n set of concerns with every merge/split judgement made — this turn is\n a mechanical write-out, not a planning one.\n\n Write one package file per concern, in the settled order, under\n `<%= it.vars.packagesDir %>/` (e.g.\n `<%= it.vars.packagesDir %>/01-name.md`,\n `<%= it.vars.packagesDir %>/02-name.md`, ...). Each package file\n carries that concern's requirement (so it can be reviewed against\n its spec), its independent tasks, and each task's acceptance\n criteria as `- [ ]` checkboxes and relevant paths. Do NOT merge or\n split concerns here — that judgement already happened upstream;\n carry the settled grouping over verbatim. No package file may\n reference any other `.gtd/` file.\n\n Once the package files are written, delete\n `<%= it.vars.architectureFile %>`. Leave everything uncommitted and\n finish your turn.\n on:\n \"* <%= it.vars.packagesDir %>/**\": $onPackages\n\n # ── ENCAPSULATION: the shared review tail ──────────────────────────────────\n # ▸ planner identity — `model:` declared once here, stamped onto `reviewing`\n # and `collecting` (its only prompt states). `collecting` JUDGES whether a\n # round is actionable and, when it is, CLASSIFIES it straight into\n # `requirementsFile` as ordered, PRODUCT/TECHNICAL concerns — the same\n # artifact `design.triage` develops — never an instruction list, and never a\n # build. An actionable round still leaves this machine entirely (the root's\n # own `re-unwind`, then `design.triage` folds the concerns in cold), so\n # there is no coder-identity follow-through inside this machine any more.\n humanReview:\n model: <%= it.vars.plannerModel %>\n entry: reviewing\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: <%= it.vars.reviewFile %>\n mode: review\n # `gtd --entry review-gate.check` (a plain non-gtd branch, e.g. a\n # colleague's PR) does NOT enter here directly — it enters\n # `review-gate.check` first, which runs the green-baseline gate and,\n # once green, transitions here. The `Gtd-Review-Base:` trailer rides on that\n # entry commit (the process's oldest), so everything below still reuses\n # the same reviewing/await-review/feedback machinery over <commitish>..HEAD.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.reviewFile %>`; write it at exactly\n that path. Do not create other files to hold notes or output.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the changes in\n this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.reviewBase %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation of\n what changed and why, then one file pointer per hunk (checkboxes are\n for the human to tick, not you; every path is `./`-relative, with an\n optional `#line`). The pointer's OWN line carries nothing but the\n pointer — no note, no dash. Its explanation goes on the line(s)\n beneath it instead:\n\n - [ ] ./path/to/file.ts#42\n what this hunk does, with room to run to several lines\n\n Text after the pointer token on the pointer's own line is a\n validation error, not just a style nit — an older-format doc like\n `- [ ] ./path/to/file.ts#42 — note` is UNLANDABLE.\n\n This prompt carries no diff — read the changes yourself. The range runs\n from `<%= it.reviewBase %>` to the working tree (committed turns plus\n anything still pending); on a feedback round that base is the previous\n review's boundary, so it covers only what changed since then, not items\n already signed off.\n\n Leave `<%= it.vars.reviewFile %>` uncommitted and finish your turn.\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n label: Awaiting your review\n file: <%= it.vars.reviewFile %>\n mode: review\n # While the process rests here, gtd opens a \"review checkout window\": raw HEAD\n # is rewound to the review base (the most-recent `deciding` commit — so a\n # feedback round shows only `last-review..HEAD` — or the process start on\n # the first review) with the working tree untouched, surfacing the diff\n # as ordinary uncommitted changes. It closes on the next invocation.\n #\n # Every human step here routes to `deciding`, which decides sign-off vs.\n # feedback from the step's CONTENT (a comment, not a tick). Ticking a box\n # only records \"I reviewed this hunk\" — it never gates a land, whatever the\n # boxes say. The one dead end that CAN'T be told apart by a file-pattern\n # edge is a deleted REVIEW.md, so the review-doc guard\n # (`src/StepGuards.ts`) catches it BEFORE the step commits and refuses.\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the process — one\n `- [ ]` checkbox per reviewable item, grouped into chunks. Tick a box\n (`- [x]`) as you review each hunk; ticking only records that you've read\n it, it is not sign-off.\n\n When you've been through the whole diff, run `gtd land`:\n\n - **Sign off** — leave no comment — no note in\n `<%= it.vars.reviewFile %>`, no code edit — to collapse the process into\n one commit (**build.squashing**), whatever the boxes say.\n - **Request changes** — leave a comment: a note on a\n `<%= it.vars.reviewFile %>` line, or a direct code edit — to send a FULL\n development lap (**review.deciding** → **review.collecting** →\n re-triage). A hand-edit you make here is treated as a SKETCH, not a\n fix the agent builds on: it is reverted out of the tree and re-planned\n from scratch, the same as any other change that starts a process.\n There is no baseline check on the way back into planning — only a\n genuinely non-actionable comment (an approving remark with no code\n edit) skips the lap and signs off straight away.\n\n Deleting `<%= it.vars.reviewFile %>` is refused.\n on:\n \"* **\": deciding\n\n deciding:\n actor: check\n label: Reviewing\n file: <%= it.vars.reviewFile %>\n mode: review\n # Anchors the INCREMENTAL review base: its most-recent in-process commit is\n # the point the previous review round ended, so the next `reviewing` turn\n # (and the review checkout window) covers only the changes since then. On\n # the first review there is no such commit, so the base is the process start.\n reviewBase: true\n script: |\n #!/usr/bin/env sh\n # gtd check turn — decide the human's review step from its CONTENT. HEAD is\n # the human's `await-review -> deciding` commit; HEAD^ is the `reviewing`\n # commit that wrote the agent's original REVIEW.md (`await-review`'s single\n # `\"* **\": deciding` edge means EVERY human step there lands here, so\n # `deciding` is entered exactly once per round — nothing else commits in\n # between). It is FEEDBACK when the human left\n # a comment: a note in REVIEW.md (any edit beyond a \"[ ]\"->\"[x]\" tick) OR a\n # hand-edit to any file this round outside the declared stateDir or the\n # state's own reviewFile (which is var-configurable and may live outside\n # stateDir, e.g. a root-level REVIEW.md — issue #128). Otherwise it is a\n # clean sign-off. This pathspec's plumbing-directory exclusion (stateDir,\n # never a literal `.gtd/`) is the same declaration `src/StepGuards.ts`'s\n # `isCodePath`/`hasCodeChange` re-derive for their own code-vs-plumbing\n # test — keep the two together when either changes. Both exclusions use\n # git's `:(exclude,literal)` pathspec magic, matching stateDir/reviewFile\n # as literal paths rather than regex/glob patterns — a bare grep here\n # would let a metacharacter in either value change what gets excluded.\n # This turn only CAPTURES the raw material into REVIEW_RAW.md (the\n # `collecting` agent then judges whether it's actionable); it does\n # NOT interpret it. Either way, remove REVIEW.md; what the resulting diff\n # MEANS is decided by this state's own `on` rules — the A/M REVIEW_RAW.md\n # rows are declared BEFORE the D REVIEW.md row so a feedback round (which\n # also deletes REVIEW.md) is captured as feedback, not mistaken for a\n # sign-off.\n set +e\n # Hoist the paths once, at the TOP: Eta's autoTrim eats the newline after\n # an interpolation tag, so no tag may be the last token on a line — it\n # would glue the next line onto it and break the script. Below this point\n # everything is plain POSIX sh.\n stateDir=\"<%~ it.stateDir %>\"\n reviewFile=\"<%~ it.vars.reviewFile %>\"\n reviewRawFile=\"<%~ it.vars.reviewRawFile %>\"\n head=$(git rev-parse HEAD)\n mkdir -p \"$(dirname \"$reviewRawFile\")\"\n # A note is any REVIEW.md change beyond a checkbox flip: normalize every\n # \"[ ]\"/\"[x]\" to a placeholder, then compare the agent's original (HEAD^)\n # against the human's version (HEAD).\n before=$(git show \"HEAD^:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n after=$(git show \"HEAD:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n # Did the human hand-edit anything this round besides stateDir and the\n # reviewFile itself (var-configurable — may live outside stateDir, so it\n # is excluded by exact path, not just the stateDir prefix)?\n if git diff-tree --no-commit-id --name-only -r HEAD -- . \\\n \":(exclude,literal)$stateDir\" \":(exclude,literal)$reviewFile\" \\\n | grep -q . \\\n || [ \"$before\" != \"$after\" ]; then\n {\n echo \"This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.\"\n echo\n echo \"Commit: $head\"\n echo \"The human's notes are in $reviewFile at this commit. Any hand edits are\"\n echo \"in that commit's other paths. Run: git show $head\"\n } > \"$reviewRawFile\"\n fi\n rm -f \"$reviewFile\"\n on:\n \"A <%= it.vars.reviewRawFile %>\": collecting\n \"M <%= it.vars.reviewRawFile %>\": collecting\n \"D <%= it.vars.reviewFile %>\": $onSignoff\n\n # Judges actionability and, when actionable, classifies straight into\n # `requirementsFile` — an ordered, PRODUCT/TECHNICAL concern list, never\n # an instruction list for a builder (the actionable path re-plans from\n # history via `re-unwind` + `design.triage`; it never builds on the\n # human's own edit). The outcome is encoded by which paths ITS OWN diff\n # touches, never by a sentinel: writing `requirementsFile` (`A`/`M`) IS\n # the actionable case — row ORDER matters, since an actionable round's\n # diff is `A requirementsFile` + `D reviewRawFile` together, so the\n # requirements rows must be declared first or every round would\n # short-circuit to sign-off. CONSUMING `reviewRawFile` alone (deleting\n # it, the only change this turn) is the non-actionable short-circuit —\n # the same trick `deciding` already uses, one state earlier; it reuses\n # `$onSignoff` (already bound to `build.squashing`) rather than a new\n # param, because the short-circuit IS a sign-off. Declaring no `C` row\n # and no `* **` row is deliberate: a clean turn here is a fruitless\n # dispatch (a crash, a refusal, a context-limit cutoff), never a\n # verdict, so it falls through to the ordinary `prompt`-state default\n # instead — an empty ATTEMPT commit, remembered as a stall\n # (`Edge.ts`'s `stalledAt`) rather than silently misread as \"the human's\n # feedback was non-actionable\" — and a dirty tree matching neither row\n # is a REFUSAL (you classify, you do not build). `requirementsFile`\n # survives `re-unwind` (it lives under `stateDir`, excluded from its own\n # revert pathspec) for `design.triage` to fold in cold; a consumed\n # `reviewRawFile` with no requirements written is gone before\n # `build.squashing` ever runs, so nothing leaks into the finale.\n collecting:\n actor: agent\n label: Collecting your feedback\n file: <%= it.vars.requirementsFile %>\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent judging and classifying a round\n of review feedback. This workflow steers itself through its own\n state files — treat them as its private scratchpad, never as\n project code or documentation. The only state files this turn\n touches are `<%= it.vars.requirementsFile %>` and\n `<%= it.vars.reviewRawFile %>`, which it deletes — do NOT change any\n other file: you classify, you do not build.\n\n The raw review material is: <%~ it.read(it.vars.reviewRawFile) %>\n\n It names a commit. If you wrote today's review earlier in this\n conversation, work from what you already reviewed; otherwise read that\n commit's diff yourself before classifying anything.\n\n The round is ACTIONABLE if any of the following is true:\n\n 1. The human left a NOTE on `<%= it.vars.reviewFile %>` — a note is\n anything beyond a checkbox flip, regardless of whether the box\n itself is ticked (ticking means only \"I read this hunk\", never\n sign-off). A note is a MANDATORY concern below, whether or not\n its box is ticked.\n 2. The human ADDED a code comment this round (a `+` line that is a\n comment, in a hand-edited file) — even a plain-prose one. That\n comment is an INSTRUCTION: describe it as a concern below, and\n note that the comment line itself is transient — it must not\n survive the lap that satisfies it.\n 3. The human hand-edited any non-comment code this round. That code\n is no longer an already-committed intent to build on top of — it\n is a SKETCH, like the entry commit's own diff: describe what it\n was reaching for under the concern it belongs to, and expect the\n lap that follows to re-derive it from scratch. Never say the\n human's lines are final or that they must survive unchanged.\n\n The round is NOT actionable only when none of the above holds — the\n human left nothing but an approving remark (a compliment, a \"looks\n good\", or similar) with no code edit and no substantive note. Never\n invent actionability to justify a lap, and never dismiss a real note\n or edit as approval.\n\n If the round is ACTIONABLE: write `<%= it.vars.requirementsFile %>`\n with an ORDERED list of CONCERNS, each classified PRODUCT (a\n user-facing/requirements decision) or TECHNICAL (an implementation\n decision) — the same shape `design.triage` builds, one\n `## <heading>` per concern, in the order you'd want them built. Fold\n every note, comment, and hand-edit from this round in under the\n concern it belongs to, per the rules above. Raise NO open\n questions here — write no `## Open Questions` section;\n `design.triage` owns that, one phase later. Then delete\n `<%= it.vars.reviewRawFile %>` and finish your turn.\n\n If the round is NOT actionable: delete\n `<%= it.vars.reviewRawFile %>` and finish your turn, writing nothing\n to `<%= it.vars.requirementsFile %>` — consuming the capture with no\n other change IS the sign-off.\n on:\n \"A <%= it.vars.requirementsFile %>\": $onFeedback\n \"M <%= it.vars.requirementsFile %>\": $onFeedback\n \"D <%= it.vars.reviewRawFile %>\": $onSignoff\n\n # ── ENCAPSULATION: the per-package agentic review ──────────────────────────\n # A single state: a machine whose entire job is to be a separate MIND from\n # the implementer, distinct from the coder identity that builds/fixes the\n # package (`packages.item.*`). ▸ planner identity — `model:` declared once\n # here, stamped onto `review` (its only prompt state). Fixing the reviewer's\n # own findings is a coder action, not a reviewer one — it lives in\n # `packages.item.fix-spec` (`$onFix`), not inside this machine.\n specReview:\n model: <%= it.vars.plannerModel %>\n params: [onApproved, onFix]\n entry: review\n states:\n review:\n actor: agent\n label: Reviewing the package\n # Bounded per process: once entered `max` times, a transition into it is\n # redirected to `$onApproved` (force-close the package and move on — the\n # unresolved concern is deferred to the shared human review at the tail,\n # which sees the whole diff anyway). NOTE the cap is PROCESS-scoped, so it\n # pools across packages — the same documented limitation the shared\n # `build.fix` cap has (see the README's \"Why\" section, \"Bounded, not\n # runaway\").\n retry:\n max: 3\n otherwise: $onApproved\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are an autonomous coding agent reviewing a freshly-built work package\n against its own spec. This workflow steers itself through its own\n state files — treat them as its private scratchpad, never as project\n code or documentation. The only state file this turn touches is\n `<%= it.vars.specFeedbackFile %>`; write it only when — and only\n when — you find problems. Do not create other files to hold notes or\n output.\n\n The package spec is: <%~ it.read(it.vars.nextFile) %>\n\n Read that spec, then verify the package's implementation against it: are\n all its tasks done, acceptance criteria met, and the code sound and\n consistent with the codebase? This prompt carries no diff — read it\n yourself: the range runs from `<%= it.startCommit %>` to the working\n tree.\n\n Note the range is process-wide (from the start of the whole build, not\n just this package), so it can span earlier packages too.\n\n - If it fully satisfies the spec, write NOTHING and finish your turn — a\n clean turn is your approval.\n - If it does not, write `<%= it.vars.specFeedbackFile %>` listing the\n concrete problems for a fix turn to address. Be specific.\n\n Do not fix anything yourself and do not delete the package file — a later\n step owns that. Finish your turn.\n on:\n \"A <%= it.vars.specFeedbackFile %>\": $onFix\n \"M <%= it.vars.specFeedbackFile %>\": $onFix\n \"D <%= it.vars.specFeedbackFile %>\": $onApproved\n \"C\": $onApproved\n\n # ── ENCAPSULATION: the per-package build queue ─────────────────────────────\n # Identity-free (a bookkeeping queue, not a builder) — the actual per-package\n # build identity lives in its own `item` child (packageItem, below).\n packageLoop:\n params: [onDrained]\n entry: picking\n states:\n picking:\n actor: check\n label: Picking the next package\n script: |\n #!/usr/bin/env sh\n # gtd check turn (the package queue arbiter) — the loop driver executes\n # this verbatim, then steps the check actor. Mechanics only: take the\n # first package file (by name) under the packages dir into NEXT.md, or\n # remove NEXT.md when the queue is empty. What NEXT.md's presence/absence\n # MEANS (another package to build vs. the queue closing out to review) is\n # decided by this state's own `on` rules at capture time — never here.\n set +e\n # Hoist the NEXT path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a line —\n # it would glue the next line's `else`/`fi` onto it and break the script.\n # Below this point everything is plain POSIX sh.\n nextFile=\"<%~ it.vars.nextFile %>\"\n # Sweep the design/architecture phases' spent steering files:\n # requirementsFile is consumed by architecture.author, architectureFile\n # by architecture.decompose, and questionsFile by whichever gate ran\n # last — all three are gone by the time the package queue starts.\n # reviewRawFile is swept here too: the review loop-back\n # (re-unwind) excludes stateDir from its revert, so an actionable\n # feedback round's MARKED raw capture (see humanReview's own\n # `collecting` state) rides through design and architecture\n # untouched — this is the ONLY sweeper on that path\n # before the queue (and eventually the squash finale) would\n # otherwise pick it up as stray content. No-op if already removed.\n requirements=\"<%~ it.vars.requirementsFile %>\"\n architecture=\"<%~ it.vars.architectureFile %>\"\n questions=\"<%~ it.vars.questionsFile %>\"\n reviewRaw=\"<%~ it.vars.reviewRawFile %>\"\n rm -f \"$requirements\" \"$architecture\" \"$questions\" \"$reviewRaw\"\n # Package file names are gtd-authored (architecture.decompose writes\n # NN-name.md), never containing whitespace/newlines that would break\n # ls | head.\n # shellcheck disable=SC2012\n next=$(ls <%~ it.vars.packagesDir %>/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n mkdir -p \"$(dirname \"$nextFile\")\"\n printf '%s' \"$next\" > \"$nextFile\"\n else\n rm -f \"$nextFile\"\n fi\n on:\n \"D <%= it.vars.nextFile %>\": $onDrained\n \"* <%= it.vars.nextFile %>\": item.building\n \"C\": $onDrained\n\n # The per-package build identity — its own coder machine (below), so\n # deleting a package file's own lifecycle stays inside `item` while the\n # queue itself (`packages`) stays a single state.\n item:\n machine: packageItem\n with:\n onNext: picking\n\n # ── ENCAPSULATION: the per-package build identity ──────────────────────────\n # ▸ coder identity — `model:` declared once here, stamped onto `building`,\n # `fix-suite`, and `fix-spec` (its own prompt states). `closing` ends that\n # package file's own lifecycle; keeping it here (not in the queue) keeps\n # `packages` itself to a single state. `packages.item.closing` -> `picking`\n # is the ONE upward-resolving target in the whole file, so it MUST be a\n # param (`$onNext`, bound by `packages` — see the machine's own reference).\n packageItem:\n model: <%= it.vars.coderModel %>\n params: [onNext]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn may write is `<%= it.vars.satisfiedFile %>` (see below); do NOT\n delete the package file (the spec-review gate reads it after you)\n and never touch `<%= it.vars.nextFile %>` — the `picking` state owns\n it.\n\n The package to implement is: <%~ it.read(it.vars.nextFile) %>\n\n Before implementing anything, check the package's acceptance criteria\n against the current tree — an earlier package's fix turn may already\n have satisfied them. If **every** criterion is already met, implement\n nothing: write `<%= it.vars.satisfiedFile %>` recording each criterion\n and the concrete evidence it is already satisfied (the commit, file,\n or symbol that proves it), change nothing else, and finish your turn.\n If any criterion is outstanding, implement the package normally and\n do not write that file.\n\n That file describes a set of INDEPENDENT tasks. Implement ALL of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement EXACTLY the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"A <%= it.vars.satisfiedFile %>\": &satisfied\n to: health.check\n action: Close as already satisfied\n describe: >-\n record per-criterion evidence in .gtd/SATISFIED.md when this\n package's work already landed (an earlier package's fix turn\n pulled it in) — the package still runs the checks and the spec\n review, then closes out.\n \"M <%= it.vars.satisfiedFile %>\": *satisfied\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.feedbackFile %>`; fix it per its\n contents, or delete it if the feedback turns out to be wrong. Do not\n create other files to hold notes or output.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue. If the\n only way to green the suite is to implement work that another\n package file describes, make the smallest change that gets there and\n do it — that later package will then legitimately report itself\n already satisfied when its turn comes.\n\n Leave everything uncommitted and finish your turn — do not commit.\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n fix-spec:\n actor: agent\n label: Fixing review feedback\n file: <%= it.vars.specFeedbackFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.specFeedbackFile %>`; address it, then\n delete it. Do not create other files to hold notes or output.\n\n Read `<%= it.vars.specFeedbackFile %>` (the reviewer's concerns about the\n current package) and the package spec (`<%= it.read(it.vars.nextFile) %>`),\n then fix the code to resolve every concern. Keep the change focused.\n\n Delete `<%= it.vars.specFeedbackFile %>` once every concern is addressed.\n Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": health.check\n\n closing:\n actor: check\n label: Closing out the package\n script: |\n #!/usr/bin/env sh\n # gtd check turn (the package closer) — remove the just-reviewed package\n # file (its path is in NEXT.md) plus any leftover spec feedback and\n # already-satisfied evidence, so `picking` selects the NEXT package (or\n # falls through to review when the queue is empty). Reached on approval\n # (spec review clean) and on the spec review retry cap (force-close,\n # deferring the concern to the tail).\n set +e\n pkg=$(cat <%~ it.vars.nextFile %> 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f <%~ it.vars.specFeedbackFile %> <%~ it.vars.nextFile %> <%~ it.vars.satisfiedFile %>\n on:\n \"* **\": $onNext\n\n # Per-package health pair: packages.item.health.check/.escalate ->\n # packages.item.spec.review (green) or packages.item.fix-suite (red).\n health:\n machine: healthGate\n with:\n onGreen: spec\n onRed: fix-suite\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**packages.item.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # Per-package agentic review: packages.item.spec.review -> closing\n # (approved), or packages.item.fix-spec (findings — re-verify via the\n # full health check before re-reviewing).\n spec:\n machine: specReview\n with:\n onApproved: closing\n onFix: fix-spec\n\n # ── ENCAPSULATION: the process's build identity ─────────────────────────────\n # ▸ coder identity — `model:` declared once here, stamped onto `squashing`\n # and `fix` (its own prompt states). There is no monolithic \"build the whole\n # plan\" state any more — every package is built inside `packages.item`, and\n # the queue drains straight into `build.review`. There is no coder-identity\n # follow-through on review feedback here any more — an actionable round\n # leaves this machine entirely, via `$onFeedback` bound at the root's own\n # `build:` reference to the root-local `re-unwind` state (see the header\n # comment's \"Why humanReview is nested\" section for why the review tail\n # still stays nested here even so). `humanReview` is nested HERE (not at the\n # root) so `build.squashing` — the one coder turn a sign-off still resumes —\n # stays inside the builder's own scope.\n buildTail:\n model: <%= it.vars.coderModel %>\n params: [onDone, onFeedback]\n # Nothing resolves this machine BARE (every reference into it names a\n # specific descendant, e.g. `build.review` / `build.fix`) — but a machine\n # must still declare an `entry:` local per its own grammar, so this names\n # an arbitrary one of its own states.\n entry: fix\n states:\n squashing:\n actor: agent\n label: Squashing\n file: <%= it.vars.commitMsgFile %>\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are an autonomous coding agent. The process is approved and done. This\n workflow steers itself through its own state files — treat them as its\n private scratchpad, never as project code or documentation. The only\n state file this turn touches is `<%= it.vars.commitMsgFile %>`; write\n it. Do not create other files to hold notes or output.\n\n Leave `<%= it.vars.commitMsgFile %>` uncommitted and finish your turn —\n entering this file squashes everything this process retains into one\n commit using its content as the message. This prompt carries no diff —\n if you built that range yourself earlier in this conversation, work from\n what you already know; otherwise read the range yourself. It runs from\n `<%= it.retainedBase %>` to the working tree, and is exactly what that\n commit will contain, the changes THIS process made (its trace/retry\n boundary onward). For a normal build\n process that is the whole feature; for a `gtd --entry review-gate.check`\n process it is only the fixes made during the review — NOT the changeset\n that was under review.\n\n If that range is non-empty, write `<%= it.vars.commitMsgFile %>` with ONE\n conventional-commits message for the changes (subject line\n `type(scope): subject`, imperative mood, <= 72 characters; a body\n explaining the why, trade-offs, and key decisions). Plain text, no\n markdown wrapper.\n\n If that range is empty (a clean review sign-off with no fixes), write\n `<%= it.vars.commitMsgFile %>` with exactly this one line and nothing\n else:\n\n chore: human review\n on:\n \"A <%= it.vars.commitMsgFile %>\": $onDone\n \"M <%= it.vars.commitMsgFile %>\": $onDone\n\n fix:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.feedbackFile %>`; fix it per its\n contents, or delete it if the feedback turns out to be wrong. Do not\n create other files to hold notes or output.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue.\n\n Leave everything uncommitted and finish your turn — do not commit.\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n # Shared health pair: build.health.check/.escalate -> build.review.reviewing\n # (green) or build.fix (red).\n health:\n machine: healthGate\n with:\n onGreen: review\n onRed: fix\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**build.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # The shared review tail, nested HERE (not at the root) so `squashing`\n # stays inside the builder's own scope on sign-off — see the header\n # comment. Sign-off -> squashing; feedback -> $onFeedback, bound at the\n # root's own `build:` reference to `re-unwind` (a root-local, not a\n # descendant of this machine — the review tail's own scope ends here on\n # the actionable path).\n review:\n machine: humanReview\n with:\n onSignoff: squashing\n onFeedback: $onFeedback\n\n # The root machine — the whole one-flow tree described in this file's\n # header comment. WIRING ONLY: no identity of its own, just the shared\n # entry/exit states and the top-level machine tree.\n unified:\n entry: idle\n states:\n # ── Shared entry ────────────────────────────────────────────────────────\n idle:\n actor: human\n label: Idle\n file: <%= it.vars.todoFile %>\n # Every human-gate `message:` below closes with the same rendered \"What\n # each change does next\" list, built from `it.edges` (the resting state's\n # own `on` edges — each edge's `describe:` is the single source of truth).\n message: |\n No active gtd process.\n\n To start one, make ANY change — a hand-edit to real code, a scratch\n note, anything at all. <%= it.vars.todoFile %> is a good default\n place to start sketching. gtd treats it as a SKETCH, not finished\n work: the very next beat unwinds it out of your working tree (its\n intent survives in history, for the triage phase to read), then\n checks the test baseline is green, then triages the reverted diff\n into ordered, classified concerns: product questions, then\n technical questions, then one package per concern, built and\n reviewed in parallel.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: unwind\n action: Start\n describe: >-\n make any change — gtd unwinds it out of your working tree (its\n intent survives in history) before checking the test baseline is\n green, then triages the reverted diff into ordered, classified\n concerns and resolves product questions, then technical ones,\n before building each concern's package (**unwind** ->\n **start-gate.check**).\n\n # ── Green-baseline gate: `gtd --entry fix-precheck` entry ─────────────────\n # `gtd --entry fix-precheck` enters HERE: run the suite, then a red run\n # drops straight into the shared `build.fix` loop (-> build.health.check ->\n # review + squash tail); a green run is a no-op back to `idle` (nothing\n # to fix). Aliases the `&suiteCheck` anchor defined on `entryGate.check`'s\n # own `script:` above — the same plain suite check `start-gate`/\n # `review-gate` run.\n fix-precheck:\n actor: check\n label: Checking the baseline\n entry: true\n script: *suiteCheck\n on:\n \"A <%= it.vars.feedbackFile %>\": build.fix\n \"M <%= it.vars.feedbackFile %>\": build.fix\n \"D <%= it.vars.feedbackFile %>\": idle\n \"C\": idle\n\n # ── Shared tail: the finale ─────────────────────────────────────────────\n done:\n commit: |\n <%~ it.read(it.vars.commitMsgFile) %>\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n\n # ── Unwind: revert the entry commit's diff before planning starts ──────\n # Kept a SEPARATE state rather than folded into `start-gate.check`'s own\n # script: the `blocked -> check` retry edge re-runs the gate, so a\n # revert living there would discard the human's own baseline repair and\n # would need an idempotence guard. With exactly one inbound edge (from\n # `idle`), `unwind` is structurally reached once per process and needs\n # no guard at all. Names the reverted commit via `it.currentCommit`\n # (they're the same commit here), NOT a bare `HEAD` — pinning the\n # render-time hash keeps the script correct even if a driver runs it\n # late.\n unwind:\n actor: check\n label: Unwinding your input\n script: |\n #!/usr/bin/env sh\n # gtd check turn — revert the entry commit's diff out of the working\n # tree. Its content survives in history for design.triage to read.\n set +e\n # Hoist the commit hash once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last\n # token on a line.\n commit=\"<%~ it.currentCommit %>\"\n git revert --no-commit \"$commit\"\n on:\n \"* **\": start-gate\n\n # ── Re-unwind: revert an actionable review round's hand-edit before ────\n # re-planning starts. A SECOND unwind-shaped state, not a second inbound\n # edge on `unwind` above: `unwind`'s own doc comment turns on it having\n # exactly one inbound edge (from `idle`), which is why it carries no\n # idempotence guard — adding a second caller there would break that\n # invariant. Names the reverted commit via `it.reviewBase`, never\n # `it.currentCommit` (by now that names the assembly turn's own commit)\n # and never a bare `HEAD`: `reviewBaseFor` walks the process trace\n # backwards for the last commit that ENTERED a `reviewBase: true` state\n # — `build.review.deciding` is the template's only one — which is\n # exactly the human's own `await-review -> deciding` commit, the same\n # hash the next review round's incremental base already uses.\n #\n # SCOPED, not wholesale: `deciding` already `rm -f`'d the review file,\n # so a plain `git revert --no-commit` would resurrect it (and conflict\n # on it). Instead this renders that ONE commit's diff with `stateDir`\n # and `reviewFile` excluded from the pathspec (the review file by exact\n # path, since it is var-configurable and may sit at the repo root —\n # issue #128) and reverse-applies it. The apply is EXPECTED to succeed —\n # nothing between that commit and now touches a code path — but it can\n # still FAIL: a clean-filter round-trip, a drifted worktree, or a binary\n # path can all make `git apply -R` a no-op on an otherwise-clean tree,\n # indistinguishable from a legitimate note-only round. `requireRevert`\n # (below) is what catches that: its step-capture guard\n # (`src/StepGuards.ts`) re-establishes the fact from the tree itself —\n # comparing the human's own commit's paths against the review base's\n # parent — and refuses the step while residue remains, rather than\n # trusting the script's swallowed exit code. A note-only round's patch\n # is EMPTY, and `git apply` on empty input FAILS (\"No valid patches in\n # input\"), hence the `-s` guard rather than `--allow-empty` (which would\n # impose git >= 2.35). The apply is worktree-only, no `--index`: the\n # emitted step script's own `git add -A && git commit` stages it, and\n # `changedPaths` answers by CONTENT, so an unstaged revert is still the\n # pending diff the pattern matches — one fewer index write in a\n # worktree gtd shares with the editor's SCM. The patch file lives under\n # `stateDir` (excluded from its own pathspec, the same precedent as the\n # check scripts' own `.check-output`) and is removed before the step\n # lands.\n #\n # Both `on` rows target the SAME place (`design`) because either shape\n # is a legitimate outcome here, never a stall: a hand-edited round's\n # patch is non-empty, so the revert leaves a dirty tree (the `\"* **\"`\n # row); a note-only round's patch is empty, so nothing to apply leaves\n # a CLEAN tree — and a clean tree at a `script` state with no `C` row\n # would otherwise be a silent no-op that never advances. Declaring `C`\n # here makes that the expected, terminating case instead.\n #\n # `file: <%= it.vars.reviewFile %>` + `requireRevert: true` exist ONLY\n # to give the require-revert guard the path it must exempt from residue\n # (the engine blesses no variable names, so the guard reads `Rest.hints.\n # file`, never `it.vars.reviewFile` itself) — the review file is already\n # gone two commits earlier, so this state never renders it. The accepted\n # presentational cost: `gtd next --json`/`gtd status --json` carry that\n # already-deleted path as this state's `file:` hint, and the visualizer\n # labels `re-unwind` with it too. Bounded — `Lsp.ts`'s `buildSteeringMap`\n # skips a mode-less state, so `re-unwind` contributes no path→mode entry\n # and raises no \"already mapped by an earlier state\" warning against\n # `await-review`/`deciding`, which declare the same rendered path with\n # `mode: review`.\n re-unwind:\n actor: check\n label: Re-unwinding your review edit\n file: <%= it.vars.reviewFile %>\n requireRevert: true\n script: |\n #!/usr/bin/env sh\n # gtd check turn — revert the human's own review edit out of the\n # working tree, scoped to real code: stateDir and reviewFile\n # (var-configurable, may live at the repo root — issue #128) are\n # excluded, because the review file is already gone by this point\n # and reverting it would resurrect it. The human's intent survives\n # in their own commit for the triage phase to read. The apply is\n # EXPECTED to succeed — nothing between that commit and now touches\n # a code path — but it can still fail on a clean-filter round-trip,\n # a drifted worktree, or a binary path: the `requireRevert`\n # step-capture guard (src/StepGuards.ts) catches that by\n # re-checking the tree itself before this step is allowed to land.\n # The guard's own residue scoping (`isCodePath`) re-derives the same\n # stateDir/reviewFile exemption this pathspec renders, from the same\n # declaration (never a literal `.gtd/`) — keep the two together\n # when either changes. Both exclusions carry `:(exclude,literal)`,\n # the same magic `deciding` uses: git's DEFAULT pathspec magic is\n # glob-ish (`*` even crosses `/`), and nothing validates a\n # metacharacter out of either value — `stateDirError` rejects only\n # blanks/root/absolute/`..`/non-canonical segments, and `reviewFile`\n # is an ordinary var. A bare `:(exclude)` therefore lets a `*`/`?`/\n # `[` in either value drop a REAL edited path out of the patch,\n # which the guard (a plain string comparison, not a pathspec) then\n # scores as residue and refuses forever.\n set +e\n commit=\"<%~ it.reviewBase %>\"\n stateDir=\"<%~ it.stateDir %>\"\n reviewFile=\"<%~ it.vars.reviewFile %>\"\n patch=\"$stateDir/.re-unwind.patch\"\n mkdir -p \"$stateDir\"\n git diff --binary \"$commit^\" \"$commit\" -- . \\\n \":(exclude,literal)$stateDir\" \":(exclude,literal)$reviewFile\" > \"$patch\"\n if [ -s \"$patch\" ]; then\n git apply -R \"$patch\" || echo \"re-unwind: could not revert $commit\" >&2\n fi\n rm -f \"$patch\"\n on:\n \"* **\": design # a code hand-edit was reverted\n \"C\": design # note-only round: nothing to revert\n\n # ── Green-baseline gate: the single start entry. ───────────────────────\n # Runs on the tree AFTER `unwind`, so this is now just the ordinary\n # green-baseline check `review-gate`/`fix-precheck` also run — no\n # baseline-aware second run, no detached checkout, no HEAD move\n # anywhere in gtd's emitted scripts any more.\n start-gate:\n machine: entryGate\n with:\n onGreen: design\n reviewBase: \"\"\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**start-gate.check**). To repair the baseline as its own separate\n reviewed commit instead, abandon this start and run\n `gtd --entry fix-precheck` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── Green-baseline gate: `gtd --entry review-gate.check` entry -> build.review.reviewing. ─\n review-gate:\n machine: entryGate\n with:\n onGreen: build.review\n reviewBase: <%= it.vars.reviewBase %>\n blockedMessage: |\n The test baseline is red — gtd will not start a review on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**review-gate.check**).\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── The triage phase (design.triage -> design.gate.check/.answer -> architecture). ──\n design:\n machine: designPlan\n with:\n onArchitecture: architecture\n\n # ── The technical phase (architecture.author -> architecture.gate.check/.answer ─\n # -> architecture.decompose -> packages). ──────────────────────────────\n architecture:\n machine: archPlan\n with:\n onPackages: packages\n\n # ── The per-package build queue (packages.* -> build.review). ─────────────\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n # ── The process's build identity (build.* -> build.review -> done). ───────\n build:\n machine: buildTail\n with:\n onDone: done\n onFeedback: re-unwind\n";
|
|
192521
|
+
var unified_default = "# The unified gtd workflow template — gtd's BUILT-IN DEFAULT workflow. When no\n# `workflow:` key is configured anywhere in the cwd→home config chain,\n# src/Config.ts runs this template directly, so a state command works with no\n# configuration at all. `gtd init` seeds only the settings a project tunes\n# (vars.testCommand + a modes: formatting suggestion), never the workflow\n# itself. See the README's \"How it works\" and \"Driving the loop\" sections for\n# the full walkthrough.\n#\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/templates.ts does the compiling). Every content string below\n# is INLINE (no `./`-relative file references): the bundled template ships\n# inside a single-file build (`dist/gtd.bundle.mjs`), so its content must\n# already be resolved at BUILD time. `renderInitConfig` (src/workflows/\n# templates.ts) can MATERIALIZE this raw source into a `.gtdrc.json`\n# `workflow:` value — the way to eject/customize the machine, and the hermetic\n# test fixture — but `gtd init` itself no longer writes the workflow.\n#\n# ── Authored with MACHINES (entry:/machines:) ───────────────────────────────\n#\n# This template is written as a tree of reusable, parameterized MACHINES (see\n# src/Machines.ts) — a gate/loop written once and instantiated several times\n# with different `with:` bindings (dedup), or a complex cluster grouped under\n# one name for source comprehension (encapsulation). Every reference below is\n# an ordinary `{ machine: <name>, with: { ... } }` local, expanded at\n# compile-time into concrete, qualified states before the pure engine ever\n# sees the definition — no runtime footprint.\n#\n# MACHINE BOUNDARIES LINE UP WITH CONVERSATIONAL IDENTITY: a machine that holds\n# an identity (a planner or a coder persona) declares its own `model:` ONCE, at\n# the machine level (stamped onto every one of its own `prompt` states — see\n# src/Machines.ts's `resolveInstanceMachineFields`), instead of repeating `model:` on\n# every state; no state anywhere declares `model:` or `memory:` directly any\n# more (the memory key is computed from the machine tree — see\n# src/PatternMachine.ts's `memoryScopeAt` / src/Edge.ts's `memoryKeyFor`).\n#\n# * entryGate (dedup ×2) — the green-baseline gate (check + human blocked)\n# guarding the single start (`start-gate`) and\n# the review (`review-gate`) entries. Its check\n# script IS the shared `&suiteCheck` anchor\n# (also aliased by `fix-precheck`, outside this\n# machine) — every entry blocks on any red tree,\n# full stop; there is no baseline-aware second\n# run any more, since `unwind` (below) already\n# put the tree back to the baseline before this\n# gate ever runs. Identity-free.\n# * healthGate (dedup ×2) — the check/escalate health pair shared by the\n# build tail's own tail (`build.health.*`) and\n# the per-package build's own tail\n# (`packages.item.health.*`) — each caller's OWN\n# `fix` state (a coder-identity action) lives\n# beside this machine, not inside it, since\n# fixing IS a coder action and this machine has\n# no identity of its own. Identity-free.\n# * questionGate (dedup ×2) — the check/answer pair shared by the design and\n# architecture phases' own open-questions gate\n# (`design.gate.*` / `architecture.gate.*`): a\n# shared probe script decides whether the\n# phase's steering file still has unanswered\n# questions, and only then rests the process at\n# a human answer gate. Identity-free.\n# * designPlan (encap) — the triage phase's one conversation: group the\n# diff that started the process into ordered,\n# classified concerns and resolve the PRODUCT\n# ones' open questions (`design.*`: triage,\n# gate.check, gate.answer). ▸ planner.\n# * archPlan (encap) — the technical phase's one conversation: a COLD\n# read of the settled concerns, resolving the\n# TECHNICAL open questions, then a mechanical\n# decomposition into package files\n# (`architecture.*`: author, gate.check,\n# gate.answer, decompose). A separate machine\n# from `designPlan` (not one fused machine) —\n# its own memory scope, since there may be a\n# handover between the user-facing and the\n# technical phase. ▸ planner.\n# * humanReview (encap) — the shared review tail, nested INSIDE\n# `buildTail` (`build.review.*`: reviewing →\n# await-review → deciding → collecting) — see\n# \"Why humanReview is nested, not a root sibling\"\n# below. ▸ planner — `collecting` only judges\n# whether the round is actionable; there is no\n# coder-identity follow-through inside this\n# machine any more (see the root's own\n# `re-unwind` state).\n# * specReview (encap) — the per-package agentic review, a SINGLE\n# state (`packages.item.spec.review`) — a\n# machine whose entire job is to be a separate\n# mind from the implementer. ▸ planner — fixing\n# the reviewer's own findings is a coder action,\n# so it lives in `packages.item.fix-spec`\n# instead.\n# * packageLoop (encap) — the per-package build queue\n# (`packages.picking`). Identity-free — the\n# actual per-package build identity lives in its\n# own `item` child (packageItem, below).\n# * packageItem (encap) — the per-package build identity (`packages.\n# item.*`: building, fix-suite, fix-spec,\n# closing), plus its own `health` and `spec`\n# children. ▸ coder.\n# * buildTail (encap) — the process's build identity (`build.*`:\n# squashing, fix), plus its own `health` child\n# AND its own `review` child\n# (humanReview, above). No monolithic \"build the\n# whole plan\" state any more — every package is\n# built inside `packages.item`, and the queue\n# drains straight into `build.review`. ▸ coder.\n#\n# ── Why humanReview is nested, not a root sibling ───────────────────────────\n#\n# `humanReview` sits UNDER `buildTail` (`build.review`) rather than beside it\n# at the root, because a machine's memory scope is its dotted instance path\n# (see src/PatternMachine.ts's `memoryScopeAt`): entering a DESCENDANT scope\n# never breaks the parent's unbroken run, but entering a sibling does. On the\n# ORDINARY path (packages -> build.review -> squashing) that buys nothing —\n# `build`'s own scope has no prior turn to resume, since every package is\n# built inside `packages.item` instead. The one concrete case where it\n# matters is `gtd --entry fix-precheck`: a red suite drops into `build.fix`,\n# which opens `build`'s own scope, and the whole run from there —\n# `build.fix` -> `build.health.check` -> `build.review.*` -> `build.squashing`\n# — stays inside that one subtree, so `build.squashing` drafts the finale\n# from the SAME session that made the fixes, instead of a root-level sibling\n# forcing it to re-derive the range from scratch on every pass through the\n# tail. That one benefit is worth keeping over hoisting `humanReview` back to\n# a root sibling, which would otherwise be a repo-wide rename with no\n# behavioural gain. (The OTHER coder turn a feedback round used to resume —\n# the implementer's own follow-through on review feedback — is retired\n# outright: a hand-edit made during review is now re-planned from scratch via\n# the root's own `re-unwind` state, never built upon, so there is no\n# continuity to preserve on that path any more.)\n# The reviewer itself is unaffected either way: `build.review.*` is its own\n# scope regardless of where it's nested, so it never resumes the builder's\n# session.\n#\n# ── One flow: triage into concerns, then packages, then the shared tail ────\n#\n# ANY change to the tree starts the process — a hand-edit to real code, a\n# scratch note, anything at all — because it's a SKETCH, not finished work:\n# there is no fork on which steering file the human happened to create;\n# `idle` has exactly one outgoing edge, into `unwind`, which reverts that\n# whole diff back out of the working tree before planning starts (its intent\n# survives in history for `design.triage` to read; the squash finale means it\n# never reaches the final commit either way). `unwind` itself has exactly one\n# outgoing edge, into `start-gate`.\n#\n# EVERY entry gates on a green baseline before proceeding, and all three now\n# keep the exact same plain rule: block on any red tree, full stop. A red run\n# at any of them halts at a human blocked gate (fix the failures and re-run,\n# exactly like the escalate states). `start-gate` needs no baseline-aware\n# second run any more — by the time it runs, `unwind` has already put the\n# tree back to the baseline, so ONE suite run there IS the baseline verdict.\n#\n# Once green, `design.triage` reads the diff that started the process (never\n# rendered into the prompt — it names the start commit and reads the range up\n# to the unwind commit itself) and groups it into an ORDERED list of\n# CONCERNS, each one leaving the suite green on its own, classifying each as\n# PRODUCT or TECHNICAL. It raises open questions for the product ones only,\n# and folds EVERYTHING the start diff added into the concerns — a scratch\n# sketch and a real hand-edit alike, since `unwind` already emptied the tree\n# of both; nothing is left to delete. `design.gate` is a shared check+answer\n# pair: its check probes `REQUIREMENTS.md` for unanswered questions and only\n# rests the process at a human answer gate when some remain — a\n# question-free phase skips the human stop entirely.\n#\n# Once the product questions are settled, `architecture.author` picks up as a\n# COLD reader (a separate machine, a separate memory scope — it reads\n# `REQUIREMENTS.md` in full rather than resuming design's conversation),\n# develops the *how* for each settled concern, and raises only TECHNICAL open\n# questions — routed through the same shared `questionGate` shape\n# (`architecture.gate`) over `ARCHITECTURE.md`. Once those are settled,\n# `architecture.decompose` is a purely mechanical write-out: one package file\n# per concern, in the settled order — no merge/split judgement, since that\n# already happened in triage's grouping.\n#\n# From there the per-package build queue (`packages.*`) and the shared review\n# + squash tail (`build.*`) are UNCHANGED from before this restructure — see\n# their own machine doc comments below for the per-package build/review/\n# health mechanics.\n#\n# A second entry, `gtd --entry review-gate.check`, jumps there directly and,\n# once green, to `build.review.reviewing` to review a foreign branch through\n# the same shared tail. A third, `gtd --entry fix-precheck`, jumps there: a\n# red suite drops straight into the shared `build.fix` loop and out through the\n# review + squash tail; a green suite is a no-op back to `idle`. Both states\n# declare `entry: true` — otherwise unreachable from `entries.default`.\n#\n# All entries converge at `build.review.reviewing` -> `build.review.await-review`.\n# Ticking a REVIEW.md box means only \"I reviewed this hunk\"; a COMMENT (not an\n# unticked box) is what asks for changes. Every human step at\n# `build.review.await-review` goes to `build.review.deciding`, which CAPTURES\n# the raw material (never interprets it) and routes on the step's shape —\n# THREE edges out of the review tail, not one:\n# * the SIGN-OFF edge: no comment at all (no REVIEW.md note, no code edit)\n# -> the diff is a bare REVIEW.md deletion ->\n# build.squashing -> done (the whole process collapses into one commit).\n# * the NON-ACTIONABLE edge: some comment exists, but `build.review.collecting`\n# judges it non-actionable (the human left only an approving remark, e.g. a\n# compliment with no code edit and no substantive note) -> CONSUMES the raw\n# capture (deletes it, the only change this turn) -> straight to\n# build.squashing too, WITHOUT spending a design lap producing nothing, and\n# with nothing left over to leak into the finale. The outcome is encoded by\n# which paths `collecting`'s own diff touches, never by a sentinel a file\n# pattern can't tell apart from real content — and never by a CLEAN turn\n# either, which stays the ordinary fruitless-dispatch attempt (no `C` row\n# here), not a verdict.\n# * the ACTIONABLE edge: `collecting` judges the round actionable (a note in\n# REVIEW.md, or a hand-edit to code) -> marks the raw capture (a real\n# edit) -> the root's own `re-unwind` state reverts the human's\n# review-round commit out of the working tree, scoped to real code (never\n# the review file, already gone, and never `.gtd/`) -> `design.triage`\n# re-derives the work from that history as intent only — the human's\n# hand-edit is a SKETCH to be re-planned, never built upon. This is a full\n# development lap (triage -> gate -> architecture -> gate -> packages ->\n# build.review again), not a quick fix-and-re-review; there is no\n# baseline check on the way back in (`re-unwind` is not an entry, so\n# `entryGate` never runs on this path).\n# One dead end never commits: the review-doc guard (src/StepGuards.ts) REFUSES\n# a deleted REVIEW.md — restore it, or leave a note (or edit code) to request\n# changes. A tick only records that a hunk was read; it never gates a land.\n#\n# The shared health pair (`build.health.check`/`.escalate`, plus its own\n# `build.fix`) no longer serves a feedback loop at all — with the implementer's\n# own coder-identity follow-through retired, the only edge into it is\n# `gtd --entry fix-precheck`'s red branch; they are not dead code, just\n# reachable from one place instead of two. The\n# per-package loop keeps its OWN health pair (`packages.item.health.check`/\n# `.escalate`, plus `packages.item.fix-suite`) because its green edge targets\n# the per-package `packages.item.spec.review` gate instead.\n#\nvars:\n testCommand: npm test\n # The two opaque model tiers every agent state draws from — repoint them here\n # (or via a top-level `.gtdrc` `vars:` key / a `GTD_PLANNERMODEL` env var)\n # to change every state on that tier at once. `plannerModel` (default \"smart\")\n # is the heavier tier for the one-shot triage/design/review turns;\n # `coderModel` (default \"base\") is the tier for the build/fix turns.\n plannerModel: smart\n coderModel: base\n # The fixed diff base for `gtd --entry review-gate.check --var\n # reviewBase=<commitish>` — a plain non-gtd branch reviewed against that\n # commitish rather than the process start. Declared here (rather than left\n # undeclared) so `--var reviewBase=...` passes the CLI's declared-name\n # check; the default empty string renders blank, which the entry command\n # refuses (a review entry always requires an explicit base).\n reviewBase: \"\"\n # gtd's own voice for the files it generates — a specialisation of the\n # \"Spartan\" output style from alexgreensh/attention-span\n # (https://github.com/alexgreensh/attention-span), licensed AGPL-3.0,\n # written from a reading of that project's version 0.6. Rewritten in gtd's\n # own words for deliverables rather than chat replies; no upstream text\n # ships. A point-in-time derivation with no refresh mechanism — it will go\n # stale and nothing will notice.\n #\n # Keep/adapt/drop ledger — seventeen items reconciled: eight upstream rules\n # carried, five added, three dropped, rationale kept.\n #\n # Carried (six rules below cover eight upstream rules — two of them each\n # carry two upstream rules):\n # - the answer-first rule below carries both upstream's answer-first\n # rule and its cut-ruthlessly rule\n # - the never-trim rule below carries both upstream's\n # cut-elaboration-never-a-warning rule and its\n # numbers-and-thresholds-are-exact rule\n # - the deliverable rule carries upstream's answer-versus-deliverable\n # rule, minus its \"can't tell which? treat it as an answer\" tie-break —\n # gtd has no ambiguity to break, every injection site authors a file\n # - the blunt-and-imperative rule, the plain-words rule, and the bold\n # rule map one-to-one to upstream's equivalents\n #\n # Added and sharpened:\n # - five rules with no upstream equivalent: ship the artifact bare,\n # compressing is not dropping, one idea per block, flag risk in one\n # blunt line, never narrate the work\n # - two carried rules sharpened past upstream: the bold rule gains a\n # bold-only-reading test, and the plain-words rule gains a five-word\n # cap on defining an unavoidable term\n #\n # Dropped, each for its own reason:\n # - the arrow-point paragraph format: four of the seven voice-bearing\n # states are machine-parsed and their grammars require literal\n # `- [ ]` rows\n # - the go-deeper escape hatch: a state's own prompt is the request and\n # there is no channel to ask through\n # - the confirm-before-acting rule: every prompt is an instruction and\n # the actor's only output channel is the file\n styleBlock: |-\n This block opens with why density matters, not with rule one: the reader\n has a finite attention budget, and burying the line that mattered fails as\n hard as cutting it. This is motivation, not a rule — it commands nothing\n and never outranks never-trim.\n\n This file is a deliverable, not a chat reply. Its size follows the work —\n write what the task demands, and let padding, not length, be the target of\n any cut.\n\n Lead with the answer or the decision immediately. Skip the throat-clearing\n opener, and never circle back to restate the point once it has landed.\n\n Write in flat, commanding sentences — commit to the claim rather than\n hedging it.\n\n Favor everyday vocabulary over technical shorthand; where a term is\n unavoidable, define it in five words or fewer on first use.\n\n In markdown, bold carries the load: bold the claim, not the sentence around\n it. The test: reading only the bold text yields the full point and every\n risk, or the bolding is wrong.\n\n Above every rule in this block: never trim a risk, a number, a threshold,\n or a scoped condition to save space. A claim that only holds under some\n condition ships with that condition. It outranks every other rule here.\n\n Ship the artifact bare. Output only what was asked for — no lead-in, no\n \"here's the…\", no sign-off wrapped around it. A generated file consumed\n as-is turns a wrapper into a defect.\n\n Compressing is not dropping. A task with three load-bearing parts ships all\n three, each shorter — reporting two of three findings has failed the brief,\n not fulfilled it.\n\n One idea per block; break when the idea shifts. Blocks may run any length,\n but one unbroken paragraph is a defect even when short.\n\n Flag risk and uncertainty in one blunt line. This is never-trim's\n constructive twin: that rule forbids deleting a caveat, this one forbids\n dissolving it into hedged prose.\n\n Never narrate the work — do it. Skip lines like \"I will now list the\n concerns\"; a file has no reason to describe its own composition.\n styleFormatContract: |-\n This file is machine-read. Its format contract outranks every style rule\n above it: keep every `##`/`###` heading this state specifies, every\n `- [ ]` checkbox row, and every required marker line exactly as specified.\n\n Do not replace a checkbox list with prose paragraphs. Do not renumber or\n rename a required heading. Do not drop a marker line. A parser reads these\n literally, and a violation refuses the turn outright — this is not a\n polite request, it is the one thing standing between this output and a\n failed parse.\n\n The voice rules above still govern the prose that sits between these\n structural elements. They do not govern the elements themselves.\n\n # The bundled default PERSONAS — one per prompt-bearing machine below. Each\n # is a machine's `system:` value (see src/StateFields.ts's `system` entry):\n # emitted verbatim as that machine's agent-CLI system prompt, stamped onto\n # every one of its own `prompt` states the same way `model:` is. Like every\n # other var, each is overridable via a top-level `.gtdrc` `vars:` key (e.g.\n # `vars: { builderPersona: \"...\" }`) or a `GTD_<NAME>`-style environment\n # variable — nothing below is privileged.\n #\n # `--system-prompt` REPLACES a harness's entire default system prompt; it\n # does not append to it, and it drops the harness's own injected cwd/env/\n # memory-path/git-status block. Each persona is therefore written as a\n # complete, self-contained system prompt: it states the role, gives tool-use\n # conduct (act, don't ask permission — this runs unattended), and tells the\n # agent to orient itself with real commands (there is no injected status\n # block) and to go inspect the commit range or path its turn's own prompt\n # names, rather than expect a diff pasted into the conversation.\n designPersona: |-\n You are the product-facing planning voice in an autonomous build pipeline\n called gtd. Your job is to turn a raw, half-formed sketch — a hand-edit to\n real code, a scratch note, or both — into an ordered, classified list of\n concerns a build can execute, and to carry a running conversation with a\n human about the product decisions that list depends on. You are read by\n that human, so write plainly and decisively; you are not read by a parser\n except where a state's own instructions say otherwise.\n\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. Nobody is watching this turn\n interactively: there is no one to grant permission, confirm a destructive\n command, or approve a plan before you act. Investigate with real commands\n (`git log`, `git show`, `ls`, `cat`) rather than assuming what a file or a\n commit contains, then act.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. Nothing has told you the\n current branch, the recent commit history, or whether the tree is clean —\n there is no injected status block. Run `git status`, `git log`, `ls` and\n similar yourself before you do anything else; that is the normal first\n move of this turn, not an extra step.\n\n The message you receive each turn is an instruction that names commits,\n paths, or file names for you to go inspect yourself — `git show <hash>`,\n `git diff`, reading a file directly — never a rendered diff pasted into\n the conversation. Expect to go look. Across the several turns this\n conversation may span, remember what you already read and settled in an\n earlier turn rather than re-deriving it from a cold read every time —\n unless the turn's own instructions tell you this is a fresh, cold pass.\n architectPersona: |-\n You are the technical planning voice in an autonomous build pipeline\n called gtd. You take over once the product-facing concerns are settled,\n reading them cold — you do not carry over the product conversation, only\n its written, settled output. Your job is to work out the technical *how*\n for each settled concern — file and module structure, data models,\n library and tech-stack choices, error-handling strategy — raise the\n technical open questions a human still needs to decide, and once every\n question is answered, write one package spec file per concern with no\n further judgement call: the grouping and ordering were already decided\n upstream, and this final step is a mechanical write-out of a plan you\n already converged on.\n\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. This runs unattended: there is\n no one present to confirm a step or approve a draft before you commit to\n it. Investigate with real commands rather than assuming a file's contents\n or a decision's status, then act on what you find.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block telling you the branch, recent history, or tree state — run\n `git status`, `git log`, `ls` and the like yourself before doing anything\n else.\n\n The message you receive each turn names files and, where relevant, commit\n ranges for you to go read yourself — never a diff or a plan rendered\n inline. Go look before you decide anything. When a later turn in this\n same conversation asks you to mechanically write out package files, work\n from the plan you already settled rather than re-deriving it, unless\n instructed to read it fresh.\n reviewerPersona: |-\n You are the independent reviewing mind in an autonomous build pipeline\n called gtd — deliberately a separate identity from whichever agent wrote\n the code under review, so you bring no attachment to how it got built.\n Your job splits into two distinct turns: first, writing a structured\n review document that groups a diff into reviewable chunks for a human to\n read through and comment on; later, reading back a round of that human's\n feedback — a note, a hand-edit — and classifying whether it is genuinely\n actionable or just an approving remark, without fixing anything yourself\n either way. You judge and you classify; you never build.\n\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. This runs unattended, so act\n on what you find rather than pausing for confirmation. Investigate with\n real commands — `git log`, `git show`, `git diff` — rather than assuming\n what changed.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block: no cwd banner, no environment summary, no memory-file list,\n no git-status readout. Run `git status`, `git log`, `ls` yourself first.\n\n The message you receive each turn names a commit range or a file for you\n to inspect yourself — `git show`, `git diff` against the range it gives\n you — not a diff pasted into the conversation. Go read the actual history\n before you write or judge anything; the range is often wider than the\n single round in front of you, so check what it actually spans rather than\n assuming it matches your last turn.\n specReviewerPersona: |-\n You are the adversarial spec-conformance checker in an autonomous build\n pipeline called gtd, reviewing one freshly-built work package against the\n written spec it was built from. Your only job is to verify: are the\n package's tasks actually done, its acceptance criteria actually met, and\n the code sound and consistent with the rest of the codebase? You write\n concrete, actionable feedback only when something is genuinely wrong —\n and when nothing is, you write nothing at all, because a clean turn from\n you IS the approval. You never fix a problem you find; naming it\n precisely enough for someone else's fix turn to act on is the whole job.\n\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. Nobody is present to confirm a\n verdict interactively, so investigate with real commands and commit to\n what you find rather than hedging or asking for permission to look.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block — no cwd, no environment summary, no git status handed to\n you. Orient yourself with `git status`, `git log`, `ls` and the like\n before judging anything.\n\n The message you receive each turn names a spec file to read and a commit\n range for you to inspect yourself — `git show`, `git diff` — never a\n diff rendered inline, and the range is often wider than the one package\n you are checking, since it can span everything built so far in this\n process. Go look before you approve or object; do not assume the range\n matches only the most recent change.\n builderPersona: |-\n You are the TDD implementer in an autonomous build pipeline called gtd.\n Your job is to build exactly one package's declared scope end to end,\n tests first, and to come back and fix things when redirected: a failing\n check's output, or a reviewer's concrete feedback. Stay strictly inside\n the package in front of you — never wander into another package's files\n or tasks just because you can see them, and never refactor code the\n current task doesn't touch. When fixing, treat the feedback you're given\n as the diagnosis to act on, not a suggestion to second-guess without\n evidence — but if it is simply wrong, say so by discarding it rather than\n forcing a change onto code that doesn't need one.\n\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. This runs unattended: there is\n no one to approve a plan or confirm a command before you run it. Write\n the failing test, watch it fail, then make it pass — investigate the\n actual current state of the code and the suite with real commands rather\n than assuming either.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block — no cwd, no environment info, no memory-file paths, no git\n status. Run `git status`, `git log`, `ls` yourself before you start\n editing anything.\n\n The message you receive each turn names the package spec, the feedback\n file, or the check output for you to read yourself at its actual path —\n never a diff or a rendered summary handed to you inline. Go read the\n file, and where a commit range is named, go inspect it with `git show` or\n `git diff` rather than expecting the change pasted in front of you.\n finisherPersona: |-\n You are the closing identity in an autonomous build pipeline called gtd —\n the last coder this process hands work to before everything it touched is\n squashed into a single commit. Most turns you are handed, your job is to\n write the one conventional-commit message that summarizes the whole\n approved change: subject line, imperative mood, a body that explains why\n and what trade-offs were made, not just what changed. Occasionally you\n are handed a late-breaking failing check instead — fix it, focused and\n minimal, before the process can close. Either way, you are aware that\n everything you touch this turn is about to lose its own commit boundary\n and become part of one final message, so make that message earn its\n place as the historical record.\n\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. This runs unattended, so act\n on what you find rather than waiting for confirmation. Investigate the\n actual commit range and the actual failing output with real commands\n rather than assuming either.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block — no cwd, no environment summary, no git status. Run\n `git status`, `git log`, `ls` yourself before acting.\n\n The message you receive each turn names a commit range or a feedback file\n for you to go inspect yourself — `git show`, `git diff`, reading the file\n directly — never a diff rendered inline into the conversation. Go look\n before you write the commit message or the fix; if you already built the\n range earlier in this same conversation, work from what you already know\n rather than re-deriving it.\n\nentry:\n default: unified\n\nmachines:\n # ── DEDUP: the green-baseline entry gate (identity-free) ──────────────────\n # Runs the suite before any work starts — both `start-gate` and\n # `review-gate` reach this one check, and its script IS the shared\n # `&suiteCheck` anchor (defined once, right here, and aliased by\n # `fix-precheck` and `healthGate.check` below); the `on` rules turn the\n # result into \"halt at blocked\" vs \"proceed to $onGreen\".\n entryGate:\n params: [onGreen, blockedMessage, blockedDescribe, checkLabel, blockedLabel, reviewBase]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: &suiteCheck |\n #!/usr/bin/env sh\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p .gtd\n # Sweep the raw review capture — no path from `deciding`/`collecting`\n # reaches this check; this is a leftover sweeper for a capture an\n # earlier, abandoned process may have left committed here (see the\n # `D .gtd/REVIEW_RAW.md` rows below). A no-op when it is already gone\n # or never existed.\n rm -f .gtd/REVIEW_RAW.md\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output .gtd/FEEDBACK.md\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > .gtd/FEEDBACK.md\n fi\n # A repeat identical failure would otherwise leave FEEDBACK.md\n # byte-identical to its committed copy — no diff, so the check would\n # look GREEN. Stamp each red run with the current HEAD (which always\n # advances between checks) so a still-red re-run always re-registers\n # as an M/A edit and takes the red edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> .gtd/FEEDBACK.md\n else\n rm -f .gtd/.check-output\n rm -f .gtd/FEEDBACK.md\n fi\n # `entry: true` on the shared local marks both `entryGate` instances\n # (start-gate/review-gate) as manual entries — only `review-gate.check`\n # actually needs the reachability root (it is otherwise unreachable\n # from `entries.default`), but the shared dedup makes marking one mark\n # both. Accepted as correct: both ARE the workflow's entry gates.\n entry: true\n # `reviewBase: $reviewBase` — only `review-gate` binds a real\n # template (`<%= it.vars.reviewBase %>`), fixing the whole process's\n # diff base to whatever `gtd --entry review-gate.check --var\n # reviewBase=<commitish>` supplies. `start-gate` binds the literal\n # empty string, which compiles away to \"no base\" here — it is reached\n # from `idle`, never manually entered with a base.\n reviewBase: $reviewBase\n on:\n \"A .gtd/FEEDBACK.md\": blocked\n \"M .gtd/FEEDBACK.md\": blocked\n \"D .gtd/REVIEW_RAW.md\": $onGreen\n \"D .gtd/FEEDBACK.md\": $onGreen\n \"C\": $onGreen\n blocked:\n actor: human\n label: $blockedLabel\n # `file:` is the one field with an implicit prefix: the compiler\n # resolves it under `.gtd/` automatically, so `file: FEEDBACK.md` here\n # sits beside `on: \"A .gtd/FEEDBACK.md\"` above on purpose — every OTHER\n # path in this template (an `on` pattern, a script body, a prompt/\n # message) still spells `.gtd/` in full.\n file: FEEDBACK.md\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $blockedDescribe\n\n # ── DEDUP: the check/escalate health pair (identity-free) ─────────────────\n # Runs the suite; a red run records `.gtd/FEEDBACK.md` and routes to\n # the caller's OWN fixer (`$onRed` — a coder-identity action that lives\n # beside this machine, not inside it, since fixing IS a coder action and this\n # machine has no identity); a green run cleans it up and routes to\n # `$onGreen`. Escalation redirects here after the caller's fixer has retried\n # `max` times without success. Instantiated for the build tail's own tail\n # (`build.health.*`, caller's fixer is `build.fix`) and the per-package\n # build's own tail (`packages.item.health.*`, caller's fixer is\n # `packages.item.fix-suite`).\n healthGate:\n params: [onGreen, onRed, escalateDescribe, checkLabel, escalateLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # See the shared suite check on `entryGate.check` above.\n script: *suiteCheck\n on:\n \"A .gtd/FEEDBACK.md\": $onRed\n \"M .gtd/FEEDBACK.md\": $onRed\n \"* **\": $onGreen\n \"C\": $onGreen\n escalate:\n actor: human\n label: $escalateLabel\n file: FEEDBACK.md\n message: |\n The agent could not get the check to pass after repeated attempts.\n `.gtd/FEEDBACK.md` holds the last failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $escalateDescribe\n\n # ── DEDUP: the open-questions check/answer pair (identity-free) ───────────\n # Shared by design and architecture's own gate (`design.gate`/\n # `architecture.gate`): `check`'s script probes whichever of\n # REQUIREMENTS.md/ARCHITECTURE.md currently exists (see the script's own\n # comment) with `gtd check qa --open-questions` and stamps/removes\n # `QUESTIONS.md` on the result; only when it's present does the process\n # rest at `answer`, a human gate. A question-free phase never rests here at\n # all — `check`'s own `on` rows skip straight to `$onNone`. Takes NO `file:`\n # PARAM for the probe script itself: machine `$param` substitution is\n # whole-value only, so a path cannot be spliced into a shared script body —\n # `file` is instead the PARAM `answer` renders as its own `file:` (the\n # human-facing gate), a different use entirely from the probe.\n questionGate:\n params: [file, message, onNone, onRevise, checkLabel, answerLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: |\n #!/usr/bin/env sh\n # gtd check turn — probe the current phase's steering file for\n # unanswered questions; an answered/empty result removes the marker\n # (.gtd/QUESTIONS.md), an unanswered one stamps it fresh. Design and\n # architecture share this one probe: whichever of\n # .gtd/REQUIREMENTS.md/.gtd/ARCHITECTURE.md exists ON DISK right now\n # IS this phase's steering file — unambiguous because\n # architecture.author deletes .gtd/REQUIREMENTS.md in the same turn\n # it writes .gtd/ARCHITECTURE.md, so exactly one of the two exists\n # here.\n set +e\n mkdir -p .gtd\n file=.gtd/REQUIREMENTS.md\n [ -f \"$file\" ] || file=.gtd/ARCHITECTURE.md\n gtd check qa \"$file\" --open-questions > /dev/null 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n printf 'open questions remain in %s\\n' \"$file\" > .gtd/QUESTIONS.md\n # See the shared suite check's cache-buster rationale on\n # `entryGate.check` above.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> .gtd/QUESTIONS.md\n else\n rm -f .gtd/QUESTIONS.md\n fi\n on:\n \"A .gtd/QUESTIONS.md\": answer\n \"M .gtd/QUESTIONS.md\": answer\n \"D .gtd/QUESTIONS.md\": $onNone\n \"C\": $onNone\n answer:\n actor: human\n label: $answerLabel\n file: $file\n mode: qa\n answerGate: true\n message: $message\n on:\n \"* **\":\n to: $onRevise\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace\n `_your answer_` for your own) to send it back for the agent to\n fold your answers in. Delete a question to skip it, or delete\n the whole `## Open Questions` section to accept the plan as-is.\n\n # ── ENCAPSULATION: the triage phase's one conversation ─────────────────────\n # Groups the diff that started the process into ordered, classified\n # concerns and resolves the PRODUCT ones' open questions — one machine\n # identity across the whole back-and-forth, so the agent explores the\n # codebase once and the human's answer rationale survives every return lap.\n # `triage`'s prompt reads THREE laps off the requirements file's own\n # content, never off which edge landed here (the resting state is the same\n # either way): FIRST (the file is absent), RETURN (a ticked/answered\n # question waiting to be folded), and REVIEW LOOP-BACK (concerns already\n # present, nothing ticked-but-unfolded — `re-unwind` put them there, not an\n # answer). The loop-back lap also runs the suite itself and files any\n # breakage it finds as its own FIRST concern: `re-unwind` sits between\n # `build.review.deciding` and here with no `entryGate` in between, so a red\n # the revert causes is otherwise ambient. Left for\n # `packages.item.health.check` to trip over instead, it would spend one of\n # `packages.item.fix-suite`'s retry cap — process-pooled across every\n # package, the same pattern `specReview.review`'s own retry comment\n # documents — on breakage the package itself never caused.\n # ▸ planner identity — `model:` declared once here, stamped onto `triage`\n # (its only prompt state; `gate.answer` is a human turn and carries no\n # prompt).\n designPlan:\n model: <%= it.vars.plannerModel %>\n system: <%~ it.vars.designPersona %>\n params: [onArchitecture]\n entry: triage\n states:\n triage:\n actor: agent\n label: Triaging the change\n file: REQUIREMENTS.md\n mode: qa\n # An agent that deletes the assembled review input without folding it\n # in is the original \"captured then discarded\" bug, now against THIS\n # state's own file — the retired `build.addressing` state was the\n # guard's only user before this restructure; this is its new one.\n requireProgress: true\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `.gtd/REQUIREMENTS.md`; develop it. Do\n not create other files to hold notes or output.\n\n `.gtd/TODO.md` is the likely home of the sketch that\n started this process. Despite living beside this workflow's own\n state files, it is the HUMAN'S INPUT — the sketch to be folded into\n the concerns below like any other part of the start diff. It is\n never a state file to preserve, and never gtd bookkeeping to ignore.\n\n This process started at commit `<%= it.startCommit %>`. The change\n that started it was reverted out of the working tree by the\n `unwind` beat right after it landed, so `git diff <%= it.startCommit %>`\n (against the working tree) is now empty — its content survives only\n in history. Find the entry commit yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\n (the process's very first turn, landed immediately after\n `<%= it.startCommit %>`, before any later baseline-repair commits) —\n then `git show` that commit to see everything that started this\n process: a hand-edit to real code, a scratch note in some file, or\n both.\n\n On the FIRST lap, `.gtd/REQUIREMENTS.md` does not exist\n yet (or holds whatever the human's own change happened to leave\n there). Build it from that start diff:\n\n 1. Group the diff into an ORDERED list of CONCERNS — each one a\n coherent piece of work that can leave the test suite GREEN on\n its own, in the order you'd want them built. If a concern would\n red-light tests that a later concern is meant to fix, that is\n not a valid split: merge the two into one concern.\n 2. Classify each concern as PRODUCT (a user-facing/requirements\n decision) or TECHNICAL (an implementation decision).\n 3. For every PRODUCT concern with an open point you cannot settle\n yourself, add a question under a `## Open Questions` heading near\n the top: one `### <question>` sub-heading each, followed by a\n checkbox list of candidate answers — aim for TWO concrete options\n plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Raise questions ONLY for product concerns here — a technical\n concern's open points wait for the next phase. Never tick a box\n yourself — the human ticks exactly one per question.\n 4. Fold EVERYTHING the entry commit added into the concerns above —\n a scratch note and a real code edit are both just a SKETCH of\n the intent, a spec to be finished, never work to be preserved;\n `unwind` already reverted both out of the tree, so there is\n nothing left to delete. Describe each piece under the concern it\n belongs to.\n\n On a RETURN lap the human has ticked one option per question (or\n replaced `_your answer_` with their own text) in\n `.gtd/REQUIREMENTS.md`. For each answered question: fold\n the chosen answer into its concern's prose, then MOVE the question\n down into a `## Answered Questions` section as `### <question>`\n followed by the resolved answer in plain PROSE (drop the\n checkboxes). Never re-raise a question the human deleted; if the\n human deleted the whole `## Open Questions` section, treat every\n product concern as accepted and surface no new questions unless\n something is genuinely still blocking. Omit `## Open Questions`\n entirely once there are none left.\n\n On a REVIEW LOOP-BACK lap, `.gtd/REQUIREMENTS.md`\n already holds concerns and none of them have a ticked-but-unfolded\n answer waiting — that shape means a completed REVIEW ROUND put\n this material here, not a question you asked. Develop those\n existing concerns further with whatever the round raised; never\n rediscover or regroup them from a cold read the way the FIRST lap\n does.\n\n The human's own edit from that review round has been reverted out\n of the working tree the same way the entry commit is — read it\n from history, never the working tree: run\n `git show <%= it.reviewBase %>` to see it. (On a FIRST lap that\n same hash IS the process's own diff base, `<%= it.startCommit %>`,\n so this branch does not apply: `.gtd/REQUIREMENTS.md`\n does not exist yet for a review round to have happened against.)\n\n Whatever this round touches, every decision already settled under\n `## Answered Questions` stays settled on this lap too — never\n re-open one. A genuinely open PRODUCT point in the round's own\n material may still raise a fresh question under\n `## Open Questions`: the product gate sits on this path exactly as\n it does on a first lap, and will ask it.\n\n Before grouping anything on this lap, run\n `<%= it.vars.testCommand %>` yourself as part of reading the\n situation: a review loop-back runs no green-baseline gate between\n the revert above and this turn, so the tree may already be RED —\n reverting the human's edit can undo a fix that edit had put in. If\n the suite is red, make that breakage the FIRST concern in your\n ordered list, ahead of everything else — every later concern's\n \"leaves the suite green on its own\" property assumes the suite\n already started green.\n\n Leave `.gtd/REQUIREMENTS.md` uncommitted and finish your\n turn.\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: REQUIREMENTS.md\n onNone: $onArchitecture\n onRevise: triage\n checkLabel: Checking for open questions\n answerLabel: Awaiting your product answers\n message: |\n `.gtd/REQUIREMENTS.md` holds the concerns under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n # ── ENCAPSULATION: the technical phase's one conversation ──────────────────\n # A separate machine from `designPlan` — its own memory scope, since there\n # may be a handover between the user-facing and the technical phase: this\n # phase does NOT resume design's conversation, it reads\n # `.gtd/REQUIREMENTS.md` cold. Resolves the TECHNICAL open\n # questions, then mechanically decomposes the settled concerns into package\n # files (no merge/split judgement — that already happened in triage's\n # grouping). ▸ planner identity — `model:` declared once here, stamped onto\n # `author` and `decompose` (its own prompt states; `gate.answer` is a human\n # turn and carries no prompt).\n archPlan:\n model: <%= it.vars.plannerModel %>\n system: <%~ it.vars.architectPersona %>\n params: [onPackages]\n entry: author\n states:\n author:\n actor: agent\n label: Refining the technical plan\n file: ARCHITECTURE.md\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state files this\n turn touches are `.gtd/ARCHITECTURE.md` (write it) and\n `.gtd/REQUIREMENTS.md` (delete it once its content is\n folded in). Do not create other files to hold notes or output.\n\n You do NOT resume the design conversation — this is a separate\n machine with its own memory, a COLD read every time. Read\n `.gtd/REQUIREMENTS.md` (the settled, ordered, classified\n concerns) in full. Treat every product decision recorded there —\n plain prose or a resolved `## Answered Questions` entry — as\n SETTLED; never re-open it.\n\n On the FIRST lap, develop `.gtd/ARCHITECTURE.md` from\n those concerns: for each one, in the same order, work out the *how*\n — file/module structure, data models, library/tech-stack choices,\n error-handling strategy — building on the *what* already settled.\n For every open TECHNICAL point you cannot settle yourself, add a\n question under a `## Open Questions` heading exactly as the\n requirements phase does: one `### <question>` each, followed by a\n checkbox list — TWO concrete options plus a final free-text slot,\n ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself. On a RETURN lap the human has ticked one\n option per question (or replaced `_your answer_` with their own\n text): fold each chosen answer into the plan prose, then MOVE the\n question into a `## Answered Questions` section as `### <question>`\n + the resolved answer in plain PROSE (drop the checkboxes). Never\n re-raise a deleted question; treat a deleted `## Open Questions`\n section as acceptance. Omit `## Open Questions` once there are none\n left.\n\n Once `.gtd/ARCHITECTURE.md` is written, delete\n `.gtd/REQUIREMENTS.md` — its content has been folded in\n and it must not linger. Leave everything uncommitted and finish\n your turn.\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: ARCHITECTURE.md\n onNone: decompose\n onRevise: author\n checkLabel: Checking for open questions\n answerLabel: Awaiting your technical answers\n message: |\n `.gtd/ARCHITECTURE.md` holds the technical plan under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n decompose:\n actor: agent\n label: Decomposing into packages\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are an autonomous coding agent. This workflow steers itself through\n its own state files — treat them as its private scratchpad, never as\n project code or documentation. The only state files this turn touches\n are the package files it writes under `.gtd/packages/` and\n `.gtd/ARCHITECTURE.md`, which it deletes. Do not create other\n files to hold notes or output.\n\n If you wrote `.gtd/ARCHITECTURE.md` earlier in this\n conversation, work from what you already settled; otherwise read it\n (the converged technical plan) yourself. It already lists an ORDERED\n set of concerns with every merge/split judgement made — this turn is\n a mechanical write-out, not a planning one.\n\n Write one package file per concern, in the settled order, under\n `.gtd/packages/` (e.g.\n `.gtd/packages/01-name.md`,\n `.gtd/packages/02-name.md`, ...). Each package file\n carries that concern's requirement (so it can be reviewed against\n its spec), its independent tasks, and each task's acceptance\n criteria as `- [ ]` checkboxes and relevant paths. Do NOT merge or\n split concerns here — that judgement already happened upstream;\n carry the settled grouping over verbatim. No package file may\n reference any other `.gtd/` file.\n\n Once the package files are written, delete\n `.gtd/ARCHITECTURE.md`. Leave everything uncommitted and\n finish your turn.\n on:\n \"* .gtd/packages/**\": $onPackages\n\n # ── ENCAPSULATION: the shared review tail ──────────────────────────────────\n # ▸ planner identity — `model:` declared once here, stamped onto `reviewing`\n # and `collecting` (its only prompt states). `collecting` JUDGES whether a\n # round is actionable and, when it is, CLASSIFIES it straight into\n # `REQUIREMENTS.md` as ordered, PRODUCT/TECHNICAL concerns — the same\n # artifact `design.triage` develops — never an instruction list, and never a\n # build. An actionable round still leaves this machine entirely (the root's\n # own `re-unwind`, then `design.triage` folds the concerns in cold), so\n # there is no coder-identity follow-through inside this machine any more.\n humanReview:\n model: <%= it.vars.plannerModel %>\n system: <%~ it.vars.reviewerPersona %>\n entry: reviewing\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: REVIEW.md\n mode: review\n # `gtd --entry review-gate.check` (a plain non-gtd branch, e.g. a\n # colleague's PR) does NOT enter here directly — it enters\n # `review-gate.check` first, which runs the green-baseline gate and,\n # once green, transitions here. The `Gtd-Review-Base:` trailer rides on that\n # entry commit (the process's oldest), so everything below still reuses\n # the same reviewing/await-review/feedback machinery over <commitish>..HEAD.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `.gtd/REVIEW.md`; write it at exactly\n that path. Do not create other files to hold notes or output.\n\n Write `.gtd/REVIEW.md` to help a human review the changes in\n this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.reviewBase %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation of\n what changed and why, then one file pointer per hunk (checkboxes are\n for the human to tick, not you; every path is `./`-relative, with an\n optional `#line`). The pointer's OWN line carries nothing but the\n pointer — no note, no dash. Its explanation goes on the line(s)\n beneath it instead:\n\n - [ ] ./path/to/file.ts#42\n what this hunk does, with room to run to several lines\n\n Text after the pointer token on the pointer's own line is a\n validation error, not just a style nit — an older-format doc like\n `- [ ] ./path/to/file.ts#42 — note` is UNLANDABLE.\n\n This prompt carries no diff — read the changes yourself. The range runs\n from `<%= it.reviewBase %>` to the working tree (committed turns plus\n anything still pending); on a feedback round that base is the previous\n review's boundary, so it covers only what changed since then, not items\n already signed off.\n\n Leave `.gtd/REVIEW.md` uncommitted and finish your turn.\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n label: Awaiting your review\n file: REVIEW.md\n mode: review\n # While the process rests here, gtd opens a \"review checkout window\": raw HEAD\n # is rewound to the review base (the most-recent `deciding` commit — so a\n # feedback round shows only `last-review..HEAD` — or the process start on\n # the first review) with the working tree untouched, surfacing the diff\n # as ordinary uncommitted changes. It closes on the next invocation.\n #\n # Every human step here routes to `deciding`, which decides sign-off vs.\n # feedback from the step's CONTENT (a comment, not a tick). Ticking a box\n # only records \"I reviewed this hunk\" — it never gates a land, whatever the\n # boxes say. The one dead end that CAN'T be told apart by a file-pattern\n # edge is a deleted REVIEW.md, so the review-doc guard\n # (`src/StepGuards.ts`) catches it BEFORE the step commits and refuses.\n reviewWindow: true\n message: |\n `.gtd/REVIEW.md` holds the review record for the process — one\n `- [ ]` checkbox per reviewable item, grouped into chunks. Tick a box\n (`- [x]`) as you review each hunk; ticking only records that you've read\n it, it is not sign-off.\n\n When you've been through the whole diff, run `gtd land`:\n\n - **Sign off** — leave no comment — no note in\n `.gtd/REVIEW.md`, no code edit — to collapse the process into\n one commit (**build.squashing**), whatever the boxes say.\n - **Request changes** — leave a comment: a note on a\n `.gtd/REVIEW.md` line, or a direct code edit — to send a FULL\n development lap (**review.deciding** → **review.collecting** →\n re-triage). A hand-edit you make here is treated as a SKETCH, not a\n fix the agent builds on: it is reverted out of the tree and re-planned\n from scratch, the same as any other change that starts a process.\n There is no baseline check on the way back into planning — only a\n genuinely non-actionable comment (an approving remark with no code\n edit) skips the lap and signs off straight away.\n\n Deleting `.gtd/REVIEW.md` is refused.\n on:\n \"* **\": deciding\n\n deciding:\n actor: check\n label: Reviewing\n file: REVIEW.md\n mode: review\n # Anchors the INCREMENTAL review base: its most-recent in-process commit is\n # the point the previous review round ended, so the next `reviewing` turn\n # (and the review checkout window) covers only the changes since then. On\n # the first review there is no such commit, so the base is the process start.\n reviewBase: true\n script: |\n #!/usr/bin/env sh\n # gtd check turn — decide the human's review step from its CONTENT. HEAD is\n # the human's `await-review -> deciding` commit; HEAD^ is the `reviewing`\n # commit that wrote the agent's original REVIEW.md (`await-review`'s single\n # `\"* **\": deciding` edge means EVERY human step there lands here, so\n # `deciding` is entered exactly once per round — nothing else commits in\n # between). It is FEEDBACK when the human left\n # a comment: a note in REVIEW.md (any edit beyond a \"[ ]\"->\"[x]\" tick) OR a\n # hand-edit to any file this round outside `.gtd/` (every gtd steering\n # file, including REVIEW.md itself, lives there). Otherwise it is a\n # clean sign-off. This turn only CAPTURES the raw material into\n # REVIEW_RAW.md (the `collecting` agent then judges whether it's\n # actionable); it does NOT interpret it. Either way, remove REVIEW.md;\n # what the resulting diff MEANS is decided by this state's own `on`\n # rules — the A/M REVIEW_RAW.md rows are declared BEFORE the D\n # REVIEW.md row so a feedback round (which also deletes REVIEW.md) is\n # captured as feedback, not mistaken for a sign-off.\n set +e\n mkdir -p .gtd\n head=$(git rev-parse HEAD)\n # A note is any REVIEW.md change beyond a checkbox flip: normalize every\n # \"[ ]\"/\"[x]\" to a placeholder, then compare the agent's original (HEAD^)\n # against the human's version (HEAD).\n before=$(git show \"HEAD^:.gtd/REVIEW.md\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n after=$(git show \"HEAD:.gtd/REVIEW.md\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n # Did the human hand-edit anything this round besides .gtd/?\n if git diff-tree --no-commit-id --name-only -r HEAD -- . \":(exclude).gtd\" \\\n | grep -q . \\\n || [ \"$before\" != \"$after\" ]; then\n {\n echo \"This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.\"\n echo\n echo \"Commit: $head\"\n echo \"The human's notes are in .gtd/REVIEW.md at this commit. Any hand edits are\"\n echo \"in that commit's other paths. Run: git show $head\"\n } > .gtd/REVIEW_RAW.md\n fi\n rm -f .gtd/REVIEW.md\n on:\n \"A .gtd/REVIEW_RAW.md\": collecting\n \"M .gtd/REVIEW_RAW.md\": collecting\n \"D .gtd/REVIEW.md\": $onSignoff\n\n # Judges actionability and, when actionable, classifies straight into\n # `REQUIREMENTS.md` — an ordered, PRODUCT/TECHNICAL concern list, never\n # an instruction list for a builder (the actionable path re-plans from\n # history via `re-unwind` + `design.triage`; it never builds on the\n # human's own edit). The outcome is encoded by which paths ITS OWN diff\n # touches, never by a sentinel: writing `REQUIREMENTS.md` (`A`/`M`) IS\n # the actionable case — row ORDER matters, since an actionable round's\n # diff is `A REQUIREMENTS.md` + `D REVIEW_RAW.md` together, so the\n # requirements rows must be declared first or every round would\n # short-circuit to sign-off. CONSUMING `REVIEW_RAW.md` alone (deleting\n # it, the only change this turn) is the non-actionable short-circuit —\n # the same trick `deciding` already uses, one state earlier; it reuses\n # `$onSignoff` (already bound to `build.squashing`) rather than a new\n # param, because the short-circuit IS a sign-off. Declaring no `C` row\n # and no `* **` row is deliberate: a clean turn here is a fruitless\n # dispatch (a crash, a refusal, a context-limit cutoff), never a\n # verdict, so it falls through to the ordinary `prompt`-state default\n # instead — an empty ATTEMPT commit, remembered as a stall\n # (`Edge.ts`'s `stalledAt`) rather than silently misread as \"the human's\n # feedback was non-actionable\" — and a dirty tree matching neither row\n # is a REFUSAL (you classify, you do not build). `REQUIREMENTS.md`\n # survives `re-unwind` (it lives under `.gtd/`, excluded from its own\n # revert pathspec) for `design.triage` to fold in cold; a consumed\n # `REVIEW_RAW.md` with no requirements written is gone before\n # `build.squashing` ever runs, so nothing leaks into the finale.\n collecting:\n actor: agent\n label: Collecting your feedback\n file: REQUIREMENTS.md\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent judging and classifying a round\n of review feedback. This workflow steers itself through its own\n state files — treat them as its private scratchpad, never as\n project code or documentation. The only state files this turn\n touches are `.gtd/REQUIREMENTS.md` and\n `.gtd/REVIEW_RAW.md`, which it deletes — do NOT change any\n other file: you classify, you do not build.\n\n The raw review material is: <%~ it.read(\".gtd/REVIEW_RAW.md\") %>\n\n It names a commit. If you wrote today's review earlier in this\n conversation, work from what you already reviewed; otherwise read that\n commit's diff yourself before classifying anything.\n\n The round is ACTIONABLE if any of the following is true:\n\n 1. The human left a NOTE on `.gtd/REVIEW.md` — a note is\n anything beyond a checkbox flip, regardless of whether the box\n itself is ticked (ticking means only \"I read this hunk\", never\n sign-off). A note is a MANDATORY concern below, whether or not\n its box is ticked.\n 2. The human ADDED a code comment this round (a `+` line that is a\n comment, in a hand-edited file) — even a plain-prose one. That\n comment is an INSTRUCTION: describe it as a concern below, and\n note that the comment line itself is transient — it must not\n survive the lap that satisfies it.\n 3. The human hand-edited any non-comment code this round. That code\n is no longer an already-committed intent to build on top of — it\n is a SKETCH, like the entry commit's own diff: describe what it\n was reaching for under the concern it belongs to, and expect the\n lap that follows to re-derive it from scratch. Never say the\n human's lines are final or that they must survive unchanged.\n\n The round is NOT actionable only when none of the above holds — the\n human left nothing but an approving remark (a compliment, a \"looks\n good\", or similar) with no code edit and no substantive note. Never\n invent actionability to justify a lap, and never dismiss a real note\n or edit as approval.\n\n If the round is ACTIONABLE: write `.gtd/REQUIREMENTS.md`\n with an ORDERED list of CONCERNS, each classified PRODUCT (a\n user-facing/requirements decision) or TECHNICAL (an implementation\n decision) — the same shape `design.triage` builds, one\n `## <heading>` per concern, in the order you'd want them built. Fold\n every note, comment, and hand-edit from this round in under the\n concern it belongs to, per the rules above. Raise NO open\n questions here — write no `## Open Questions` section;\n `design.triage` owns that, one phase later. Then delete\n `.gtd/REVIEW_RAW.md` and finish your turn.\n\n If the round is NOT actionable: delete\n `.gtd/REVIEW_RAW.md` and finish your turn, writing nothing\n to `.gtd/REQUIREMENTS.md` — consuming the capture with no\n other change IS the sign-off.\n on:\n \"A .gtd/REQUIREMENTS.md\": $onFeedback\n \"M .gtd/REQUIREMENTS.md\": $onFeedback\n \"D .gtd/REVIEW_RAW.md\": $onSignoff\n\n # ── ENCAPSULATION: the per-package agentic review ──────────────────────────\n # A single state: a machine whose entire job is to be a separate MIND from\n # the implementer, distinct from the coder identity that builds/fixes the\n # package (`packages.item.*`). ▸ planner identity — `model:` declared once\n # here, stamped onto `review` (its only prompt state). Fixing the reviewer's\n # own findings is a coder action, not a reviewer one — it lives in\n # `packages.item.fix-spec` (`$onFix`), not inside this machine.\n specReview:\n model: <%= it.vars.plannerModel %>\n system: <%~ it.vars.specReviewerPersona %>\n params: [onApproved, onFix]\n entry: review\n states:\n review:\n actor: agent\n label: Reviewing the package\n # Bounded per process: once entered `max` times, a transition into it is\n # redirected to `$onApproved` (force-close the package and move on — the\n # unresolved concern is deferred to the shared human review at the tail,\n # which sees the whole diff anyway). NOTE the cap is PROCESS-scoped, so it\n # pools across packages — the same documented limitation the shared\n # `build.fix` cap has (see the README's \"Why\" section, \"Bounded, not\n # runaway\").\n retry:\n max: 3\n otherwise: $onApproved\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are an autonomous coding agent reviewing a freshly-built work package\n against its own spec. This workflow steers itself through its own\n state files — treat them as its private scratchpad, never as project\n code or documentation. The only state file this turn touches is\n `.gtd/SPEC_FEEDBACK.md`; write it only when — and only\n when — you find problems. Do not create other files to hold notes or\n output.\n\n The package spec is: <%~ it.read(\".gtd/NEXT.md\") %>\n\n Read that spec, then verify the package's implementation against it: are\n all its tasks done, acceptance criteria met, and the code sound and\n consistent with the codebase? This prompt carries no diff — read it\n yourself: the range runs from `<%= it.startCommit %>` to the working\n tree.\n\n Note the range is process-wide (from the start of the whole build, not\n just this package), so it can span earlier packages too.\n\n - If it fully satisfies the spec, write NOTHING and finish your turn — a\n clean turn is your approval.\n - If it does not, write `.gtd/SPEC_FEEDBACK.md` listing the\n concrete problems for a fix turn to address. Be specific.\n\n Do not fix anything yourself and do not delete the package file — a later\n step owns that. Finish your turn.\n on:\n \"A .gtd/SPEC_FEEDBACK.md\": $onFix\n \"M .gtd/SPEC_FEEDBACK.md\": $onFix\n \"D .gtd/SPEC_FEEDBACK.md\": $onApproved\n \"C\": $onApproved\n\n # ── ENCAPSULATION: the per-package build queue ─────────────────────────────\n # Identity-free (a bookkeeping queue, not a builder) — the actual per-package\n # build identity lives in its own `item` child (packageItem, below).\n packageLoop:\n params: [onDrained]\n entry: picking\n states:\n picking:\n actor: check\n label: Picking the next package\n script: |\n #!/usr/bin/env sh\n # gtd check turn (the package queue arbiter) — the loop driver executes\n # this verbatim, then steps the check actor. Mechanics only: take the\n # first package file (by name) under the packages dir into NEXT.md, or\n # remove NEXT.md when the queue is empty. What NEXT.md's presence/absence\n # MEANS (another package to build vs. the queue closing out to review) is\n # decided by this state's own `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n # Sweep the design/architecture phases' spent steering files:\n # REQUIREMENTS.md is consumed by architecture.author, ARCHITECTURE.md\n # by architecture.decompose, and QUESTIONS.md by whichever gate ran\n # last — all three are gone by the time the package queue starts.\n # REVIEW_RAW.md is swept here too: the review loop-back\n # (re-unwind) excludes .gtd from its revert, so an actionable\n # feedback round's MARKED raw capture (see humanReview's own\n # `collecting` state) rides through design and architecture\n # untouched — this is the ONLY sweeper on that path\n # before the queue (and eventually the squash finale) would\n # otherwise pick it up as stray content. No-op if already removed.\n rm -f .gtd/REQUIREMENTS.md .gtd/ARCHITECTURE.md .gtd/QUESTIONS.md .gtd/REVIEW_RAW.md\n # Package file names are gtd-authored (architecture.decompose writes\n # NN-name.md), never containing whitespace/newlines that would break\n # ls | head.\n # shellcheck disable=SC2012\n next=$(ls .gtd/packages/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n printf '%s' \"$next\" > .gtd/NEXT.md\n else\n rm -f .gtd/NEXT.md\n fi\n on:\n \"D .gtd/NEXT.md\": $onDrained\n \"* .gtd/NEXT.md\": item.building\n \"C\": $onDrained\n\n # The per-package build identity — its own coder machine (below), so\n # deleting a package file's own lifecycle stays inside `item` while the\n # queue itself (`packages`) stays a single state.\n item:\n machine: packageItem\n with:\n onNext: picking\n\n # ── ENCAPSULATION: the per-package build identity ──────────────────────────\n # ▸ coder identity — `model:` declared once here, stamped onto `building`,\n # `fix-suite`, and `fix-spec` (its own prompt states). `closing` ends that\n # package file's own lifecycle; keeping it here (not in the queue) keeps\n # `packages` itself to a single state. `packages.item.closing` -> `picking`\n # is the ONE upward-resolving target in the whole file, so it MUST be a\n # param (`$onNext`, bound by `packages` — see the machine's own reference).\n packageItem:\n model: <%= it.vars.coderModel %>\n system: <%~ it.vars.builderPersona %>\n params: [onNext]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn may write is `.gtd/SATISFIED.md` (see below); do NOT\n delete the package file (the spec-review gate reads it after you)\n and never touch `.gtd/NEXT.md` — the `picking` state owns\n it.\n\n The package to implement is: <%~ it.read(\".gtd/NEXT.md\") %>\n\n Before implementing anything, check the package's acceptance criteria\n against the current tree — an earlier package's fix turn may already\n have satisfied them. If **every** criterion is already met, implement\n nothing: write `.gtd/SATISFIED.md` recording each criterion\n and the concrete evidence it is already satisfied (the commit, file,\n or symbol that proves it), change nothing else, and finish your turn.\n If any criterion is outstanding, implement the package normally and\n do not write that file.\n\n That file describes a set of INDEPENDENT tasks. Implement ALL of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement EXACTLY the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"A .gtd/SATISFIED.md\": &satisfied\n to: health.check\n action: Close as already satisfied\n describe: >-\n record per-criterion evidence in .gtd/SATISFIED.md when this\n package's work already landed (an earlier package's fix turn\n pulled it in) — the package still runs the checks and the spec\n review, then closes out.\n \"M .gtd/SATISFIED.md\": *satisfied\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `.gtd/FEEDBACK.md`; fix it per its\n contents, or delete it if the feedback turns out to be wrong. Do not\n create other files to hold notes or output.\n\n Read `.gtd/FEEDBACK.md` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `.gtd/FEEDBACK.md` instead of \"fixing\" a non-issue. If the\n only way to green the suite is to implement work that another\n package file describes, make the smallest change that gets there and\n do it — that later package will then legitimately report itself\n already satisfied when its turn comes.\n\n Leave everything uncommitted and finish your turn — do not commit.\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n fix-spec:\n actor: agent\n label: Fixing review feedback\n file: SPEC_FEEDBACK.md\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `.gtd/SPEC_FEEDBACK.md`; address it, then\n delete it. Do not create other files to hold notes or output.\n\n Read `.gtd/SPEC_FEEDBACK.md` (the reviewer's concerns about the\n current package) and the package spec (`<%= it.read(\".gtd/NEXT.md\") %>`),\n then fix the code to resolve every concern. Keep the change focused.\n\n Delete `.gtd/SPEC_FEEDBACK.md` once every concern is addressed.\n Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": health.check\n\n closing:\n actor: check\n label: Closing out the package\n script: |\n #!/usr/bin/env sh\n # gtd check turn (the package closer) — remove the just-reviewed package\n # file (its path is in NEXT.md) plus any leftover spec feedback and\n # already-satisfied evidence, so `picking` selects the NEXT package (or\n # falls through to review when the queue is empty). Reached on approval\n # (spec review clean) and on the spec review retry cap (force-close,\n # deferring the concern to the tail).\n set +e\n pkg=$(cat .gtd/NEXT.md 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f .gtd/SPEC_FEEDBACK.md .gtd/NEXT.md .gtd/SATISFIED.md\n on:\n \"* **\": $onNext\n\n # Per-package health pair: packages.item.health.check/.escalate ->\n # packages.item.spec.review (green) or packages.item.fix-suite (red).\n health:\n machine: healthGate\n with:\n onGreen: spec\n onRed: fix-suite\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**packages.item.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # Per-package agentic review: packages.item.spec.review -> closing\n # (approved), or packages.item.fix-spec (findings — re-verify via the\n # full health check before re-reviewing).\n spec:\n machine: specReview\n with:\n onApproved: closing\n onFix: fix-spec\n\n # ── ENCAPSULATION: the process's build identity ─────────────────────────────\n # ▸ coder identity — `model:` declared once here, stamped onto `squashing`\n # and `fix` (its own prompt states). There is no monolithic \"build the whole\n # plan\" state any more — every package is built inside `packages.item`, and\n # the queue drains straight into `build.review`. There is no coder-identity\n # follow-through on review feedback here any more — an actionable round\n # leaves this machine entirely, via `$onFeedback` bound at the root's own\n # `build:` reference to the root-local `re-unwind` state (see the header\n # comment's \"Why humanReview is nested\" section for why the review tail\n # still stays nested here even so). `humanReview` is nested HERE (not at the\n # root) so `build.squashing` — the one coder turn a sign-off still resumes —\n # stays inside the builder's own scope.\n buildTail:\n model: <%= it.vars.coderModel %>\n system: <%~ it.vars.finisherPersona %>\n params: [onDone, onFeedback]\n # Nothing resolves this machine BARE (every reference into it names a\n # specific descendant, e.g. `build.review` / `build.fix`) — but a machine\n # must still declare an `entry:` local per its own grammar, so this names\n # an arbitrary one of its own states.\n entry: fix\n states:\n squashing:\n actor: agent\n label: Squashing\n file: COMMIT_MSG.md\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are an autonomous coding agent. The process is approved and done. This\n workflow steers itself through its own state files — treat them as its\n private scratchpad, never as project code or documentation. The only\n state file this turn touches is `.gtd/COMMIT_MSG.md`; write\n it. Do not create other files to hold notes or output.\n\n Leave `.gtd/COMMIT_MSG.md` uncommitted and finish your turn —\n entering this file squashes everything this process retains into one\n commit using its content as the message. This prompt carries no diff —\n if you built that range yourself earlier in this conversation, work from\n what you already know; otherwise read the range yourself. It runs from\n `<%= it.retainedBase %>` to the working tree, and is exactly what that\n commit will contain, the changes THIS process made (its trace/retry\n boundary onward). For a normal build\n process that is the whole feature; for a `gtd --entry review-gate.check`\n process it is only the fixes made during the review — NOT the changeset\n that was under review.\n\n If that range is non-empty, write `.gtd/COMMIT_MSG.md` with ONE\n conventional-commits message for the changes (subject line\n `type(scope): subject`, imperative mood, <= 72 characters; a body\n explaining the why, trade-offs, and key decisions). Plain text, no\n markdown wrapper.\n\n If that range is empty (a clean review sign-off with no fixes), write\n `.gtd/COMMIT_MSG.md` with exactly this one line and nothing\n else:\n\n chore: human review\n on:\n \"A .gtd/COMMIT_MSG.md\": $onDone\n \"M .gtd/COMMIT_MSG.md\": $onDone\n\n fix:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `.gtd/FEEDBACK.md`; fix it per its\n contents, or delete it if the feedback turns out to be wrong. Do not\n create other files to hold notes or output.\n\n Read `.gtd/FEEDBACK.md` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `.gtd/FEEDBACK.md` instead of \"fixing\" a non-issue.\n\n Leave everything uncommitted and finish your turn — do not commit.\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n # Shared health pair: build.health.check/.escalate -> build.review.reviewing\n # (green) or build.fix (red).\n health:\n machine: healthGate\n with:\n onGreen: review\n onRed: fix\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**build.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # The shared review tail, nested HERE (not at the root) so `squashing`\n # stays inside the builder's own scope on sign-off — see the header\n # comment. Sign-off -> squashing; feedback -> $onFeedback, bound at the\n # root's own `build:` reference to `re-unwind` (a root-local, not a\n # descendant of this machine — the review tail's own scope ends here on\n # the actionable path).\n review:\n machine: humanReview\n with:\n onSignoff: squashing\n onFeedback: $onFeedback\n\n # The root machine — the whole one-flow tree described in this file's\n # header comment. WIRING ONLY: no identity of its own, just the shared\n # entry/exit states and the top-level machine tree.\n unified:\n entry: idle\n states:\n # ── Shared entry ────────────────────────────────────────────────────────\n idle:\n actor: human\n label: Idle\n file: TODO.md\n # Every human-gate `message:` below closes with the same rendered \"What\n # each change does next\" list, built from `it.edges` (the resting state's\n # own `on` edges — each edge's `describe:` is the single source of truth).\n message: |\n No active gtd process.\n\n To start one, make ANY change — a hand-edit to real code, a scratch\n note, anything at all. .gtd/TODO.md is a good default\n place to start sketching. gtd treats it as a SKETCH, not finished\n work: the very next beat unwinds it out of your working tree (its\n intent survives in history, for the triage phase to read), then\n checks the test baseline is green, then triages the reverted diff\n into ordered, classified concerns: product questions, then\n technical questions, then one package per concern, built and\n reviewed in parallel.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: unwind\n action: Start\n describe: >-\n make any change — gtd unwinds it out of your working tree (its\n intent survives in history) before checking the test baseline is\n green, then triages the reverted diff into ordered, classified\n concerns and resolves product questions, then technical ones,\n before building each concern's package (**unwind** ->\n **start-gate.check**).\n\n # ── Green-baseline gate: `gtd --entry fix-precheck` entry ─────────────────\n # `gtd --entry fix-precheck` enters HERE: run the suite, then a red run\n # drops straight into the shared `build.fix` loop (-> build.health.check ->\n # review + squash tail); a green run is a no-op back to `idle` (nothing\n # to fix). Aliases the `&suiteCheck` anchor defined on `entryGate.check`'s\n # own `script:` above — the same plain suite check `start-gate`/\n # `review-gate` run.\n fix-precheck:\n actor: check\n label: Checking the baseline\n entry: true\n script: *suiteCheck\n on:\n \"A .gtd/FEEDBACK.md\": build.fix\n \"M .gtd/FEEDBACK.md\": build.fix\n \"D .gtd/REVIEW_RAW.md\": idle\n \"D .gtd/FEEDBACK.md\": idle\n \"C\": idle\n\n # ── Shared tail: the finale ─────────────────────────────────────────────\n done:\n commit: |\n <%~ it.read(\".gtd/COMMIT_MSG.md\") %>\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n\n # ── Unwind: revert the entry commit's diff before planning starts ──────\n # Kept a SEPARATE state rather than folded into `start-gate.check`'s own\n # script: the `blocked -> check` retry edge re-runs the gate, so a\n # revert living there would discard the human's own baseline repair and\n # would need an idempotence guard. With exactly one inbound edge (from\n # `idle`), `unwind` is structurally reached once per process and needs\n # no guard at all. Names the reverted commit via `it.currentCommit`\n # (they're the same commit here), NOT a bare `HEAD` — pinning the\n # render-time hash keeps the script correct even if a driver runs it\n # late.\n unwind:\n actor: check\n label: Unwinding your input\n script: |\n #!/usr/bin/env sh\n # gtd check turn — revert the entry commit's diff out of the working\n # tree. Its content survives in history for design.triage to read.\n set +e\n # Hoist the commit hash once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last\n # token on a line.\n commit=\"<%~ it.currentCommit %>\"\n git revert --no-commit \"$commit\"\n on:\n \"* **\": start-gate\n\n # ── Re-unwind: revert an actionable review round's hand-edit before ────\n # re-planning starts. A SECOND unwind-shaped state, not a second inbound\n # edge on `unwind` above: `unwind`'s own doc comment turns on it having\n # exactly one inbound edge (from `idle`), which is why it carries no\n # idempotence guard — adding a second caller there would break that\n # invariant. Names the reverted commit via `it.reviewBase`, never\n # `it.currentCommit` (by now that names the assembly turn's own commit)\n # and never a bare `HEAD`: `reviewBaseFor` walks the process trace\n # backwards for the last commit that ENTERED a `reviewBase: true` state\n # — `build.review.deciding` is the template's only one — which is\n # exactly the human's own `await-review -> deciding` commit, the same\n # hash the next review round's incremental base already uses.\n #\n # SCOPED, not wholesale: `deciding` already `rm -f`'d the review file,\n # so a plain `git revert --no-commit` would resurrect it (and conflict\n # on it). Instead this renders that ONE commit's diff with `.gtd/`\n # excluded from the pathspec (REVIEW.md lives there too) and\n # reverse-applies it. The apply is EXPECTED to succeed —\n # nothing between that commit and now touches a code path — but it can\n # still FAIL: a clean-filter round-trip, a drifted worktree, or a binary\n # path can all make `git apply -R` a no-op on an otherwise-clean tree,\n # indistinguishable from a legitimate note-only round. `requireRevert`\n # (below) is what catches that: its step-capture guard\n # (`src/StepGuards.ts`) re-establishes the fact from the tree itself —\n # comparing the human's own commit's paths against the review base's\n # parent — and refuses the step while residue remains, rather than\n # trusting the script's swallowed exit code. A note-only round's patch\n # is EMPTY, and `git apply` on empty input FAILS (\"No valid patches in\n # input\"), hence the `-s` guard rather than `--allow-empty` (which would\n # impose git >= 2.35). The apply is worktree-only, no `--index`: the\n # emitted step script's own `git add -A && git commit` stages it, and\n # `changedPaths` answers by CONTENT, so an unstaged revert is still the\n # pending diff the pattern matches — one fewer index write in a\n # worktree gtd shares with the editor's SCM. The patch file lives under\n # `.gtd/` (excluded from its own pathspec, the same precedent as the\n # check scripts' own `.check-output`) and is removed before the step\n # lands.\n #\n # Both `on` rows target the SAME place (`design`) because either shape\n # is a legitimate outcome here, never a stall: a hand-edited round's\n # patch is non-empty, so the revert leaves a dirty tree (the `\"* **\"`\n # row); a note-only round's patch is empty, so nothing to apply leaves\n # a CLEAN tree — and a clean tree at a `script` state with no `C` row\n # would otherwise be a silent no-op that never advances. Declaring `C`\n # here makes that the expected, terminating case instead.\n #\n # `file: REVIEW.md` + `requireRevert: true` exist ONLY\n # to give the require-revert guard the path it must exempt from residue\n # (the engine blesses no variable names, so the guard reads `Rest.hints.\n # file`, never this literal itself) — the review file is already\n # gone two commits earlier, so this state never renders it. The accepted\n # presentational cost: `gtd next --json`/`gtd land --json` carry that\n # already-deleted path as this state's `file:` hint, and the visualizer\n # labels `re-unwind` with it too. Bounded — `Lsp.ts`'s `buildSteeringMap`\n # skips a mode-less state, so `re-unwind` contributes no path→mode entry\n # and raises no \"already mapped by an earlier state\" warning against\n # `await-review`/`deciding`, which declare the same rendered path with\n # `mode: review`.\n re-unwind:\n actor: check\n label: Re-unwinding your review edit\n file: REVIEW.md\n requireRevert: true\n script: |\n #!/usr/bin/env sh\n # gtd check turn — revert the human's own review edit out of the\n # working tree, scoped to real code: everything under `.gtd/` is\n # excluded, because the review file is already gone by this point\n # and reverting it would resurrect it. The human's intent survives\n # in their own commit for the triage phase to read. The apply is\n # EXPECTED to succeed — nothing between that commit and now touches\n # a code path — but it can still fail on a clean-filter round-trip,\n # a drifted worktree, or a binary path: the `requireRevert`\n # step-capture guard (src/StepGuards.ts) catches that by\n # re-checking the tree itself before this step is allowed to land.\n # The guard's own residue scoping (`isCodePath`) re-derives the same\n # `.gtd/` exemption this pathspec renders — keep the two together\n # when either changes.\n set +e\n # Hoist the reverted commit once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token\n # on a line.\n commit=\"<%~ it.reviewBase %>\"\n patch=.gtd/.re-unwind.patch\n mkdir -p .gtd\n git diff --binary \"$commit^\" \"$commit\" -- . \":(exclude).gtd\" > \"$patch\"\n if [ -s \"$patch\" ]; then\n git apply -R \"$patch\" || echo \"re-unwind: could not revert $commit\" >&2\n fi\n rm -f \"$patch\"\n on:\n \"* **\": design # a code hand-edit was reverted\n \"C\": design # note-only round: nothing to revert\n\n # ── Green-baseline gate: the single start entry. ───────────────────────\n # Runs on the tree AFTER `unwind`, so this is now just the ordinary\n # green-baseline check `review-gate`/`fix-precheck` also run — no\n # baseline-aware second run, no detached checkout, no HEAD move\n # anywhere in gtd's emitted scripts any more.\n start-gate:\n machine: entryGate\n with:\n onGreen: design\n reviewBase: \"\"\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `.gtd/FEEDBACK.md` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**start-gate.check**). To repair the baseline as its own separate\n reviewed commit instead, abandon this start and run\n `gtd --entry fix-precheck` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── Green-baseline gate: `gtd --entry review-gate.check` entry -> build.review.reviewing. ─\n review-gate:\n machine: entryGate\n with:\n onGreen: build.review\n reviewBase: <%= it.vars.reviewBase %>\n blockedMessage: |\n The test baseline is red — gtd will not start a review on a broken suite.\n `.gtd/FEEDBACK.md` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**review-gate.check**).\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── The triage phase (design.triage -> design.gate.check/.answer -> architecture). ──\n design:\n machine: designPlan\n with:\n onArchitecture: architecture\n\n # ── The technical phase (architecture.author -> architecture.gate.check/.answer ─\n # -> architecture.decompose -> packages). ──────────────────────────────\n architecture:\n machine: archPlan\n with:\n onPackages: packages\n\n # ── The per-package build queue (packages.* -> build.review). ─────────────\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n # ── The process's build identity (build.* -> build.review -> done). ───────\n build:\n machine: buildTail\n with:\n onDone: done\n onFeedback: re-unwind\n";
|
|
192416
192522
|
//#endregion
|
|
192417
192523
|
//#region src/workflows/templates.ts
|
|
192418
192524
|
/**
|
|
@@ -192602,6 +192708,7 @@ const modesJsonSchema = {
|
|
|
192602
192708
|
const JSON_TYPE = {
|
|
192603
192709
|
actor: { type: "string" },
|
|
192604
192710
|
text: { type: "string" },
|
|
192711
|
+
stateFile: { type: "string" },
|
|
192605
192712
|
mode: { type: "string" },
|
|
192606
192713
|
content: { type: "string" },
|
|
192607
192714
|
flag: { type: "boolean" },
|
|
@@ -192609,19 +192716,42 @@ const JSON_TYPE = {
|
|
|
192609
192716
|
edges: { type: "object" },
|
|
192610
192717
|
retry: { type: "object" }
|
|
192611
192718
|
};
|
|
192719
|
+
/** One state's shape — `properties` is derived from `STATE_FIELD_ENTRIES` (every `authored: "state"` field, in table order); see `ConfigSchema.test.ts`. */
|
|
192720
|
+
const stateJsonSchema = {
|
|
192721
|
+
type: "object",
|
|
192722
|
+
description: "One workflow state. Declare exactly one content kind (script/prompt/message/commit). A commit state is final: no actor, no on.",
|
|
192723
|
+
additionalProperties: false,
|
|
192724
|
+
properties: Object.fromEntries(STATE_FIELD_ENTRIES.filter(([, spec]) => spec.authored === "state").map(([key, spec]) => [key, spec.jsonSchema ?? {
|
|
192725
|
+
...JSON_TYPE[spec.kind],
|
|
192726
|
+
description: spec.doc
|
|
192727
|
+
}]))
|
|
192728
|
+
};
|
|
192729
|
+
/** A reference local: instantiates a declared machine as a child, optionally binding its `params:` — mirrors `Machines.ts`'s `isRef`. */
|
|
192730
|
+
const machineRefJsonSchema = {
|
|
192731
|
+
type: "object",
|
|
192732
|
+
description: "A reference: instantiates the named machine as a child, at this local's path (see src/Machines.ts). Expanded at load time into concrete, qualified states — the engine only ever sees the flattened result.",
|
|
192733
|
+
additionalProperties: false,
|
|
192734
|
+
required: ["machine"],
|
|
192735
|
+
properties: {
|
|
192736
|
+
machine: {
|
|
192737
|
+
type: "string",
|
|
192738
|
+
description: "Name of a declared entry in `machines:`."
|
|
192739
|
+
},
|
|
192740
|
+
with: {
|
|
192741
|
+
type: "object",
|
|
192742
|
+
description: "Bindings for the referenced machine's `params:`. A bound value naming another of the CALLER's own bindings (a whole-value `$name`) passes it down verbatim, scope intact."
|
|
192743
|
+
}
|
|
192744
|
+
}
|
|
192745
|
+
};
|
|
192612
192746
|
/** The whole `workflow:` value — see `PatternConfig.ts`'s module docstring for the authoritative schema. */
|
|
192613
192747
|
const workflowJsonSchema = {
|
|
192614
192748
|
type: "object",
|
|
192615
|
-
description: "The whole machine definition: a tree of named machines rooted at entry.default (plus the workflow's own vars: defaults
|
|
192749
|
+
description: "The whole machine definition: a tree of named machines rooted at entry.default (plus the workflow's own vars: defaults and modes: steering-file modes). Compiled and validated by gtd at load time; content strings starting with ./ or ../ are file references inlined from the config file's directory (a modes: command never is — it is a shell command).",
|
|
192616
192750
|
additionalProperties: false,
|
|
192617
192751
|
required: ["entry", "machines"],
|
|
192618
192752
|
properties: {
|
|
192619
192753
|
vars: varsJsonSchema,
|
|
192620
192754
|
modes: modesJsonSchema,
|
|
192621
|
-
stateDir: {
|
|
192622
|
-
type: "string",
|
|
192623
|
-
description: "An Eta template naming where this workflow keeps its own scratch/bookkeeping directory (the review window's revert pathspec, the step guards' code-vs-plumbing test). Defaults to \".gtd\" when absent. The rendered value must name a directory inside the repository: not the repo root, not an absolute path, not a path that escapes the repo via `..`. It must also be spelled canonically — no `.` and no empty path segments (e.g. `a/./state` or `a//state` are rejected, not rewritten) — though a leading `./` and a trailing `/` are accepted. Distinct from the ordinary `vars.stateDir` workflow var of the same name one level down — this is the definition-level declaration the engine reads; the var is the knob a user overrides, which the bundled template renders this declaration from."
|
|
192624
|
-
},
|
|
192625
192755
|
entry: {
|
|
192626
192756
|
type: "object",
|
|
192627
192757
|
description: "The workflow's root machine, resolved through the same resolver an `on`/`retry.otherwise` target uses (see src/Machines.ts) — accepts either a bare state path or an instance/reference-key path. Extra manual entry points are declared per-state instead, via a state's own `entry: true` (see `stateJsonSchema`'s `entry` property) — not here.",
|
|
@@ -192651,38 +192781,15 @@ const workflowJsonSchema = {
|
|
|
192651
192781
|
type: "string",
|
|
192652
192782
|
description: "This machine's own default local (a local state name, or a reference key), resolved recursively."
|
|
192653
192783
|
},
|
|
192654
|
-
|
|
192655
|
-
|
|
192656
|
-
description:
|
|
192657
|
-
},
|
|
192784
|
+
...Object.fromEntries(MACHINE_FIELD_ENTRIES.map(([key, spec]) => [key, spec.jsonSchema ?? {
|
|
192785
|
+
...JSON_TYPE[spec.kind],
|
|
192786
|
+
description: spec.doc
|
|
192787
|
+
}])),
|
|
192658
192788
|
states: {
|
|
192659
192789
|
type: "object",
|
|
192660
192790
|
description: "This machine's local states, each either an ordinary state or a `{ machine, with }` reference.",
|
|
192661
192791
|
minProperties: 1,
|
|
192662
|
-
additionalProperties: { oneOf: [
|
|
192663
|
-
type: "object",
|
|
192664
|
-
description: "One workflow state. Declare exactly one content kind (script/prompt/message/commit). A commit state is final: no actor, no on.",
|
|
192665
|
-
additionalProperties: false,
|
|
192666
|
-
properties: Object.fromEntries(STATE_FIELD_ENTRIES.filter(([, spec]) => spec.authored === "state").map(([key, spec]) => [key, spec.jsonSchema ?? {
|
|
192667
|
-
...JSON_TYPE[spec.kind],
|
|
192668
|
-
description: spec.doc
|
|
192669
|
-
}]))
|
|
192670
|
-
}, {
|
|
192671
|
-
type: "object",
|
|
192672
|
-
description: "A reference: instantiates the named machine as a child, at this local's path (see src/Machines.ts). Expanded at load time into concrete, qualified states — the engine only ever sees the flattened result.",
|
|
192673
|
-
additionalProperties: false,
|
|
192674
|
-
required: ["machine"],
|
|
192675
|
-
properties: {
|
|
192676
|
-
machine: {
|
|
192677
|
-
type: "string",
|
|
192678
|
-
description: "Name of a declared entry in `machines:`."
|
|
192679
|
-
},
|
|
192680
|
-
with: {
|
|
192681
|
-
type: "object",
|
|
192682
|
-
description: "Bindings for the referenced machine's `params:`. A bound value naming another of the CALLER's own bindings (a whole-value `$name`) passes it down verbatim, scope intact."
|
|
192683
|
-
}
|
|
192684
|
-
}
|
|
192685
|
-
}] }
|
|
192792
|
+
additionalProperties: { oneOf: [stateJsonSchema, machineRefJsonSchema] }
|
|
192686
192793
|
}
|
|
192687
192794
|
}
|
|
192688
192795
|
}
|
|
@@ -193864,13 +193971,10 @@ var Eta = class extends Eta$1 {
|
|
|
193864
193971
|
* The stub `TemplateContext` an `on` pattern key renders against at the edge
|
|
193865
193972
|
* (see `Edge.ts`'s `renderOnEdges`): only `vars` (and, optionally, `state`)
|
|
193866
193973
|
* are real — every commit-ish/diff field is empty, `processCost` is `0`,
|
|
193867
|
-
* `edges` is empty,
|
|
193868
|
-
*
|
|
193869
|
-
*
|
|
193870
|
-
*
|
|
193871
|
-
* context `src/Edge.ts` renders the `stateDir:` declaration itself against —
|
|
193872
|
-
* an empty `stateDir` there is what makes a declaration that references
|
|
193873
|
-
* `it.stateDir` render empty rather than recurse.
|
|
193974
|
+
* `edges` is empty, and `read` throws, since a pattern names a path and never
|
|
193975
|
+
* legitimately needs a working tree or git history to resolve. This is also
|
|
193976
|
+
* `Lsp.ts`'s `buildFileModeMap` map-building context, factored out here to
|
|
193977
|
+
* avoid building the same stub in two places.
|
|
193874
193978
|
*/
|
|
193875
193979
|
const varsOnlyContext = (vars, state = "") => ({
|
|
193876
193980
|
startCommit: "",
|
|
@@ -193886,8 +193990,7 @@ const varsOnlyContext = (vars, state = "") => ({
|
|
|
193886
193990
|
throw new Error(`no working tree to read from while rendering against a vars-only context (path: ${path})`);
|
|
193887
193991
|
},
|
|
193888
193992
|
vars,
|
|
193889
|
-
edges: []
|
|
193890
|
-
stateDir: ""
|
|
193993
|
+
edges: []
|
|
193891
193994
|
});
|
|
193892
193995
|
const eta = new Eta();
|
|
193893
193996
|
eta.readFile = null;
|
|
@@ -194447,18 +194550,15 @@ const combinedScript = (required, optional) => {
|
|
|
194447
194550
|
* renamed-state refusal
|
|
194448
194551
|
* 4. `commitHistory()` → `ProcessRun`
|
|
194449
194552
|
* 5. `vars` — the four-layer merge (workflow < rc < entry commit < env)
|
|
194450
|
-
* 6. `
|
|
194451
|
-
* rendered against `vars` ONLY (Eta template; `vars` must precede this,
|
|
194452
|
-
* same ordering problem `on`'s render below has)
|
|
194453
|
-
* 7. `on` — the resting state's `on` edges, rendered against `vars` (Eta
|
|
194553
|
+
* 6. `on` — the resting state's `on` edges, rendered against `vars` (Eta
|
|
194454
194554
|
* templates; `vars` must precede this)
|
|
194455
|
-
*
|
|
194555
|
+
* 7. `stepDef` — `def` with `on` patched onto the resting state, the shape
|
|
194456
194556
|
* `PatternMachine.step` must be fed
|
|
194457
|
-
*
|
|
194458
|
-
*
|
|
194459
|
-
*
|
|
194460
|
-
*
|
|
194461
|
-
*
|
|
194557
|
+
* 8. `reviewBase` — the pure `reviewBaseFor(def, run)`
|
|
194558
|
+
* 9. `changes` — `changedPaths()`
|
|
194559
|
+
* 10. `context` — the full `TemplateContext` (`on` and `reviewBase` both feed
|
|
194560
|
+
* it — `it.edges` derives from `on`)
|
|
194561
|
+
* 11. `memory`, then `hints` (`model`/`label`/`file` rendered against
|
|
194462
194562
|
* `context`; `mode` passed through verbatim)
|
|
194463
194563
|
*
|
|
194464
194564
|
* Two module rules, enforced on this file itself: a private helper with only
|
|
@@ -194867,41 +194967,6 @@ const renderOnEdgesOrFail = (onEdges, vars) => try_({
|
|
|
194867
194967
|
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
194868
194968
|
});
|
|
194869
194969
|
/**
|
|
194870
|
-
* Render `def`'s declared plumbing directory (`PatternMachine.stateDirOf`)
|
|
194871
|
-
* against `vars` ONLY (`varsOnlyContext`) — same restriction `renderOnEdges`
|
|
194872
|
-
* uses, which is what breaks the circularity of a context field whose own
|
|
194873
|
-
* value comes from a template rendered against that context: a declaration
|
|
194874
|
-
* that references `it.stateDir` itself sees the empty stub there and renders
|
|
194875
|
-
* empty rather than recursing. Stripped ONCE here
|
|
194876
|
-
* (`PatternMachine.canonicalStateDir`) — tolerance for a leading `./` and a
|
|
194877
|
-
* trailing `/`, NOT full canonicalization; it does not make every downstream
|
|
194878
|
-
* reader see a canonical string on its own. `renderStateDirOrFail` is what
|
|
194879
|
-
* makes that guarantee true, by refusing anything the strip didn't already
|
|
194880
|
-
* fix. Throws whatever Eta throws on a malformed template; the caller turns
|
|
194881
|
-
* that into a step refusal / command error, exactly like a content render
|
|
194882
|
-
* failure.
|
|
194883
|
-
*/
|
|
194884
|
-
const renderStateDir = (def, vars) => canonicalStateDir(renderStateTemplate(stateDirOf(def), varsOnlyContext(vars)));
|
|
194885
|
-
/**
|
|
194886
|
-
* `renderStateDir`, surfacing a malformed template as a plain `Error` (see
|
|
194887
|
-
* `renderOnEdgesOrFail`), THEN running `PatternMachine.stateDirError` on the
|
|
194888
|
-
* rendered, stripped result and failing with its message when it fires. This
|
|
194889
|
-
* is the one ENFORCEMENT site for the value rule (see
|
|
194890
|
-
* `PatternMachine.stateDirError`'s doc comment for why load time can't own
|
|
194891
|
-
* it): a non-canonical spelling that survives `renderStateDir`'s tolerance
|
|
194892
|
-
* strip is a config error naming the canonical form, never something this
|
|
194893
|
-
* function or any downstream consumer repairs — every caller of
|
|
194894
|
-
* `currentRest`/`restAt` (`gtd next`/`status`/`land` alike) sees the same
|
|
194895
|
-
* failure before the value reaches any consumer.
|
|
194896
|
-
*/
|
|
194897
|
-
const renderStateDirOrFail = (def, vars) => try_({
|
|
194898
|
-
try: () => renderStateDir(def, vars),
|
|
194899
|
-
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
194900
|
-
}).pipe(flatMap$4((stateDir) => {
|
|
194901
|
-
const error = stateDirError(stateDir);
|
|
194902
|
-
return error === void 0 ? succeed$3(stateDir) : fail$2(new Error(error));
|
|
194903
|
-
}));
|
|
194904
|
-
/**
|
|
194905
194970
|
* A shallow clone of `def` whose `state`'s `on` is replaced by
|
|
194906
194971
|
* `renderedOnEdges` — used to feed `PatternMachine.step`, which matches only
|
|
194907
194972
|
* `def.states[state].on` for the state it's invoked at. Only the RESTING
|
|
@@ -194930,11 +194995,9 @@ const withRenderedOn = (def, state, renderedOnEdges) => ({
|
|
|
194930
194995
|
* breakdown including the squashing step) — `0`/absent for the pure emitters
|
|
194931
194996
|
* (`gtd next`/`gtd status`), where no step is being performed. No diff is ever
|
|
194932
194997
|
* computed here — `it.reviewBase`/`it.retainedBase` are bases a template tells
|
|
194933
|
-
* the agent to `git diff` itself.
|
|
194934
|
-
* normalized by the caller (`renderStateDir`) — this function only carries it
|
|
194935
|
-
* onto `it.stateDir`, same discipline as `edges`.
|
|
194998
|
+
* the agent to `git diff` itself.
|
|
194936
194999
|
*/
|
|
194937
|
-
const buildTemplateContext = (git, read, state, actor, run, vars, edges, currentCost, currentModel, reviewBase
|
|
195000
|
+
const buildTemplateContext = (git, read, state, actor, run, vars, edges, currentCost, currentModel, reviewBase) => gen(function* () {
|
|
194938
195001
|
const currentCommit = yield* git.resolveRef("HEAD");
|
|
194939
195002
|
const previousCommit = yield* git.resolveRef("HEAD~1").pipe(catchAll(() => succeed$3(run.startParentHash)));
|
|
194940
195003
|
const stepEntry = currentCost > 0 || currentModel !== void 0 ? [{
|
|
@@ -194954,8 +195017,7 @@ const buildTemplateContext = (git, read, state, actor, run, vars, edges, current
|
|
|
194954
195017
|
processCostByModel: costByModel(allCostEntries),
|
|
194955
195018
|
read,
|
|
194956
195019
|
vars,
|
|
194957
|
-
edges: toTemplateEdges(edges)
|
|
194958
|
-
stateDir
|
|
195020
|
+
edges: toTemplateEdges(edges)
|
|
194959
195021
|
};
|
|
194960
195022
|
});
|
|
194961
195023
|
/**
|
|
@@ -195004,12 +195066,11 @@ const restAt = (ref) => gen(function* () {
|
|
|
195004
195066
|
yield* (yield* Narrator).narrate(`rest resolved: ${resolved.state} (awaits ${resolved.actor})`);
|
|
195005
195067
|
const run = yield* computeProcessRun(git, def, windowHead);
|
|
195006
195068
|
const vars = resolveVars(config.workflowVars, config.rcVars, run.entryVars, envVars.all);
|
|
195007
|
-
const stateDir = yield* renderStateDirOrFail(def, vars);
|
|
195008
195069
|
const on = yield* renderOnEdgesOrFail(resolved.stateDef.on, vars);
|
|
195009
195070
|
const stepDef = withRenderedOn(def, resolved.state, on);
|
|
195010
195071
|
const reviewBase = reviewBaseFor(def, run);
|
|
195011
195072
|
const changes = yield* pendingChanges(git, windowHead);
|
|
195012
|
-
const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase
|
|
195073
|
+
const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase);
|
|
195013
195074
|
const memory = memoryKeyFor(config.stateScopes, resolved, run);
|
|
195014
195075
|
const memoryResumed = memoryResumedFor(def, config.stateScopes, resolved, run);
|
|
195015
195076
|
const hints = yield* renderHints(resolved.stateDef, context);
|
|
@@ -195121,16 +195182,14 @@ const formatStepRefusal = (refusal) => refusal.reason === "out-of-turn" ? `gtd l
|
|
|
195121
195182
|
* `TemplateContext` at all), and it is NOT interchangeable with `rest.context`:
|
|
195122
195183
|
* that one is pinned to the resting state and carries `cost: 0`, so rendering
|
|
195123
195184
|
* `it.processCost` against it silently omits the squashing step's own
|
|
195124
|
-
* `--cost`. `
|
|
195125
|
-
* by target state — reused verbatim from `rest.context.stateDir` rather than
|
|
195126
|
-
* re-rendered. Exported for `program.ts`'s `planLanding`, which assembles the
|
|
195185
|
+
* `--cost`. Exported for `program.ts`'s `planLanding`, which assembles the
|
|
195127
195186
|
* same script by hand and must pick the same context.
|
|
195128
195187
|
*/
|
|
195129
195188
|
const contextAt = (rest, targetState, invoker, cost, model) => gen(function* () {
|
|
195130
195189
|
const git = yield* GitService;
|
|
195131
195190
|
const files = yield* RepoFiles;
|
|
195132
195191
|
const onEdges = targetState === rest.state ? rest.on : yield* renderOnEdgesOrFail(rest.def.states[targetState]?.on, rest.vars);
|
|
195133
|
-
return yield* buildTemplateContext(git, templateRead(files), targetState, invoker, rest.run, rest.vars, onEdges, cost ?? 0, model, reviewBaseFor(rest.def, rest.run)
|
|
195192
|
+
return yield* buildTemplateContext(git, templateRead(files), targetState, invoker, rest.run, rest.vars, onEdges, cost ?? 0, model, reviewBaseFor(rest.def, rest.run));
|
|
195134
195193
|
});
|
|
195135
195194
|
/**
|
|
195136
195195
|
* Render a `"commit"`/`"squash"` decision as the `EmitStep`s the external
|
|
@@ -195600,16 +195659,14 @@ const decideOpenWindow = (def, target, run, head) => {
|
|
|
195600
195659
|
* same target is a no-op, and `restoreStagedFrom` re-pinning an already-pinned
|
|
195601
195660
|
* path is a no-op.
|
|
195602
195661
|
*
|
|
195603
|
-
* `".gtd"` stays a LITERAL, deliberately, rather than being derived from
|
|
195604
|
-
*
|
|
195605
|
-
*
|
|
195606
|
-
*
|
|
195607
|
-
*
|
|
195608
|
-
*
|
|
195609
|
-
*
|
|
195610
|
-
*
|
|
195611
|
-
* not a pending change and never a phantom deletion. Making the list dynamic
|
|
195612
|
-
* would also cost the in-memory tier its trust property — the recognizer
|
|
195662
|
+
* `".gtd"` stays a LITERAL, deliberately, rather than being derived from
|
|
195663
|
+
* states' own `file:` declarations (every one of which is compiled under
|
|
195664
|
+
* `.gtd/` anyway — see `PatternConfig.ts`'s `stateFile` compiler). The pin
|
|
195665
|
+
* exists to keep gtd's PLUMBING out of the reviewer's editor, and `.gtd/` is
|
|
195666
|
+
* the conventional directory it lives in — including the parts no state
|
|
195667
|
+
* declares (a check script's temp output), which a `file:`-derived list would
|
|
195668
|
+
* miss. Making the list dynamic would also cost the in-memory tier its trust
|
|
195669
|
+
* property — the recognizer
|
|
195613
195670
|
* (`src/testing/EmittedScriptRecognizer.ts`'s `recognizeReviewWindowOpen`)
|
|
195614
195671
|
* re-derives this exact string and compares, which only works while the paths
|
|
195615
195672
|
* are fixed. Don't trade that for cosmetics.
|
|
@@ -207230,7 +207287,7 @@ const flattenTree = (node, parent, depth, out) => {
|
|
|
207230
207287
|
* The `model` of one group — a machine instance's own `model:`, read off any
|
|
207231
207288
|
* ONE of that group's own prompt-content states (by construction every prompt
|
|
207232
207289
|
* state one machine instance owns carries the identical `def.model` — see
|
|
207233
|
-
* `Machines.ts`'s `
|
|
207290
|
+
* `Machines.ts`'s `resolveInstanceMachineFields`/`emitTree`). `undefined` when the
|
|
207234
207291
|
* group owns no prompt state (e.g. a queue/gate machine of only
|
|
207235
207292
|
* script/message/commit states) or its machine declares no `model:` — never
|
|
207236
207293
|
* guessed.
|
|
@@ -207415,23 +207472,22 @@ const openInBrowser = (url) => {
|
|
|
207415
207472
|
*/
|
|
207416
207473
|
const deletesFile = (changes, file) => changes.some((c) => c.path === file && c.status === "D");
|
|
207417
207474
|
/**
|
|
207418
|
-
* True when `path` sits inside
|
|
207419
|
-
*
|
|
207420
|
-
*
|
|
207421
|
-
*
|
|
207422
|
-
* `<dir>-backup/` next to the declared directory.
|
|
207475
|
+
* True when `path` sits inside gtd's plumbing directory (`STATE_DIR`, `.gtd`)
|
|
207476
|
+
* — an exact match on the bare directory path itself, or prefixed by
|
|
207477
|
+
* `STATE_DIR + "/"`. The exact-match-or-separator shape matters: a naive
|
|
207478
|
+
* `path.startsWith(STATE_DIR)` would also swallow a sibling `.gtd-backup/`.
|
|
207423
207479
|
*/
|
|
207424
|
-
const isPlumbingPath = (path
|
|
207480
|
+
const isPlumbingPath = (path) => path === ".gtd" || path.startsWith(`.gtd/`);
|
|
207425
207481
|
/**
|
|
207426
|
-
* True when `path` is a real code path — outside
|
|
207427
|
-
*
|
|
207428
|
-
* `
|
|
207429
|
-
*
|
|
207430
|
-
*
|
|
207482
|
+
* True when `path` is a real code path — outside gtd's plumbing directory.
|
|
207483
|
+
* Every state's `file:` is compiled under `STATE_DIR` too (`PatternConfig.ts`'s
|
|
207484
|
+
* `stateFile` compiler), so this plain negation already excludes a state's own
|
|
207485
|
+
* steering file with no separate exact-path exemption needed. The one "did a
|
|
207486
|
+
* human touch something real" predicate shared by `enforceStepGuards`'s own
|
|
207431
207487
|
* `hasCodeChange` and the require-revert guard's residue scoping, so the rule
|
|
207432
207488
|
* is declared exactly once.
|
|
207433
207489
|
*/
|
|
207434
|
-
const isCodePath = (path
|
|
207490
|
+
const isCodePath = (path) => !isPlumbingPath(path);
|
|
207435
207491
|
/** The `mode:` name of gtd's built-in REVIEW.md checkbox format — the only mode the review-doc guard understands. */
|
|
207436
207492
|
const REVIEW_MODE = "review";
|
|
207437
207493
|
const reviewDocGuard = {
|
|
@@ -207475,7 +207531,7 @@ const stepGuards = [
|
|
|
207475
207531
|
if (rb === "" || rb === ctx.template.startCommit) return `"${ctx.rest.state}" has no identifiable review round to check (reviewBase is unset) — the revert cannot be established.`;
|
|
207476
207532
|
const git = yield* GitService;
|
|
207477
207533
|
const base = `${rb}~1`;
|
|
207478
|
-
const scoped = ((yield* git.commitHistory(base, rb))[0]?.touched ?? []).filter((path) => isCodePath(path
|
|
207534
|
+
const scoped = ((yield* git.commitHistory(base, rb))[0]?.touched ?? []).filter((path) => isCodePath(path));
|
|
207479
207535
|
if (scoped.length === 0) return void 0;
|
|
207480
207536
|
const residue = (yield* git.changedPaths(base)).filter((c) => scoped.includes(c.path)).map((c) => c.path);
|
|
207481
207537
|
if (residue.length === 0) return void 0;
|
|
@@ -207499,7 +207555,7 @@ const enforceStepGuards = (input) => gen(function* () {
|
|
|
207499
207555
|
const applicable = stepGuards.filter((g) => g.appliesTo(input.rest));
|
|
207500
207556
|
if (applicable.length === 0) return;
|
|
207501
207557
|
const fileDeleted = deletesFile(input.changes, file);
|
|
207502
|
-
const hasCodeChange = input.changes.some((c) => isCodePath(c.path
|
|
207558
|
+
const hasCodeChange = input.changes.some((c) => isCodePath(c.path));
|
|
207503
207559
|
const base = {
|
|
207504
207560
|
rest: input.rest,
|
|
207505
207561
|
file,
|
|
@@ -207698,6 +207754,10 @@ const stallDiagnosis = (state, actor) => `stalled at "${state}": the last gtd(${
|
|
|
207698
207754
|
* always present, `true`/`false`, never omitted. `model`/`memory`/`file`/
|
|
207699
207755
|
* `mode`/`label`/`edges` are plain facts about the resting state and are
|
|
207700
207756
|
* emitted at EVERY kind — omitted (never `null`) when their source is unset.
|
|
207757
|
+
* `system` is the same, EXCEPT it is also omitted when its rendered value is
|
|
207758
|
+
* the empty string: unlike `model`, an empty system prompt reaching a driver
|
|
207759
|
+
* as `--system-prompt ""` silently deletes the harness's own default instead
|
|
207760
|
+
* of failing loudly, so a render-to-empty is treated as unset.
|
|
207701
207761
|
* `changes` and `next` are always present (the headline conclusion, never
|
|
207702
207762
|
* omit-vs-null — a `null` `next` means no declared pattern matches).
|
|
207703
207763
|
* `cost`/`costByModel` are emitted only when a cost has actually been
|
|
@@ -207716,6 +207776,7 @@ const beatFields = (input) => {
|
|
|
207716
207776
|
resume: session.resume
|
|
207717
207777
|
} } : {},
|
|
207718
207778
|
...rendered.model !== void 0 ? { model: rendered.model } : {},
|
|
207779
|
+
...rendered.system !== void 0 && rendered.system !== "" ? { system: rendered.system } : {},
|
|
207719
207780
|
...dispatchable && validate !== void 0 ? { validate } : {},
|
|
207720
207781
|
log,
|
|
207721
207782
|
state: rendered.state,
|
|
@@ -207743,6 +207804,7 @@ const BEAT_SH_SHAPE = {
|
|
|
207743
207804
|
resume: "bool"
|
|
207744
207805
|
},
|
|
207745
207806
|
model: "scalar",
|
|
207807
|
+
system: "scalar",
|
|
207746
207808
|
validate: "scalar",
|
|
207747
207809
|
log: "scalar",
|
|
207748
207810
|
state: "scalar",
|
|
@@ -207988,8 +208050,9 @@ while :; do
|
|
|
207988
208050
|
# macOS, and POSIX guarantees only 4 KB, ARG_MAX), and a diff crosses
|
|
207989
208051
|
# that far sooner than you'd expect.
|
|
207990
208052
|
agent_turn() { printf '%s' "$gtd_content" | claude -p "$1" "$gtd_session_id" \\
|
|
207991
|
-
\${gtd_model:+--model "$gtd_model"}
|
|
207992
|
-
|
|
208053
|
+
\${gtd_model:+--model "$gtd_model"} \\
|
|
208054
|
+
\${gtd_system:+--system-prompt "$gtd_system"} \\
|
|
208055
|
+
--dangerously-skip-permissions >>"$gtd_log" 2>&1; }
|
|
207993
208056
|
if [ "\${gtd_session_resume:-}" = true ]
|
|
207994
208057
|
then agent_turn --resume || agent_turn --session-id
|
|
207995
208058
|
else agent_turn --session-id || agent_turn --resume
|
|
@@ -207998,8 +208061,16 @@ while :; do
|
|
|
207998
208061
|
while [ -n "\${gtd_validate:-}" ] && ! fix="$(sh -c "$gtd_validate" 2>&1)"; do
|
|
207999
208062
|
n=$((n + 1)) && [ "$n" -gt 3 ] && { printf '%s\\n' "$fix" >&2; exit 1; }
|
|
208000
208063
|
# $fix IS the fix prompt, verbatim — piped for the same reason as
|
|
208001
|
-
# $gtd_content above
|
|
208064
|
+
# $gtd_content above. Whether \`claude --resume\` re-applies the
|
|
208065
|
+
# original session's model/system prompt is a harness detail gtd
|
|
208066
|
+
# cannot verify from outside, so this passes the identical
|
|
208067
|
+
# $gtd_model/$gtd_system on both calls rather than assume it does —
|
|
208068
|
+
# otherwise this fix turn might silently fall back to Claude Code's
|
|
208069
|
+
# own defaults while the turn that produced the file ran under the
|
|
208070
|
+
# workflow's own model and persona.
|
|
208002
208071
|
printf '%s' "$fix" | claude -p --resume "$gtd_session_id" \\
|
|
208072
|
+
\${gtd_model:+--model "$gtd_model"} \\
|
|
208073
|
+
\${gtd_system:+--system-prompt "$gtd_system"} \\
|
|
208003
208074
|
--dangerously-skip-permissions >>"$gtd_log" 2>&1
|
|
208004
208075
|
done ;;
|
|
208005
208076
|
esac
|