@pmelab/gtd 8.5.0 → 9.0.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.
Files changed (4) hide show
  1. package/README.md +468 -150
  2. package/dist/gtd.bundle.mjs +1989 -515
  3. package/package.json +2 -3
  4. package/bin/gtd +0 -715
@@ -4,12 +4,13 @@ const require = __createRequire(import.meta.url);
4
4
  import { createRequire } from "node:module";
5
5
  import { createServer } from "node:http";
6
6
  import * as Crypto from "node:crypto";
7
+ import { createHash, randomUUID } from "node:crypto";
7
8
  import * as NFS from "node:fs";
8
9
  import { existsSync, readFileSync } from "node:fs";
9
10
  import * as OS from "node:os";
10
11
  import { homedir } from "node:os";
11
12
  import * as Path from "node:path";
12
- import { basename, dirname, join, resolve } from "node:path";
13
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
13
14
  import * as ChildProcess from "node:child_process";
14
15
  import { spawn } from "node:child_process";
15
16
  import * as NodeUrl from "node:url";
@@ -189824,7 +189825,7 @@ const STATE_FIELDS = {
189824
189825
  * A STRING is a different shape entirely: an Eta template rendering a
189825
189826
  * commitish. Entering that state fixes the WHOLE PROCESS's diff base to the
189826
189827
  * rendered value (not a window anchor) — this is how a manual entry (e.g.
189827
- * `gtd step <actor> --entry review --base <commitish>`) pins what the rest
189828
+ * `gtd --entry review --base <commitish>`) pins what the rest
189828
189829
  * of the process diffs against. Rendering the template happens at the edge,
189829
189830
  * not here — this module only carries the raw string and, per
189830
189831
  * `isReviewBaseState`, a string value is NEVER treated as the `true`/
@@ -189884,7 +189885,7 @@ const STATE_FIELDS = {
189884
189885
  },
189885
189886
  /**
189886
189887
  * Authoring-only: an EXTRA reachability root (`WorkflowEntries.manual`),
189887
- * enterable via `gtd step <actor> --entry <this state's qualified name>`.
189888
+ * enterable via `gtd --entry <this state's qualified name>`.
189888
189889
  * Never lands on the compiled `StateDef` — `compileState` validates its
189889
189890
  * shape but discards the result; `compileWorkflowConfig` reads the RAW
189890
189891
  * `entry: true` flag directly off each qualified state to build
@@ -189928,8 +189929,8 @@ const validateFieldRules = (name, state, key, spec) => {
189928
189929
  //#endregion
189929
189930
  //#region src/PatternMachine.ts
189930
189931
  /**
189931
- * The v3 "pattern machine": gtd's ground-up rewrite of the state-machine
189932
- * core. This module is the pure engine: definition types, the pattern grammar's
189932
+ * The pattern machine — gtd's state-machine core. This module is the pure
189933
+ * engine: definition types, the pattern grammar's
189933
189934
  * parser/matcher, HEAD resolution, and step decisions (refusals, no-ops,
189934
189935
  * commits, retry redirection, and the commit-state squash decision).
189935
189936
  *
@@ -189942,18 +189943,17 @@ const validateFieldRules = (name, state, key, spec) => {
189942
189943
  * commit states), and an optional `retry` cap. A `WorkflowDefinition`
189943
189944
  * separately declares `entries` — the state names a process may START at
189944
189945
  * (`default`, plus `manual` — every state declaring `entry: true`, enterable
189945
- * via `gtd step <actor> --entry <state>`). A definition
189946
+ * via `gtd --entry <state>`). A definition
189946
189947
  * may also declare `modes:` —
189947
189948
  * named pairs of format/validate shell commands a state's `mode:` can point
189948
189949
  * at (see `ModeDef`); they are inert data here too, rendered and executed
189949
189950
  * only at the edge (`src/SteeringMode.ts`).
189950
189951
  *
189951
189952
  * This module is intentionally pure — no git, no filesystem, no Effect, no
189952
- * IO of any kind. It mirrors the purity discipline documented at the top of
189953
- * `./Subjects.ts` and `./Machine.ts`: every export here is a plain function
189954
- * of its arguments. Rendering templates, executing scripts, walking git
189955
- * history for the process trace, and performing the actual commit/squash
189956
- * are all EDGE concerns for a later phase.
189953
+ * IO of any kind: every export is a plain function of its arguments.
189954
+ * Rendering templates (`src/PatternTemplates.ts`), walking git history for
189955
+ * the process trace (`src/Edge.ts`), and emitting the commit/squash scripts
189956
+ * a driver runs (`src/Edge.ts`/`src/Emit.ts`) are all EDGE concerns.
189957
189957
  *
189958
189958
  * Its one import is `./StateFields.js` — the state-field vocabulary
189959
189959
  * (`Actor`, `StateName`, `ContentKind`, `StateMode`, `OnEdge`, `RetryDef`,
@@ -190005,7 +190005,7 @@ const TRANSITION_SEP = " → ";
190005
190005
  * state being ENTERED and `<from>` the state the authored changes were made
190006
190006
  * in, so the subject reads as what this commit DID, not just where the machine
190007
190007
  * is headed. `from` is optional: when it is omitted or equals `to` (a
190008
- * self-loop, or a manual entry like `gtd step <actor> --entry <state>` that
190008
+ * self-loop, or a manual entry like `gtd --entry <state>` that
190009
190009
  * has no meaningful source), the subject collapses to the bare
190010
190010
  * `gtd(<actor>): <to>` form.
190011
190011
  * `resolveState` reads back only `<to>` — the ` → ` prefix is human context.
@@ -190237,8 +190237,16 @@ const memoryScopeAt = (scopes, state, trace) => {
190237
190237
  * effect. Refusals: `invoker` isn't `state`'s declared actor (out-of-turn),
190238
190238
  * or the tree is dirty and no `on` pattern matches (no-match, naming the
190239
190239
  * declared patterns so the CLI can print them). A clean tree with no
190240
- * matching pattern is a no-op (not a refusal) — the loop protocol's clean
190241
- * steps are the default, silent case. A match's target is retry-redirected
190240
+ * matching pattern is a plain no-op at a `script`/`message` rest — the loop
190241
+ * protocol's clean steps are the default, silent case there — but at a
190242
+ * `prompt` rest it is an ATTEMPT instead: the state itself becomes the raw
190243
+ * target, so it falls through the same retry/commit-state tail as a real
190244
+ * match, tagged `attempt: true` on the resulting `"commit"` (never on a
190245
+ * `"squash"`, which a redirect straight into a commit state still produces
190246
+ * unchanged) — see `StepCommit.attempt`'s doc comment for why a fruitless
190247
+ * dispatch is committed at all, and `wouldAttempt` for the "would another
190248
+ * dispatch just repeat this" question a step itself doesn't need to ask. A
190249
+ * match's target (or an attempt's self-target) is retry-redirected
190242
190250
  * (`applyRetry`) before being classified: a commit-state target yields a
190243
190251
  * `"squash"` decision carrying its `commit` template verbatim; anything
190244
190252
  * else yields a `"commit"` decision naming the `gtd(<invoker>): <from> → <to>`
@@ -190260,20 +190268,23 @@ const step = (def, state, invoker, payload) => {
190260
190268
  awaits: stateDef.actor
190261
190269
  };
190262
190270
  const onEdges = stateDef.on ?? [];
190263
- const rawTarget = matchOn(onEdges, payload.changes);
190264
- if (rawTarget === void 0) {
190265
- if (payload.changes.length === 0) return {
190266
- kind: "noop",
190267
- state
190268
- };
190269
- return {
190271
+ let target = matchOn(onEdges, payload.changes);
190272
+ let attempt = false;
190273
+ if (target === void 0) {
190274
+ if (payload.changes.length !== 0) return {
190270
190275
  kind: "refusal",
190271
190276
  reason: "no-match",
190272
190277
  state,
190273
190278
  patterns: onEdges.map(([pattern]) => pattern)
190274
190279
  };
190280
+ if (contentKindOf(stateDef) !== "prompt") return {
190281
+ kind: "noop",
190282
+ state
190283
+ };
190284
+ target = state;
190285
+ attempt = true;
190275
190286
  }
190276
- const finalTarget = applyRetry(def, rawTarget, payload.processTrace);
190287
+ const finalTarget = applyRetry(def, target, payload.processTrace);
190277
190288
  const targetDef = def.states[finalTarget];
190278
190289
  if (targetDef === void 0) throw new Error(`step: "${state}" transitions to undefined state "${finalTarget}"`);
190279
190290
  if (targetDef.commit !== void 0) return {
@@ -190287,10 +190298,32 @@ const step = (def, state, invoker, payload) => {
190287
190298
  subject: stateSubject(invoker, finalTarget, state),
190288
190299
  actor: invoker,
190289
190300
  from: state,
190290
- to: finalTarget
190301
+ to: finalTarget,
190302
+ ...attempt ? { attempt: true } : {}
190291
190303
  };
190292
190304
  };
190293
190305
  /**
190306
+ * Would a clean step (an empty change set) at `state`, invoked by its own
190307
+ * declared actor, record ANOTHER attempt that stays AT `state` — "another
190308
+ * dispatch would just record another attempt" (see `Edge.ts`'s `stalledAt`,
190309
+ * the derived stall's other half). Runs `step` itself rather than
190310
+ * restating its retry-redirect precedence: a capped `retry` that would
190311
+ * redirect the attempt to `otherwise` (or squash it into a commit state)
190312
+ * makes this `false` — a further dispatch would actually escalate, not
190313
+ * repeat the same fruitless turn. `false` for an unknown/commit state, or
190314
+ * any state whose clean step is a signal (`C`) or a plain no-op
190315
+ * (`script`/`message`).
190316
+ */
190317
+ const wouldAttempt = (def, state, processTrace) => {
190318
+ const stateDef = def.states[state];
190319
+ if (stateDef?.actor === void 0) return false;
190320
+ const decision = step(def, state, stateDef.actor, {
190321
+ changes: [],
190322
+ processTrace
190323
+ });
190324
+ return decision.kind === "commit" && decision.attempt === true && decision.to === state;
190325
+ };
190326
+ /**
190294
190327
  * `entries.default` names a defined, non-commit state. Every entry in
190295
190328
  * `entries.manual`, when present, must likewise name a defined, non-commit
190296
190329
  * state distinct from `entries.default` — a manual entry is a DELIBERATE,
@@ -190402,8 +190435,8 @@ const validateRetry = (name, state, names) => {
190402
190435
  * `retry.otherwise` redirects (a redirect ENTERS its `otherwise` state exactly
190403
190436
  * like an `on` match enters its target — see `applyRetry` — so both are real
190404
190437
  * edges). The roots are `def.entries` — `default` PLUS every state named in
190405
- * `entries.manual`: a manual entry is entered directly (`gtd step <actor>
190406
- * --entry <state>`), so a state reachable only from one of them is
190438
+ * `entries.manual`: a manual entry is entered directly (`gtd --entry
190439
+ * <state>`), so a state reachable only from one of them is
190407
190440
  * legitimately reachable, not dead config (without seeding them, e.g. a
190408
190441
  * manual entry whose only inbound path is `--entry` would be wrongly
190409
190442
  * flagged). Plain BFS; targets naming undefined states are skipped here (they
@@ -191387,6 +191420,20 @@ const STEERING_FORMATS = /* @__PURE__ */ new Map([["qa", QA_FORMAT], ["review",
191387
191420
  const builtInModeNames = () => [...STEERING_FORMATS.keys()];
191388
191421
  /** The built-in `SteeringFormat` named `mode`, or `undefined` when `mode` names no built-in. */
191389
191422
  const steeringFormatFor = (mode) => STEERING_FORMATS.get(mode);
191423
+ /**
191424
+ * The literal (unrendered) Eta template a workflow compiler seeds as a
191425
+ * built-in mode's own `validate:` command — quoted so a file path containing
191426
+ * spaces survives `renderModeCommand`'s interpolation.
191427
+ */
191428
+ const seededValidateCommand = (mode) => `gtd check ${mode} '<%= it.file %>'`;
191429
+ /**
191430
+ * True when `command` is EXACTLY `mode`'s own seeded template string — a
191431
+ * literal comparison against the declared template, never a rendered
191432
+ * command, so recognition doesn't depend on which file the mode is applied
191433
+ * to. Lets a later mode resolver tell gtd's own seeding apart from a user's
191434
+ * hand-written override of the same mode.
191435
+ */
191436
+ const isSeededValidateCommand = (mode, command) => command === seededValidateCommand(mode);
191390
191437
  //#endregion
191391
191438
  //#region src/PatternConfig.ts
191392
191439
  /**
@@ -191427,7 +191474,7 @@ const steeringFormatFor = (mode) => STEERING_FORMATS.get(mode);
191427
191474
  * label: <string> # optional, opaque display name — never on a commit state
191428
191475
  * file: <string> # optional, an Eta template naming the state's steering file — never on a commit state
191429
191476
  * mode: <modeName> # optional, requires "file" — a built-in (qa/review) or a `modes:` entry; never on a commit state
191430
- * entry: true # optional — an EXTRA reachability root (WorkflowEntries.manual), enterable via `gtd step <actor> --entry <this state's qualified name>`; distinct from the top-level `entry:` key above
191477
+ * 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
191431
191478
  * <local>: { machine: <name>, with: { ... } } # a REFERENCE — instantiates <name> as a child, see src/Machines.ts
191432
191479
  * ```
191433
191480
  *
@@ -191460,11 +191507,13 @@ const steeringFormatFor = (mode) => STEERING_FORMATS.get(mode);
191460
191507
  * `ConfigService` compiles through this module's `compileModesMap` and hands
191461
191508
  * back in as `rcModes`, merged per half by `mergeModes`. Underneath BOTH layers
191462
191509
  * this compiler seeds `src/SteeringFormats.ts`'s built-in registry names (`qa`/
191463
- * `review`) as empty entries, so every compiled definition's `modes` map always
191464
- * carries them and a workflow never has to declare them just to use gtd's own
191465
- * validators — resolution of the merged map against the registry (whether a
191466
- * declared `validate:` displaces a format's own parser) is the edge's job
191467
- * (`src/SteeringMode.ts`), not this compiler's.
191510
+ * `review`) with that module's own `seededValidateCommand(name)` template as
191511
+ * their `validate:`, so every compiled definition's `modes` map always carries
191512
+ * a usable `validate:` command for them and a workflow never has to declare
191513
+ * them just to use gtd's own validators — a workflow (or `.gtdrc`) may still
191514
+ * override either half per name, and `src/SteeringFormats.ts`'s
191515
+ * `isSeededValidateCommand` lets the edge (`src/SteeringMode.ts`) tell gtd's
191516
+ * own seeding apart from a genuine user override.
191468
191517
  *
191469
191518
  * ## File references
191470
191519
  *
@@ -192105,7 +192154,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
192105
192154
  const unknownTopKeys = Object.keys(raw).filter((k) => !KNOWN_TOP_KEYS.has(k));
192106
192155
  if (unknownTopKeys.length > 0) errors.push(`unknown top-level key(s) ${unknownTopKeys.join(", ")}`);
192107
192156
  const vars = compileVarsMap(raw.vars, errors);
192108
- const seeded = Object.fromEntries(builtInModeNames().map((name) => [name, {}]));
192157
+ const seeded = Object.fromEntries(builtInModeNames().map((name) => [name, { validate: seededValidateCommand(name) }]));
192109
192158
  const modes = mergeModes(mergeModes(seeded, compileModesMap(raw.modes, errors)), rcModes);
192110
192159
  validateMachinesShape(raw.machines, errors);
192111
192160
  const flattened = flattenMachines(raw, errors);
@@ -192138,7 +192187,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
192138
192187
  };
192139
192188
  //#endregion
192140
192189
  //#region src/workflows/unified.yaml
192141
- 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`). Two\n# gate/queue machines below are deliberately IDENTITY-FREE (no `model:`):\n# `entryGate`/`healthGate` are infrastructure, not a persona, and `packageLoop`\n# is a bookkeeping queue, not a builder.\n#\n# * entryGate (dedup ×3) — the green-baseline gate (check + human blocked)\n# guarding the simple (`plan-gate`), advanced\n# (`spec-gate`) and review (`review-gate`)\n# entries. Identity-free.\n# * healthGate (dedup ×2) — the check/escalate health pair shared by the\n# simple build'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# * advancedPlan (encap) — the advanced flow's one design conversation:\n# product Q&A, technical Q&A, and decomposition\n# (`design.*`: product-author/-answer,\n# technical-author/-answer, decompose) all share\n# ONE machine identity, so the agent explores the\n# codebase once, not three times, and the human's\n# answer rationale survives into decomposition.\n# ▸ planner.\n# * planLoop (encap) — the simple flow's plan-iteration loop (`plan.*`).\n# ▸ planner.\n# * humanReview (encap) — the shared review tail, nested INSIDE\n# `simpleBuild` (`build.review.*`: reviewing →\n# await-review → deciding → collecting) — see\n# \"Why humanReview is nested, not a root sibling\"\n# below. ▸ planner — the implementer's OWN\n# follow-through on feedback is a coder action,\n# not a reviewer one, so it lives in\n# `build.addressing` instead.\n# * specReview (encap) — the advanced flow's per-package agentic review,\n# now a SINGLE state (`packages.item.spec.review`)\n# — a machine whose entire job is to be a\n# separate mind from the implementer. ▸ planner\n# — fixing the reviewer's own findings is a coder\n# action, so it lives in `packages.item.fix-spec`\n# instead.\n# * packageLoop (encap) — the advanced flow's 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 advanced flow's per-package build identity\n# (`packages.item.*`: building, fix-suite,\n# fix-spec, closing), plus its own `health` and\n# `spec` children. ▸ coder.\n# * simpleBuild (encap) — the simple flow's build identity (`build.*`:\n# building, squashing, addressing, fix), plus its\n# own `health` child AND its own `review` child\n# (humanReview, above). ▸ coder.\n#\n# ── Why humanReview is nested, not a root sibling ───────────────────────────\n#\n# `humanReview` sits UNDER `simpleBuild` (`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. The\n# review tail sits between the two coder turns that most need continuity —\n# `build.addressing` (follows through on review feedback) and `build.squashing`\n# (drafts the finale from the range this process built) — so hoisting it back\n# to a root sibling would force both of those turns to re-derive the feature\n# from scratch on every pass through the tail, instead of resuming the session\n# that built it. The reviewer itself is unaffected either way: `build.review.*`\n# is its own scope regardless of where it's nested, so it never resumes the\n# builder's session.\n#\n# ── Four entry points behind a green-baseline gate, one shared tail ─────────\n#\n# EVERY entry first runs the test suite and only proceeds when it is GREEN — you\n# never start new work (or a review) on top of a red baseline. A red run halts\n# at a human blocked gate (fix the failures and re-run, exactly like the\n# escalate states), or is what `gtd --entry fix-precheck` exists to repair.\n#\n# The initial `idle` state FORKS on which steering file the human creates,\n# routing into that flow's OWN start gate:\n#\n# * the configured requirementsFile (default .gtd/REQUIREMENTS.md) ->\n# `spec-gate.check` -> the ADVANCED flow (one design conversation across\n# product + technical Q&A and decomposition, per-package parallel build +\n# agentic spec-review).\n# * anything else (e.g. the configured todoFile, default .gtd/TODO.md) ->\n# `plan-gate.check` -> the SIMPLE flow (one grilling Q&A loop, a monolithic\n# build, no decomposition, no agentic review).\n#\n# A third 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 fourth, `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 three 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 inspects\n# the step and routes:\n# * no comment (every box ticked, no REVIEW.md note, no code edit) -> the diff\n# is a bare REVIEW.md deletion -> build.squashing -> done (the ONLY path\n# to the finale — the whole process collapses into one commit).\n# * any comment — a note in REVIEW.md OR a hand-edit to code — ->\n# build.review.deciding writes REVIEW_FEEDBACK.md -> build.review.collecting\n# implements it (completing any code the human hand-edited, never\n# reverting their lines) -> build.addressing (a coder action, not a\n# reviewer one — see \"Why humanReview is nested\" above) -> back through\n# `build.health.check` (the shared health pair, bound via the\n# `$onFeedback`/local `review` reference) -> build.review.reviewing\n# regenerates an INCREMENTAL REVIEW.md (build.review.deciding carries\n# `reviewBase: true`, so each feedback round's review + checkout window\n# covers only `last-review..HEAD`, i.e. the agent's follow-through, not the\n# human's own edit) -> await-review, looping until sign-off.\n# Two dead ends never commit: the review-signoff guard (src/StepGuards.ts)\n# REFUSES a step that leaves a box unticked with no comment (finish reviewing\n# first) and a deleted REVIEW.md (restore it and tick the boxes).\n#\n# The shared health pair (`build.health.check`/`.escalate`, plus the caller's\n# own `build.fix`) serves the simple build and the feedback loop — its green\n# edge targets `build.review.reviewing`. The advanced per-package loop keeps\n# its OWN health pair (`packages.item.health.check`/`.escalate`, plus\n# `packages.item.fix-suite`) because its green edge targets the per-package\n# `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\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.\n stateDir: .gtd\n todoFile: .gtd/TODO.md\n requirementsFile: .gtd/REQUIREMENTS.md\n architectureFile: .gtd/ARCHITECTURE.md\n packagesDir: .gtd/packages\n nextFile: .gtd/NEXT.md\n reviewFile: .gtd/REVIEW.md\n reviewRawFile: .gtd/REVIEW_RAW.md\n reviewFeedbackFile: .gtd/REVIEW_FEEDBACK.md\n feedbackFile: .gtd/FEEDBACK.md\n specFeedbackFile: .gtd/SPEC_FEEDBACK.md\n commitMsgFile: .gtd/COMMIT_MSG.md\n # The two opaque model tiers every agent state draws from — repoint them here\n # (or via a top-level `.gtdrc` `vars:` key / a `GTD_PLANNERMODEL` env var)\n # to change every state on that tier at once. `plannerModel` (default \"smart\")\n # is the heavier tier for the one-shot planning/technical-qa/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\n# `prose` is a format-only steering-file mode this workflow blesses for itself:\n# a curated free-form document with no gtd-side schema (the simple flow's plan\n# file, .gtd/TODO.md — see the `plan.planning` state below). An empty entry is\n# the format-only tier any workflow can declare for a name with no built-in\n# parser; `qa`/`review` need no entry here — src/PatternConfig.ts's compiler\n# seeds those two from the built-in registry (src/SteeringFormats.ts)\n# automatically.\nmodes:\n prose: {}\n\nentry:\n default: unified\n\nmachines:\n # ── DEDUP: the green-baseline entry gate (identity-free) ──────────────────\n # Runs the suite before any work starts (its script is the shared suite-check\n # wrapper — red -> the configured feedbackFile, green -> remove it); the `on`\n # rules turn that into \"halt at blocked\" vs \"proceed to $onGreen\". The script\n # and blocked message/describe differ per entry, so they ride in as `with:`\n # params. Kept SEPARATE from `healthGate` (below) even though both are a\n # check+something-else shape: this gate runs BEFORE planning, so it must NOT\n # sweep the plan/review steering files healthGate does — a human's fresh\n # sketch legitimately still exists here.\n entryGate:\n params: [onGreen, blockedMessage, blockedDescribe, checkLabel, blockedLabel, reviewBase]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # Anchored here and aliased by `fix-precheck` (`gtd --entry\n # fix-precheck` entry): one suite-check script shared by all four\n # call sites (this machine's ×3 instances plus `fix-precheck`)\n # instead of four source copies. These gates run BEFORE planning, so\n # — unlike `healthGate.check` — they must NOT sweep\n # `todoFile`/`requirementsFile`: the human's fresh sketch still\n # legitimately exists here.\n # `entry: true` on the shared local marks all three `entryGate`\n # instances (plan-gate/spec-gate/review-gate) as manual entries —\n # only `review-gate.check` actually needs the reachability root (it\n # is otherwise unreachable from `entries.default`), but the shared\n # dedup makes marking one mark all three. Accepted as correct: all\n # three 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. `plan-gate`/`spec-gate` bind the\n # literal empty string, which compiles away to \"no base\" here — they\n # are reached from `idle`, never manually entered with a base.\n reviewBase: $reviewBase\n script: &suiteCheck |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p \"<%~ it.vars.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 bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n mkdir -p \"$(dirname \"$feedback\")\"\n <%~ it.vars.testCommand %> > <%~ it.vars.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.vars.stateDir %>/.check-output ]; then\n mv <%~ it.vars.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.vars.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.vars.stateDir %>/.check-output\n rm -f \"$feedback\"\n fi\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 simple build'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 spent steering files (`todoFile`,\n # `reviewRawFile`, `reviewFeedbackFile`) — a mechanic `entryGate.check`\n # must NOT run, since it executes before planning, while a fresh\n # sketch legitimately still exists.\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p \"<%~ it.vars.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 bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n mkdir -p \"$(dirname \"$feedback\")\"\n # Sweep steering files spent by the states that led here: the plan\n # (building), the raw review capture (review.collecting), and the\n # feedback instructions (build.addressing) — each `rm -f` is a\n # no-op when the file is already gone or never existed on this path.\n todo=\"<%~ it.vars.todoFile %>\"\n reviewRaw=\"<%~ it.vars.reviewRawFile %>\"\n reviewFeedback=\"<%~ it.vars.reviewFeedbackFile %>\"\n rm -f \"$todo\" \"$reviewRaw\" \"$reviewFeedback\"\n <%~ it.vars.testCommand %> > <%~ it.vars.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.vars.stateDir %>/.check-output ]; then\n mv <%~ it.vars.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.vars.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.vars.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 step human`):\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 # ── ENCAPSULATION: the advanced flow's design conversation ─────────────────\n # One planner identity across product Q&A -> technical Q&A -> decomposition:\n # a single codebase exploration instead of three, and the human's answer\n # rationale survives from product into technical and into decomposition (all\n # five states share one memory scope, `design`). Replaces the former\n # `qaLoop ×2` (which split one design conversation into two minds that each\n # re-explored the codebase) plus `simpleBuild`'s own `decompose` (which\n # landed on the coder tier only because it happened to live in the build\n # machine — decomposition is judgement-heavy planning work, not building).\n # ▸ planner identity — `model:` declared once here, stamped onto\n # `product-author`, `technical-author`, and `decompose` (its own prompt\n # states; the `*-answer` states are human turns and carry no prompt).\n advancedPlan:\n model: <%= it.vars.plannerModel %>\n params: [onPackages]\n entry: product-author\n states:\n product-author:\n actor: agent\n label: Refining the product plan\n file: <%= it.vars.requirementsFile %>\n mode: qa\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 turn\n touches is `<%= it.vars.requirementsFile %>`; develop it. Do not\n create other files to hold notes or output.\n\n `<%= it.vars.requirementsFile %>` holds the product requirements (or the\n plan you already wrote plus the human's answers on a return lap). Read it,\n explore the codebase, and develop it into a concrete, PRODUCT-level plan\n in this one turn.\n\n Scope: product and user-facing decisions only. Leave implementation\n details (file/module structure, data models, tech-stack choices) for the\n next phase (`<%= it.vars.architectureFile %>`).\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading near the top: one `### <question>` sub-heading\n each, followed by a checkbox list of candidate answers — aim for TWO\n concrete options plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself — the human ticks exactly one per question. The\n last `- [ ] _your answer_` line is the human's own-answer slot; write it\n with the literal `_your answer_` placeholder and leave it for them.\n\n On a RETURN lap the human has ticked one option per question (or replaced\n `_your answer_` with their own text). For each answered question: fold the\n chosen answer into the plan prose, then MOVE the question down into a\n `## Answered Questions` section as `### <question>` followed by the\n resolved answer in plain PROSE (drop the checkboxes). Never re-raise a\n question the human deleted; if the human deleted the whole\n `## Open Questions` section, treat the plan as accepted and surface no new\n questions unless something is genuinely still blocking. Omit\n `## Open Questions` entirely once there are none left.\n\n Leave `<%= it.vars.requirementsFile %>` uncommitted and finish your turn.\n on:\n \"* **\": product-answer\n\n product-answer:\n actor: human\n label: Awaiting your answers\n file: <%= it.vars.requirementsFile %>\n mode: qa\n answerGate: true\n message: |\n `<%= it.vars.requirementsFile %>` holds the product plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: technical-author\n action: Accept answers\n describe: >-\n with no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to move on to\n technical Q&A (**design.technical-author**).\n \"* **\":\n to: product-author\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**design.product-author**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n\n technical-author:\n actor: agent\n label: Refining the technical plan\n file: <%= it.vars.architectureFile %>\n mode: qa\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 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 If you developed those requirements earlier in this conversation, work\n from what you already settled; otherwise read\n `<%= it.vars.requirementsFile %>` (the converged product plan) yourself.\n Develop `<%= it.vars.architectureFile %>` from it in this one turn:\n file/module structure, data models, library/tech-stack choices,\n error-handling strategy — the *how*, building on the *what* already\n settled. Do not re-open product-level decisions; treat them as settled.\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading exactly as the grilling phase does: one\n `### <question>` each, followed by a checkbox list — TWO concrete options\n plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself. On a RETURN lap the human has ticked one option\n per question (or replaced `_your answer_` with their own text): fold each\n chosen answer into the plan prose, then MOVE the question into a\n `## Answered Questions` section as `### <question>` + the resolved answer\n in plain PROSE (drop the checkboxes). Never re-raise a deleted question;\n treat a deleted `## Open Questions` section as acceptance. Omit\n `## Open Questions` once there are none left.\n\n Once `<%= it.vars.architectureFile %>` is written, delete\n `<%= it.vars.requirementsFile %>` — its content has been folded in and it\n must not linger. Leave everything uncommitted and finish your turn.\n on:\n \"* **\": technical-answer\n\n technical-answer:\n actor: human\n label: Awaiting your answers\n file: <%= it.vars.architectureFile %>\n mode: qa\n answerGate: true\n message: |\n `<%= it.vars.architectureFile %>` holds the technical plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: decompose\n action: Accept answers\n describe: >-\n with no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to move on to\n decomposing the work into packages (**design.decompose**).\n \"* **\":\n to: technical-author\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**design.technical-author**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n\n decompose:\n actor: agent\n label: Decomposing into packages\n prompt: |\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. Decompose it into an ordered\n set of WORK PACKAGES, one file each under\n `<%= it.vars.packagesDir %>/` (e.g. `<%= it.vars.packagesDir %>/01-name.md`,\n `<%= it.vars.packagesDir %>/02-name.md`, ...). Order the packages so the\n test suite can pass again after each one lands.\n\n Each package file describes a set of INDEPENDENT tasks that can be\n implemented concurrently without ordering constraints between them — a\n single build turn will fan them out to parallel subagents. Give each task\n a clear description, acceptance criteria as `- [ ]` checkboxes, and the\n relevant paths. Also state the package's overall requirement so it can be\n reviewed against its spec. Do NOT put ordering-dependent tasks in the same\n package — split them across ordered packages instead.\n\n No package file may reference any other `.gtd/` file. Once the package\n files are written, delete `<%= it.vars.architectureFile %>`. Leave\n everything uncommitted and finish your turn.\n on:\n \"* <%= it.vars.packagesDir %>/**\": $onPackages\n\n # ── ENCAPSULATION: the simple flow's plan-iteration loop ───────────────────\n # ▸ planner identity — `model:` declared once here, stamped onto `planning`\n # (its only prompt state).\n planLoop:\n model: <%= it.vars.plannerModel %>\n params: [onAccept]\n entry: planning\n states:\n planning:\n actor: agent\n label: Refining the plan\n file: <%= it.vars.todoFile %>\n mode: prose\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.todoFile %>`; develop it into the plan.\n Do not create other files to hold notes or output.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build — or, on a\n return lap, the plan you already wrote plus the human's edits and comments\n on it. Read it, explore the codebase, and develop it into a concrete\n implementation plan: the files to change, the approach, and the steps to\n take.\n\n Do NOT ask questions and do NOT add an \"Open Questions\" section — decide\n every open point yourself and write the plan as a set of concrete\n decisions. Where a decision is genuinely a judgement call, state the\n choice you made and, in one line, why. The human iterates on the plan by\n editing it directly, not by answering questions.\n\n On a return lap, `<%= it.vars.todoFile %>` carries the human's edits and\n inline comments on your previous plan. If you wrote that plan earlier in\n this conversation, work from what you already know plus these edits;\n otherwise read the file fresh. Fold every edit and comment into a\n revised, self-contained plan and remove the addressed comments — the plan\n is always the current, clean proposal, never a diff.\n\n Leave `<%= it.vars.todoFile %>` uncommitted and finish your turn — do\n not commit, and do not run `gtd step agent` yourself; the harness does\n that.\n on:\n \"* **\": await-plan\n\n await-plan:\n actor: human\n label: Awaiting your review\n file: <%= it.vars.todoFile %>\n mode: prose\n message: |\n `<%= it.vars.todoFile %>` holds the proposed implementation plan. Read it,\n then either accept it as-is or edit it — rewrite a step, add a\n constraint, or drop an inline comment — to send it back for another round.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: $onAccept\n action: Accept plan\n describe: >-\n change nothing to move on to implementation (**build.building**).\n \"* **\":\n to: planning\n action: Revise plan\n describe: >-\n rewrite a step, add a constraint, or leave an inline comment to\n send it back for another round (**plan.planning**).\n\n # ── ENCAPSULATION: the shared review tail ──────────────────────────────────\n # ▸ planner identity — `model:` declared once here, stamped onto `reviewing`\n # and `collecting` (its only prompt states). The implementer's OWN\n # follow-through on feedback is a coder action, not a reviewer one — it\n # lives in `build.addressing` (`$onFeedback`), not inside this machine.\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 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 `- [ ] ./path/to/file.ts#42 — note` file\n pointer per hunk (checkboxes are for the human to tick, not you; every\n path is `./`-relative, with an optional `#line` and an optional\n ` — note`).\n\n 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\". Two dead ends can't be told apart by a\n # file-pattern edge, so the review-signoff guard (`src/StepGuards.ts`)\n # catches them BEFORE the step commits and refuses: an unfinished\n # review (a box still `- [ ]` with no comment) and a deleted REVIEW.md.\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 step human`:\n\n - **Sign off** — tick EVERY box and leave no comment — no note in\n `<%= it.vars.reviewFile %>`, no code edit — to collapse the process into\n one commit (**build.squashing**).\n - **Request changes** — leave a comment: a note on a\n `<%= it.vars.reviewFile %>` line, or a direct code edit — to send it to a\n build + re-review round (**review.deciding** → **build.addressing**). A\n code edit counts as your own fix: the agent completes the follow-through\n it implies and never reverts your lines.\n\n Stepping with a box still unticked and no comment is refused (finish\n reviewing first); so is deleting `<%= it.vars.reviewFile %>`.\n on:\n \"* **\": 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 bash\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 (the sign-off gate\n # refuses an incomplete step, so `deciding` is entered exactly once per\n # round — nothing commits in 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 other than `.gtd/` or the state's own\n # reviewFile (which is var-configurable and may live outside `.gtd/`, e.g.\n # a root-level REVIEW.md — issue #128). Otherwise it is a clean sign-off.\n # This turn only CAPTURES the raw material into REVIEW_RAW.md (the\n # `collecting` agent then turns it into instructions); it does NOT\n # 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 bash.\n stateDir=\"<%~ it.vars.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 | grep -v \"^$stateDir/\" | grep -vxF \"$reviewFile\" | grep -q . \\\n || [ \"$before\" != \"$after\" ]; then\n {\n echo \"Raw review material captured for classification. The human reviewed the changes and left the feedback below; a downstream agent turns it into an instruction list. This file is machine-captured input, not instructions — it names a commit, never inlined content.\"\n echo\n echo \"Commit: $head\"\n echo \"The human's notes are in $reviewFile at this commit; their hand edits,\"\n echo \"if any, are 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 collecting:\n actor: agent\n label: Collecting your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n prompt: |\n You are an autonomous coding agent turning raw review feedback into an\n explicit instruction list for a builder. 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.reviewRawFile %>` (read it, then\n delete it) and `<%= it.vars.reviewFeedbackFile %>` (write it) — do\n NOT change any 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 Classify every piece of it into `<%= it.vars.reviewFeedbackFile %>` as a\n flat, numbered instruction list. Each item is `N. ./path/to/file.ts#line —\n <imperative>`. Apply these rules exactly:\n\n 1. NOTES the human added to REVIEW.md — each note is a MANDATORY change,\n regardless of whether its checkbox is ticked (ticking means only \"I read\n this hunk\", never sign-off). Anchor each to the `./path#line` its review\n item names (or the chunk it sits under). Emit it as a concrete\n imperative.\n 2. Every COMMENT the human ADDED this round (a `+` line that is a code\n comment, in the hand-edited files) is a MANDATORY instruction the human\n is giving the builder — even a plain-prose one, even if its box is\n ticked. Emit an imperative to satisfy it and note that the comment line\n itself must be REMOVED once addressed. If a comment turns out to describe\n behaviour the code already has, the instruction is still \"confirm the\n code matches and remove the comment line\".\n 3. Every NON-comment code line the human hand-edited is an already-committed\n INTENT, NOT a do-then-delete item. Emit an instruction that names those\n files and says: their lines are final — complete ONLY the follow-through\n they imply (callers, tests, types, docs) and NEVER revert or rewrite\n them. Do not turn their own edit into a task.\n\n Split each hand-edited hunk per line: comment `+` lines follow rule 2, the\n surrounding real code follows rule 3.\n\n If — and only if — nothing in the raw material is actionable (e.g. the human\n left only an approving remark), write `<%= it.vars.reviewFeedbackFile %>`\n containing exactly one line: `NOTHING ACTIONABLE — <one-sentence reason>`.\n Never invent work to satisfy a non-actionable remark.\n\n Delete `<%= it.vars.reviewRawFile %>`, leave everything else uncommitted,\n and finish your turn.\n on:\n \"A <%= it.vars.reviewFeedbackFile %>\": $onFeedback\n \"M <%= it.vars.reviewFeedbackFile %>\": $onFeedback\n\n # ── ENCAPSULATION: the advanced flow's 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 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 advanced flow's 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 bash\n # gtd check turn (the package queue arbiter) — the loop driver executes\n # this verbatim, then steps the check actor. Mechanics only: take the\n # first package file (by name) under the packages dir into NEXT.md, or\n # remove NEXT.md when the queue is empty. What NEXT.md's presence/absence\n # MEANS (another package to build vs. the queue closing out to review) is\n # decided by this state's own `on` rules at capture time — never here.\n set +e\n # Hoist the NEXT path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a line —\n # it would glue the next line's `else`/`fi` onto it and break the script.\n # Below this point everything is plain bash.\n nextFile=\"<%~ it.vars.nextFile %>\"\n # Sweep the advanced flow's spent Q&A steering files: requirementsFile\n # is consumed by technical-qa, architectureFile by decompose — both are\n # gone by the time the package queue starts. No-op if already removed.\n requirements=\"<%~ it.vars.requirementsFile %>\"\n architecture=\"<%~ it.vars.architectureFile %>\"\n rm -f \"$requirements\" \"$architecture\"\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 advanced flow's 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. This turn writes no state\n file: do NOT delete the package file (the spec-review gate reads\n it after you) and never touch `<%= it.vars.nextFile %>` — the\n `picking` state owns it.\n\n The package to implement is: <%~ it.read(it.vars.nextFile) %>\n\n That file describes a set of INDEPENDENT tasks. Implement ALL of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement EXACTLY the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: &suiteFixPrompt |\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 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 bash\n # gtd check turn (the package closer) — remove the just-reviewed package\n # file (its path is in NEXT.md) plus any leftover spec feedback, so\n # `picking` selects the NEXT package (or falls through to review when the\n # queue is empty). Reached on approval (spec review clean) and on the\n # spec review retry cap (force-close, deferring the concern to the tail).\n set +e\n pkg=$(cat <%~ it.vars.nextFile %> 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f <%~ it.vars.specFeedbackFile %> <%~ it.vars.nextFile %>\n on:\n \"* **\": $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 simple flow's build identity ────────────────────────\n # ▸ coder identity — `model:` declared once here, stamped onto `building`,\n # `squashing`, `addressing`, and `fix` (its own prompt states). Decomposition\n # (`design.decompose`) belongs to the planner tier now, not here — see\n # `advancedPlan` below. `addressing` is the implementer's OWN follow-through\n # on review feedback (a coder action) — reached via `$onFeedback`, bound at\n # its own `review:` reference below, not nested inside the (planner-only)\n # `humanReview` machine. `humanReview` is nested HERE (not at the root) so\n # the review tail's run stays inside the builder's own scope — see the\n # header comment.\n simpleBuild:\n model: <%= it.vars.coderModel %>\n params: [onDone]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n file: <%= it.vars.todoFile %>\n prompt: |\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 file this turn touches is\n `<%= it.vars.todoFile %>`; delete it once its work is complete. Do not\n create other files to hold notes or output.\n\n The plan to implement is: <%~ it.read(it.vars.todoFile) %>\n\n Read that plan and implement it in full. Use TDD discipline: one test,\n then the implementation that passes it, then the next — never write all\n the tests first.\n\n Delete `<%= it.vars.todoFile %>` once the work it describes is complete\n and verified. Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": health.check\n\n squashing:\n actor: agent\n label: Squashing\n file: <%= it.vars.commitMsgFile %>\n prompt: |\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 addressing:\n actor: agent\n label: Addressing your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n # Refuse a work-free turn: a step whose only pending change is deleting the\n # instructions file (the original \"captured then discarded\" bug). The\n # feedback-progress guard (`src/StepGuards.ts`) reads this flag and\n # exempts a `NOTHING ACTIONABLE` sentinel (a non-actionable review round,\n # which legitimately makes no code change). The pure engine carries it inert.\n requireProgress: true\n prompt: |\n You are an autonomous coding agent addressing review feedback. This\n workflow steers itself through its own state files — treat them as\n its private scratchpad, never as project code or documentation. The\n only state file this turn touches is\n `<%= it.vars.reviewFeedbackFile %>`; address it, then delete it. Do\n not create other files to hold notes or output.\n\n The instructions to implement are: <%~ it.read(it.vars.reviewFeedbackFile) %>\n\n This is a flat instruction list, not a diff — execute every item exactly as\n written, no Q&A, no re-planning, using TDD discipline:\n\n - An item that says the human's own lines are final: build ON TOP of them to\n complete only the follow-through it names (callers, tests, types, docs).\n NEVER revert or rewrite the lines they already committed.\n - An item derived from a human comment: satisfy it AND delete that comment\n line from the code (the human's review comments are transient — none of\n them survive this round).\n\n If the file's sole content is a `NOTHING ACTIONABLE` line, make no code\n change; just delete the instructions file.\n\n Delete `<%= it.vars.reviewFeedbackFile %>` once every item is addressed.\n Leave everything else uncommitted and finish your turn. Deleting the\n instructions file WITHOUT making the changes it asks for is refused — the\n feedback must actually be addressed.\n on:\n \"* **\": health.check\n\n fix:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: *suiteFixPrompt\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 its run stays\n # inside the builder's own scope across the feedback round-trip — see\n # the header comment. Sign-off -> squashing; feedback -> addressing (the\n # implementer's own coder-identity follow-through, not a reviewer action).\n review:\n machine: humanReview\n with:\n onSignoff: squashing\n onFeedback: addressing\n\n # The root machine — the whole \"advanced/simple\" flow tree described in\n # this file's header comment. WIRING ONLY: no identity of its own, just the\n # shared 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 # 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, create a steering file describing what you want built:\n\n - `<%= it.vars.todoFile %>` — a short sketch — starts the SIMPLE flow\n (plan, build, review).\n - `<%= it.vars.requirementsFile %>` — the product requirements — starts\n the ADVANCED flow (product + technical Q&A, package decomposition,\n parallel build, per-package agentic review).\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* <%= it.vars.requirementsFile %>\":\n to: spec-gate\n action: Start advanced flow\n describe: >-\n save the product requirements to `.gtd/REQUIREMENTS.md` — gtd first\n checks the test baseline is green, then the agent develops them\n through product then technical Q&A, decomposes the work into\n packages, and builds each in parallel (**spec-gate.check**).\n \"* **\":\n to: plan-gate\n action: Start simple flow\n describe: >-\n save a short sketch to `.gtd/TODO.md` (or any other change) — gtd\n first checks the test baseline is green, then the agent reads it,\n explores the codebase, and develops it into a concrete\n implementation plan (**plan-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).\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 # ── Green-baseline gate: simple flow entry. ────────────────────────────────\n plan-gate:\n machine: entryGate\n with:\n onGreen: plan\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 step human`):\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 (**plan-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: advanced flow entry. ──────────────────────────────\n spec-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 step human`):\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 (**spec-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 step human`):\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 # ── Advanced flow: the design conversation (design.product-author/.product-answer ─\n # -> design.technical-author/.technical-answer -> design.decompose -> packages). ──\n design:\n machine: advancedPlan\n with:\n onPackages: packages\n\n # ── Simple flow plan-iteration loop (plan.planning/.await-plan -> build.building). ──\n plan:\n machine: planLoop\n with:\n onAccept: build.building\n\n # ── Advanced per-package build queue (packages.* -> build.review). ────────\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n # ── Simple flow's build identity (build.* -> build.review -> done). ───────\n build:\n machine: simpleBuild\n with:\n onPackages: packages\n onDone: done\n";
192190
+ 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`). Two\n# gate/queue machines below are deliberately IDENTITY-FREE (no `model:`):\n# `entryGate`/`healthGate` are infrastructure, not a persona, and `packageLoop`\n# is a bookkeeping queue, not a builder.\n#\n# * entryGate (dedup ×3) — the green-baseline gate (check + human blocked)\n# guarding the simple (`plan-gate`), advanced\n# (`spec-gate`) and review (`review-gate`)\n# entries. Identity-free.\n# * healthGate (dedup ×2) — the check/escalate health pair shared by the\n# simple build'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# * advancedPlan (encap) — the advanced flow's one design conversation:\n# product Q&A, technical Q&A, and decomposition\n# (`design.*`: product-author/-answer,\n# technical-author/-answer, decompose) all share\n# ONE machine identity, so the agent explores the\n# codebase once, not three times, and the human's\n# answer rationale survives into decomposition.\n# ▸ planner.\n# * planLoop (encap) — the simple flow's plan-iteration loop (`plan.*`).\n# ▸ planner.\n# * humanReview (encap) — the shared review tail, nested INSIDE\n# `simpleBuild` (`build.review.*`: reviewing →\n# await-review → deciding → collecting) — see\n# \"Why humanReview is nested, not a root sibling\"\n# below. ▸ planner — the implementer's OWN\n# follow-through on feedback is a coder action,\n# not a reviewer one, so it lives in\n# `build.addressing` instead.\n# * specReview (encap) — the advanced flow's per-package agentic review,\n# now a SINGLE state (`packages.item.spec.review`)\n# — a machine whose entire job is to be a\n# separate mind from the implementer. ▸ planner\n# — fixing the reviewer's own findings is a coder\n# action, so it lives in `packages.item.fix-spec`\n# instead.\n# * packageLoop (encap) — the advanced flow's 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 advanced flow's per-package build identity\n# (`packages.item.*`: building, fix-suite,\n# fix-spec, closing), plus its own `health` and\n# `spec` children. ▸ coder.\n# * simpleBuild (encap) — the simple flow's build identity (`build.*`:\n# building, squashing, addressing, fix), plus its\n# own `health` child AND its own `review` child\n# (humanReview, above). ▸ coder.\n#\n# ── Why humanReview is nested, not a root sibling ───────────────────────────\n#\n# `humanReview` sits UNDER `simpleBuild` (`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. The\n# review tail sits between the two coder turns that most need continuity —\n# `build.addressing` (follows through on review feedback) and `build.squashing`\n# (drafts the finale from the range this process built) — so hoisting it back\n# to a root sibling would force both of those turns to re-derive the feature\n# from scratch on every pass through the tail, instead of resuming the session\n# that built it. The reviewer itself is unaffected either way: `build.review.*`\n# is its own scope regardless of where it's nested, so it never resumes the\n# builder's session.\n#\n# ── Four entry points behind a green-baseline gate, one shared tail ─────────\n#\n# EVERY entry first runs the test suite and only proceeds when it is GREEN — you\n# never start new work (or a review) on top of a red baseline. A red run halts\n# at a human blocked gate (fix the failures and re-run, exactly like the\n# escalate states), or is what `gtd --entry fix-precheck` exists to repair.\n#\n# The initial `idle` state FORKS on which steering file the human creates,\n# routing into that flow's OWN start gate:\n#\n# * the configured requirementsFile (default .gtd/REQUIREMENTS.md) ->\n# `spec-gate.check` -> the ADVANCED flow (one design conversation across\n# product + technical Q&A and decomposition, per-package parallel build +\n# agentic spec-review).\n# * anything else (e.g. the configured todoFile, default .gtd/TODO.md) ->\n# `plan-gate.check` -> the SIMPLE flow (one grilling Q&A loop, a monolithic\n# build, no decomposition, no agentic review).\n#\n# A third 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 fourth, `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 three 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 inspects\n# the step and routes:\n# * no comment (every box ticked, no REVIEW.md note, no code edit) -> the diff\n# is a bare REVIEW.md deletion -> build.squashing -> done (the ONLY path\n# to the finale — the whole process collapses into one commit).\n# * any comment — a note in REVIEW.md OR a hand-edit to code — ->\n# build.review.deciding writes REVIEW_FEEDBACK.md -> build.review.collecting\n# implements it (completing any code the human hand-edited, never\n# reverting their lines) -> build.addressing (a coder action, not a\n# reviewer one — see \"Why humanReview is nested\" above) -> back through\n# `build.health.check` (the shared health pair, bound via the\n# `$onFeedback`/local `review` reference) -> build.review.reviewing\n# regenerates an INCREMENTAL REVIEW.md (build.review.deciding carries\n# `reviewBase: true`, so each feedback round's review + checkout window\n# covers only `last-review..HEAD`, i.e. the agent's follow-through, not the\n# human's own edit) -> await-review, looping until sign-off.\n# Two dead ends never commit: the review-signoff guard (src/StepGuards.ts)\n# REFUSES a step that leaves a box unticked with no comment (finish reviewing\n# first) and a deleted REVIEW.md (restore it and tick the boxes).\n#\n# The shared health pair (`build.health.check`/`.escalate`, plus the caller's\n# own `build.fix`) serves the simple build and the feedback loop — its green\n# edge targets `build.review.reviewing`. The advanced per-package loop keeps\n# its OWN health pair (`packages.item.health.check`/`.escalate`, plus\n# `packages.item.fix-suite`) because its green edge targets the per-package\n# `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\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.\n stateDir: .gtd\n todoFile: .gtd/TODO.md\n requirementsFile: .gtd/REQUIREMENTS.md\n architectureFile: .gtd/ARCHITECTURE.md\n packagesDir: .gtd/packages\n nextFile: .gtd/NEXT.md\n reviewFile: .gtd/REVIEW.md\n reviewRawFile: .gtd/REVIEW_RAW.md\n reviewFeedbackFile: .gtd/REVIEW_FEEDBACK.md\n feedbackFile: .gtd/FEEDBACK.md\n specFeedbackFile: .gtd/SPEC_FEEDBACK.md\n commitMsgFile: .gtd/COMMIT_MSG.md\n # The two opaque model tiers every agent state draws from — repoint them here\n # (or via a top-level `.gtdrc` `vars:` key / a `GTD_PLANNERMODEL` env var)\n # to change every state on that tier at once. `plannerModel` (default \"smart\")\n # is the heavier tier for the one-shot planning/technical-qa/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\n# `prose` is a format-only steering-file mode this workflow blesses for itself:\n# a curated free-form document with no gtd-side schema (the simple flow's plan\n# file, .gtd/TODO.md — see the `plan.planning` state below). An empty entry is\n# the format-only tier any workflow can declare for a name with no built-in\n# parser; `qa`/`review` need no entry here — src/PatternConfig.ts's compiler\n# seeds those two from the built-in registry (src/SteeringFormats.ts)\n# automatically.\nmodes:\n prose: {}\n\nentry:\n default: unified\n\nmachines:\n # ── DEDUP: the green-baseline entry gate (identity-free) ──────────────────\n # Runs the suite before any work starts (its script is the shared suite-check\n # wrapper — red -> the configured feedbackFile, green -> remove it); the `on`\n # rules turn that into \"halt at blocked\" vs \"proceed to $onGreen\". The script\n # and blocked message/describe differ per entry, so they ride in as `with:`\n # params. Kept SEPARATE from `healthGate` (below) even though both are a\n # check+something-else shape: this gate runs BEFORE planning, so it must NOT\n # sweep the plan/review steering files healthGate does — a human's fresh\n # sketch legitimately still exists here.\n entryGate:\n params: [onGreen, blockedMessage, blockedDescribe, checkLabel, blockedLabel, reviewBase]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # Anchored here and aliased by `fix-precheck` (`gtd --entry\n # fix-precheck` entry): one suite-check script shared by all four\n # call sites (this machine's ×3 instances plus `fix-precheck`)\n # instead of four source copies. These gates run BEFORE planning, so\n # — unlike `healthGate.check` — they must NOT sweep\n # `todoFile`/`requirementsFile`: the human's fresh sketch still\n # legitimately exists here.\n # `entry: true` on the shared local marks all three `entryGate`\n # instances (plan-gate/spec-gate/review-gate) as manual entries —\n # only `review-gate.check` actually needs the reachability root (it\n # is otherwise unreachable from `entries.default`), but the shared\n # dedup makes marking one mark all three. Accepted as correct: all\n # three 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. `plan-gate`/`spec-gate` bind the\n # literal empty string, which compiles away to \"no base\" here — they\n # are reached from `idle`, never manually entered with a base.\n reviewBase: $reviewBase\n script: &suiteCheck |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p \"<%~ it.vars.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 bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n mkdir -p \"$(dirname \"$feedback\")\"\n <%~ it.vars.testCommand %> > <%~ it.vars.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.vars.stateDir %>/.check-output ]; then\n mv <%~ it.vars.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.vars.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.vars.stateDir %>/.check-output\n rm -f \"$feedback\"\n fi\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 simple build'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 spent steering files (`todoFile`,\n # `reviewRawFile`, `reviewFeedbackFile`) — a mechanic `entryGate.check`\n # must NOT run, since it executes before planning, while a fresh\n # sketch legitimately still exists.\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p \"<%~ it.vars.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 bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n mkdir -p \"$(dirname \"$feedback\")\"\n # Sweep steering files spent by the states that led here: the plan\n # (building), the raw review capture (review.collecting), and the\n # feedback instructions (build.addressing) — each `rm -f` is a\n # no-op when the file is already gone or never existed on this path.\n todo=\"<%~ it.vars.todoFile %>\"\n reviewRaw=\"<%~ it.vars.reviewRawFile %>\"\n reviewFeedback=\"<%~ it.vars.reviewFeedbackFile %>\"\n rm -f \"$todo\" \"$reviewRaw\" \"$reviewFeedback\"\n <%~ it.vars.testCommand %> > <%~ it.vars.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.vars.stateDir %>/.check-output ]; then\n mv <%~ it.vars.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.vars.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.vars.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 # ── ENCAPSULATION: the advanced flow's design conversation ─────────────────\n # One planner identity across product Q&A -> technical Q&A -> decomposition:\n # a single codebase exploration instead of three, and the human's answer\n # rationale survives from product into technical and into decomposition (all\n # five states share one memory scope, `design`). Replaces the former\n # `qaLoop ×2` (which split one design conversation into two minds that each\n # re-explored the codebase) plus `simpleBuild`'s own `decompose` (which\n # landed on the coder tier only because it happened to live in the build\n # machine — decomposition is judgement-heavy planning work, not building).\n # ▸ planner identity — `model:` declared once here, stamped onto\n # `product-author`, `technical-author`, and `decompose` (its own prompt\n # states; the `*-answer` states are human turns and carry no prompt).\n advancedPlan:\n model: <%= it.vars.plannerModel %>\n params: [onPackages]\n entry: product-author\n states:\n product-author:\n actor: agent\n label: Refining the product plan\n file: <%= it.vars.requirementsFile %>\n mode: qa\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 turn\n touches is `<%= it.vars.requirementsFile %>`; develop it. Do not\n create other files to hold notes or output.\n\n `<%= it.vars.requirementsFile %>` holds the product requirements (or the\n plan you already wrote plus the human's answers on a return lap). Read it,\n explore the codebase, and develop it into a concrete, PRODUCT-level plan\n in this one turn.\n\n Scope: product and user-facing decisions only. Leave implementation\n details (file/module structure, data models, tech-stack choices) for the\n next phase (`<%= it.vars.architectureFile %>`).\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading near the top: one `### <question>` sub-heading\n each, followed by a checkbox list of candidate answers — aim for TWO\n concrete options plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself — the human ticks exactly one per question. The\n last `- [ ] _your answer_` line is the human's own-answer slot; write it\n with the literal `_your answer_` placeholder and leave it for them.\n\n On a RETURN lap the human has ticked one option per question (or replaced\n `_your answer_` with their own text). For each answered question: fold the\n chosen answer into the plan prose, then MOVE the question down into a\n `## Answered Questions` section as `### <question>` followed by the\n resolved answer in plain PROSE (drop the checkboxes). Never re-raise a\n question the human deleted; if the human deleted the whole\n `## Open Questions` section, treat the plan as accepted and surface no new\n questions unless something is genuinely still blocking. Omit\n `## Open Questions` entirely once there are none left.\n\n Leave `<%= it.vars.requirementsFile %>` uncommitted and finish your turn.\n on:\n \"* **\": product-answer\n\n product-answer:\n actor: human\n label: Awaiting your answers\n file: <%= it.vars.requirementsFile %>\n mode: qa\n answerGate: true\n message: |\n `<%= it.vars.requirementsFile %>` holds the product plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: technical-author\n action: Accept answers\n describe: >-\n with no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to move on to\n technical Q&A (**design.technical-author**).\n \"* **\":\n to: product-author\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**design.product-author**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n\n technical-author:\n actor: agent\n label: Refining the technical plan\n file: <%= it.vars.architectureFile %>\n mode: qa\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 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 If you developed those requirements earlier in this conversation, work\n from what you already settled; otherwise read\n `<%= it.vars.requirementsFile %>` (the converged product plan) yourself.\n Develop `<%= it.vars.architectureFile %>` from it in this one turn:\n file/module structure, data models, library/tech-stack choices,\n error-handling strategy — the *how*, building on the *what* already\n settled. Do not re-open product-level decisions; treat them as settled.\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading exactly as the grilling phase does: one\n `### <question>` each, followed by a checkbox list — TWO concrete options\n plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself. On a RETURN lap the human has ticked one option\n per question (or replaced `_your answer_` with their own text): fold each\n chosen answer into the plan prose, then MOVE the question into a\n `## Answered Questions` section as `### <question>` + the resolved answer\n in plain PROSE (drop the checkboxes). Never re-raise a deleted question;\n treat a deleted `## Open Questions` section as acceptance. Omit\n `## Open Questions` once there are none left.\n\n Once `<%= it.vars.architectureFile %>` is written, delete\n `<%= it.vars.requirementsFile %>` — its content has been folded in and it\n must not linger. Leave everything uncommitted and finish your turn.\n on:\n \"* **\": technical-answer\n\n technical-answer:\n actor: human\n label: Awaiting your answers\n file: <%= it.vars.architectureFile %>\n mode: qa\n answerGate: true\n message: |\n `<%= it.vars.architectureFile %>` holds the technical plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: decompose\n action: Accept answers\n describe: >-\n with no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to move on to\n decomposing the work into packages (**design.decompose**).\n \"* **\":\n to: technical-author\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**design.technical-author**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n\n decompose:\n actor: agent\n label: Decomposing into packages\n prompt: |\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. Decompose it into an ordered\n set of WORK PACKAGES, one file each under\n `<%= it.vars.packagesDir %>/` (e.g. `<%= it.vars.packagesDir %>/01-name.md`,\n `<%= it.vars.packagesDir %>/02-name.md`, ...). Order the packages so the\n test suite can pass again after each one lands.\n\n Each package file describes a set of INDEPENDENT tasks that can be\n implemented concurrently without ordering constraints between them — a\n single build turn will fan them out to parallel subagents. Give each task\n a clear description, acceptance criteria as `- [ ]` checkboxes, and the\n relevant paths. Also state the package's overall requirement so it can be\n reviewed against its spec. Do NOT put ordering-dependent tasks in the same\n package — split them across ordered packages instead.\n\n No package file may reference any other `.gtd/` file. Once the package\n files are written, delete `<%= it.vars.architectureFile %>`. Leave\n everything uncommitted and finish your turn.\n on:\n \"* <%= it.vars.packagesDir %>/**\": $onPackages\n\n # ── ENCAPSULATION: the simple flow's plan-iteration loop ───────────────────\n # ▸ planner identity — `model:` declared once here, stamped onto `planning`\n # (its only prompt state).\n planLoop:\n model: <%= it.vars.plannerModel %>\n params: [onAccept]\n entry: planning\n states:\n planning:\n actor: agent\n label: Refining the plan\n file: <%= it.vars.todoFile %>\n mode: prose\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.todoFile %>`; develop it into the plan.\n Do not create other files to hold notes or output.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build — or, on a\n return lap, the plan you already wrote plus the human's edits and comments\n on it. Read it, explore the codebase, and develop it into a concrete\n implementation plan: the files to change, the approach, and the steps to\n take.\n\n Do NOT ask questions and do NOT add an \"Open Questions\" section — decide\n every open point yourself and write the plan as a set of concrete\n decisions. Where a decision is genuinely a judgement call, state the\n choice you made and, in one line, why. The human iterates on the plan by\n editing it directly, not by answering questions.\n\n On a return lap, `<%= it.vars.todoFile %>` carries the human's edits and\n inline comments on your previous plan. If you wrote that plan earlier in\n this conversation, work from what you already know plus these edits;\n otherwise read the file fresh. Fold every edit and comment into a\n revised, self-contained plan and remove the addressed comments — the plan\n is always the current, clean proposal, never a diff.\n\n Leave `<%= it.vars.todoFile %>` uncommitted and finish your turn — do\n not commit, and do not run `gtd land` yourself; the harness does\n that.\n on:\n \"* **\": await-plan\n\n await-plan:\n actor: human\n label: Awaiting your review\n file: <%= it.vars.todoFile %>\n mode: prose\n message: |\n `<%= it.vars.todoFile %>` holds the proposed implementation plan. Read it,\n then either accept it as-is or edit it — rewrite a step, add a\n constraint, or drop an inline comment — to send it back for another round.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: $onAccept\n action: Accept plan\n describe: >-\n change nothing to move on to implementation (**build.building**).\n \"* **\":\n to: planning\n action: Revise plan\n describe: >-\n rewrite a step, add a constraint, or leave an inline comment to\n send it back for another round (**plan.planning**).\n\n # ── ENCAPSULATION: the shared review tail ──────────────────────────────────\n # ▸ planner identity — `model:` declared once here, stamped onto `reviewing`\n # and `collecting` (its only prompt states). The implementer's OWN\n # follow-through on feedback is a coder action, not a reviewer one — it\n # lives in `build.addressing` (`$onFeedback`), not inside this machine.\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 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 `- [ ] ./path/to/file.ts#42 — note` file\n pointer per hunk (checkboxes are for the human to tick, not you; every\n path is `./`-relative, with an optional `#line` and an optional\n ` — note`).\n\n 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\". Two dead ends can't be told apart by a\n # file-pattern edge, so the review-signoff guard (`src/StepGuards.ts`)\n # catches them BEFORE the step commits and refuses: an unfinished\n # review (a box still `- [ ]` with no comment) and a deleted REVIEW.md.\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** — tick EVERY box and leave no comment — no note in\n `<%= it.vars.reviewFile %>`, no code edit — to collapse the process into\n one commit (**build.squashing**).\n - **Request changes** — leave a comment: a note on a\n `<%= it.vars.reviewFile %>` line, or a direct code edit — to send it to a\n build + re-review round (**review.deciding** → **build.addressing**). A\n code edit counts as your own fix: the agent completes the follow-through\n it implies and never reverts your lines.\n\n Stepping with a box still unticked and no comment is refused (finish\n reviewing first); so is deleting `<%= it.vars.reviewFile %>`.\n on:\n \"* **\": 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 bash\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 (the sign-off gate\n # refuses an incomplete step, so `deciding` is entered exactly once per\n # round — nothing commits in 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 other than `.gtd/` or the state's own\n # reviewFile (which is var-configurable and may live outside `.gtd/`, e.g.\n # a root-level REVIEW.md — issue #128). Otherwise it is a clean sign-off.\n # This turn only CAPTURES the raw material into REVIEW_RAW.md (the\n # `collecting` agent then turns it into instructions); it does NOT\n # 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 bash.\n stateDir=\"<%~ it.vars.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 | grep -v \"^$stateDir/\" | grep -vxF \"$reviewFile\" | grep -q . \\\n || [ \"$before\" != \"$after\" ]; then\n {\n echo \"Raw review material captured for classification. The human reviewed the changes and left the feedback below; a downstream agent turns it into an instruction list. This file is machine-captured input, not instructions — it names a commit, never inlined content.\"\n echo\n echo \"Commit: $head\"\n echo \"The human's notes are in $reviewFile at this commit; their hand edits,\"\n echo \"if any, are 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 collecting:\n actor: agent\n label: Collecting your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n prompt: |\n You are an autonomous coding agent turning raw review feedback into an\n explicit instruction list for a builder. 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.reviewRawFile %>` (read it, then\n delete it) and `<%= it.vars.reviewFeedbackFile %>` (write it) — do\n NOT change any 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 Classify every piece of it into `<%= it.vars.reviewFeedbackFile %>` as a\n flat, numbered instruction list. Each item is `N. ./path/to/file.ts#line —\n <imperative>`. Apply these rules exactly:\n\n 1. NOTES the human added to REVIEW.md — each note is a MANDATORY change,\n regardless of whether its checkbox is ticked (ticking means only \"I read\n this hunk\", never sign-off). Anchor each to the `./path#line` its review\n item names (or the chunk it sits under). Emit it as a concrete\n imperative.\n 2. Every COMMENT the human ADDED this round (a `+` line that is a code\n comment, in the hand-edited files) is a MANDATORY instruction the human\n is giving the builder — even a plain-prose one, even if its box is\n ticked. Emit an imperative to satisfy it and note that the comment line\n itself must be REMOVED once addressed. If a comment turns out to describe\n behaviour the code already has, the instruction is still \"confirm the\n code matches and remove the comment line\".\n 3. Every NON-comment code line the human hand-edited is an already-committed\n INTENT, NOT a do-then-delete item. Emit an instruction that names those\n files and says: their lines are final — complete ONLY the follow-through\n they imply (callers, tests, types, docs) and NEVER revert or rewrite\n them. Do not turn their own edit into a task.\n\n Split each hand-edited hunk per line: comment `+` lines follow rule 2, the\n surrounding real code follows rule 3.\n\n If — and only if — nothing in the raw material is actionable (e.g. the human\n left only an approving remark), write `<%= it.vars.reviewFeedbackFile %>`\n containing exactly one line: `NOTHING ACTIONABLE — <one-sentence reason>`.\n Never invent work to satisfy a non-actionable remark.\n\n Delete `<%= it.vars.reviewRawFile %>`, leave everything else uncommitted,\n and finish your turn.\n on:\n \"A <%= it.vars.reviewFeedbackFile %>\": $onFeedback\n \"M <%= it.vars.reviewFeedbackFile %>\": $onFeedback\n\n # ── ENCAPSULATION: the advanced flow's 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 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 advanced flow's 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 bash\n # gtd check turn (the package queue arbiter) — the loop driver executes\n # this verbatim, then steps the check actor. Mechanics only: take the\n # first package file (by name) under the packages dir into NEXT.md, or\n # remove NEXT.md when the queue is empty. What NEXT.md's presence/absence\n # MEANS (another package to build vs. the queue closing out to review) is\n # decided by this state's own `on` rules at capture time — never here.\n set +e\n # Hoist the NEXT path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a line —\n # it would glue the next line's `else`/`fi` onto it and break the script.\n # Below this point everything is plain bash.\n nextFile=\"<%~ it.vars.nextFile %>\"\n # Sweep the advanced flow's spent Q&A steering files: requirementsFile\n # is consumed by technical-qa, architectureFile by decompose — both are\n # gone by the time the package queue starts. No-op if already removed.\n requirements=\"<%~ it.vars.requirementsFile %>\"\n architecture=\"<%~ it.vars.architectureFile %>\"\n rm -f \"$requirements\" \"$architecture\"\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 advanced flow's 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. This turn writes no state\n file: do NOT delete the package file (the spec-review gate reads\n it after you) and never touch `<%= it.vars.nextFile %>` — the\n `picking` state owns it.\n\n The package to implement is: <%~ it.read(it.vars.nextFile) %>\n\n That file describes a set of INDEPENDENT tasks. Implement ALL of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement EXACTLY the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: &suiteFixPrompt |\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 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 bash\n # gtd check turn (the package closer) — remove the just-reviewed package\n # file (its path is in NEXT.md) plus any leftover spec feedback, so\n # `picking` selects the NEXT package (or falls through to review when the\n # queue is empty). Reached on approval (spec review clean) and on the\n # spec review retry cap (force-close, deferring the concern to the tail).\n set +e\n pkg=$(cat <%~ it.vars.nextFile %> 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f <%~ it.vars.specFeedbackFile %> <%~ it.vars.nextFile %>\n on:\n \"* **\": $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 simple flow's build identity ────────────────────────\n # ▸ coder identity — `model:` declared once here, stamped onto `building`,\n # `squashing`, `addressing`, and `fix` (its own prompt states). Decomposition\n # (`design.decompose`) belongs to the planner tier now, not here — see\n # `advancedPlan` below. `addressing` is the implementer's OWN follow-through\n # on review feedback (a coder action) — reached via `$onFeedback`, bound at\n # its own `review:` reference below, not nested inside the (planner-only)\n # `humanReview` machine. `humanReview` is nested HERE (not at the root) so\n # the review tail's run stays inside the builder's own scope — see the\n # header comment.\n simpleBuild:\n model: <%= it.vars.coderModel %>\n params: [onDone]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n file: <%= it.vars.todoFile %>\n prompt: |\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 file this turn touches is\n `<%= it.vars.todoFile %>`; delete it once its work is complete. Do not\n create other files to hold notes or output.\n\n The plan to implement is: <%~ it.read(it.vars.todoFile) %>\n\n Read that plan and implement it in full. Use TDD discipline: one test,\n then the implementation that passes it, then the next — never write all\n the tests first.\n\n Delete `<%= it.vars.todoFile %>` once the work it describes is complete\n and verified. Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": health.check\n\n squashing:\n actor: agent\n label: Squashing\n file: <%= it.vars.commitMsgFile %>\n prompt: |\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 addressing:\n actor: agent\n label: Addressing your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n # Refuse a work-free turn: a step whose only pending change is deleting the\n # instructions file (the original \"captured then discarded\" bug). The\n # feedback-progress guard (`src/StepGuards.ts`) reads this flag and\n # exempts a `NOTHING ACTIONABLE` sentinel (a non-actionable review round,\n # which legitimately makes no code change). The pure engine carries it inert.\n requireProgress: true\n prompt: |\n You are an autonomous coding agent addressing review feedback. This\n workflow steers itself through its own state files — treat them as\n its private scratchpad, never as project code or documentation. The\n only state file this turn touches is\n `<%= it.vars.reviewFeedbackFile %>`; address it, then delete it. Do\n not create other files to hold notes or output.\n\n The instructions to implement are: <%~ it.read(it.vars.reviewFeedbackFile) %>\n\n This is a flat instruction list, not a diff — execute every item exactly as\n written, no Q&A, no re-planning, using TDD discipline:\n\n - An item that says the human's own lines are final: build ON TOP of them to\n complete only the follow-through it names (callers, tests, types, docs).\n NEVER revert or rewrite the lines they already committed.\n - An item derived from a human comment: satisfy it AND delete that comment\n line from the code (the human's review comments are transient — none of\n them survive this round).\n\n If the file's sole content is a `NOTHING ACTIONABLE` line, make no code\n change; just delete the instructions file.\n\n Delete `<%= it.vars.reviewFeedbackFile %>` once every item is addressed.\n Leave everything else uncommitted and finish your turn. Deleting the\n instructions file WITHOUT making the changes it asks for is refused — the\n feedback must actually be addressed.\n on:\n \"* **\": health.check\n\n fix:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: *suiteFixPrompt\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 its run stays\n # inside the builder's own scope across the feedback round-trip — see\n # the header comment. Sign-off -> squashing; feedback -> addressing (the\n # implementer's own coder-identity follow-through, not a reviewer action).\n review:\n machine: humanReview\n with:\n onSignoff: squashing\n onFeedback: addressing\n\n # The root machine — the whole \"advanced/simple\" flow tree described in\n # this file's header comment. WIRING ONLY: no identity of its own, just the\n # shared 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 # 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, create a steering file describing what you want built:\n\n - `<%= it.vars.todoFile %>` — a short sketch — starts the SIMPLE flow\n (plan, build, review).\n - `<%= it.vars.requirementsFile %>` — the product requirements — starts\n the ADVANCED flow (product + technical Q&A, package decomposition,\n parallel build, per-package agentic review).\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 \"* <%= it.vars.requirementsFile %>\":\n to: spec-gate\n action: Start advanced flow\n describe: >-\n save the product requirements to `.gtd/REQUIREMENTS.md` — gtd first\n checks the test baseline is green, then the agent develops them\n through product then technical Q&A, decomposes the work into\n packages, and builds each in parallel (**spec-gate.check**).\n \"* **\":\n to: plan-gate\n action: Start simple flow\n describe: >-\n save a short sketch to `.gtd/TODO.md` (or any other change) — gtd\n first checks the test baseline is green, then the agent reads it,\n explores the codebase, and develops it into a concrete\n implementation plan (**plan-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).\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 # ── Green-baseline gate: simple flow entry. ────────────────────────────────\n plan-gate:\n machine: entryGate\n with:\n onGreen: plan\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 (**plan-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: advanced flow entry. ──────────────────────────────\n spec-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 (**spec-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 # ── Advanced flow: the design conversation (design.product-author/.product-answer ─\n # -> design.technical-author/.technical-answer -> design.decompose -> packages). ──\n design:\n machine: advancedPlan\n with:\n onPackages: packages\n\n # ── Simple flow plan-iteration loop (plan.planning/.await-plan -> build.building). ──\n plan:\n machine: planLoop\n with:\n onAccept: build.building\n\n # ── Advanced per-package build queue (packages.* -> build.review). ────────\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n # ── Simple flow's build identity (build.* -> build.review -> done). ───────\n build:\n machine: simpleBuild\n with:\n onPackages: packages\n onDone: done\n";
192142
192191
  //#endregion
192143
192192
  //#region src/workflows/templates.ts
192144
192193
  /**
@@ -192761,13 +192810,15 @@ const makeGitImpl = (executor, root) => {
192761
192810
  hasCommits: () => exec("git", "rev-parse", "--verify", "HEAD").pipe(map$4(() => true), catchAll(() => succeed$3(false))),
192762
192811
  readFileAtRef: (ref, path) => exec("git", "show", `${ref}:${path}`),
192763
192812
  changedPathsSince: (ref) => exec("git", "diff", "--name-status", ref, "HEAD").pipe(map$4(parseNameStatus)),
192764
- changedPaths: () => gen(function* () {
192765
- const nameStatusOut = yield* exec("git", "diff", "--name-status", "HEAD").pipe(catchIf((e) => !isIndexLockError(e), () => succeed$3("")));
192813
+ changedPaths: (base) => gen(function* () {
192814
+ const nameStatusOut = yield* exec("git", "diff", "--name-status", base ?? "HEAD").pipe(catchIf((e) => !isIndexLockError(e), () => succeed$3("")));
192766
192815
  const trackedPaths = parseNameStatus(nameStatusOut);
192767
- const untracked = (yield* exec("git", "ls-files", "--others", "--exclude-standard", "-z")).split("\0").filter((s) => s.length > 0).map((path) => ({
192816
+ const untrackedAll = (yield* exec("git", "ls-files", "--others", "--exclude-standard", "-z")).split("\0").filter((s) => s.length > 0).map((path) => ({
192768
192817
  path,
192769
192818
  status: "A"
192770
192819
  }));
192820
+ const atBase = base === void 0 ? /* @__PURE__ */ new Set() : new Set((yield* exec("git", "ls-tree", "-r", "--name-only", "-z", base)).split("\0").filter((s) => s.length > 0));
192821
+ const untracked = untrackedAll.filter((entry) => !atBase.has(entry.path));
192771
192822
  const seen = /* @__PURE__ */ new Set();
192772
192823
  const all = [];
192773
192824
  for (const entry of [...trackedPaths, ...untracked]) if (!seen.has(entry.path)) {
@@ -192780,8 +192831,9 @@ const makeGitImpl = (executor, root) => {
192780
192831
  readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
192781
192832
  isAncestor: (a, b) => run(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$2(succeed$2(CommandExecutor, executor)), map$4(() => true), catchAll(() => succeed$3(false))),
192782
192833
  topLevel: () => exec("git", "rev-parse", "--show-toplevel").pipe(map$4((s) => s.trim())),
192783
- commitHistory: (base) => {
192784
- const range = base !== void 0 ? `${base}..HEAD` : void 0;
192834
+ gitDir: () => exec("git", "rev-parse", "--absolute-git-dir").pipe(map$4((s) => s.trim())),
192835
+ commitHistory: (base, head = "HEAD") => {
192836
+ const range = base !== void 0 ? `${base}..${head}` : head !== "HEAD" ? head : void 0;
192785
192837
  const args = [
192786
192838
  "git",
192787
192839
  "log",
@@ -192890,6 +192942,79 @@ var CommandRunner = class CommandRunner extends Tag("CommandRunner")() {
192890
192942
  }));
192891
192943
  };
192892
192944
  //#endregion
192945
+ //#region src/Sessions.ts
192946
+ /**
192947
+ * A conversation's session id is DERIVED, not remembered: `uuidv5(namespace,
192948
+ * memoryKey)`, where `memoryKey` (`memoryKeyFor` in `src/Edge.ts`) is already
192949
+ * a pure function of history — `<scope>#<anchor7>`, anchored to the commit
192950
+ * the current unbroken scope-run began from. Same scope-run → same key → same
192951
+ * id → the agent resumes its conversation; a new, unbroken entry into the
192952
+ * scope changes the anchor, hence the key, hence the id → a fresh
192953
+ * conversation. Nothing is written anywhere, so a peek (`gtd next --json`)
192954
+ * and a dispatch derive the exact same id — there is no table to keep in
192955
+ * sync, no write to race, no file to go stale.
192956
+ *
192957
+ * A prompt rest with no memory key (its state is absent from `stateScopes` —
192958
+ * possible only for a hand-built definition) keeps the old ephemeral
192959
+ * behaviour instead: a random id, `resume: false` — deriving something
192960
+ * stable there would resume forever across unrelated processes, and there is
192961
+ * no history to anchor a key to anyway.
192962
+ *
192963
+ * The crash edge: an agent turn that creates session X but lands no commit
192964
+ * (a crash, a killed driver) re-derives X on the next lap with the SAME
192965
+ * `resume: false` a fresh scope-run would report, so `claude --session-id X`
192966
+ * hits "id already in use" the second time around. `resolveSession` can't see
192967
+ * that from `memoryKey`/`resume` alone — the fix lives in the driver, which
192968
+ * must treat `resume` as a HINT rather than a contract: try the flag `resume`
192969
+ * points at first, and fall back to the other on failure —
192970
+ *
192971
+ * if [ "$resume" = true ]
192972
+ * then agent_turn --resume || agent_turn --session-id
192973
+ * else agent_turn --session-id || agent_turn --resume
192974
+ * fi
192975
+ *
192976
+ * — which also makes the inverse mismatch (`resume: true`, id gone: retention
192977
+ * expired, `~/.claude/projects` wiped) recover on its own, with no file to
192978
+ * delete.
192979
+ */
192980
+ /** Fixed forever — every derived session id descends from this one constant. */
192981
+ const GTD_SESSION_NAMESPACE = "ca4be249-805e-41c1-8b6a-75a11c011e25";
192982
+ const toBytes = (uuid) => Buffer.from(uuid.replace(/-/g, ""), "hex");
192983
+ const toUuidString = (bytes) => {
192984
+ const hex = bytes.toString("hex");
192985
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
192986
+ };
192987
+ /**
192988
+ * A UUIDv5 (RFC 4122 §4.3): `sha1(namespaceBytes ‖ nameBytes)`, the first 16
192989
+ * bytes, with the version nibble forced to `5` and the variant bits forced to
192990
+ * `10xx`. Hand-rolled over `node:crypto`'s SHA-1 rather than a dependency, so
192991
+ * its correctness is checked directly against the RFC's own DNS-namespace
192992
+ * test vector in `Sessions.test.ts` — not against this implementation's own
192993
+ * output.
192994
+ */
192995
+ const uuidv5 = (namespace, name) => {
192996
+ const bytes = createHash("sha1").update(toBytes(namespace)).update(name, "utf8").digest().subarray(0, 16);
192997
+ bytes[6] = bytes[6] & 15 | 80;
192998
+ bytes[8] = bytes[8] & 63 | 128;
192999
+ return toUuidString(bytes);
193000
+ };
193001
+ /**
193002
+ * Resolve the session id a `prompt` rest's turn should use: `memoryKey ===
193003
+ * undefined` mints an ephemeral id via `mint` (defaults to `randomUUID`) and
193004
+ * forces `resume: false` — there is no history to derive a stable id from.
193005
+ * Otherwise the id is `uuidv5(GTD_SESSION_NAMESPACE, memoryKey)` and `resume`
193006
+ * is passed through verbatim (the caller — `src/Edge.ts`'s
193007
+ * `memoryResumedFor` — already computed it from the same trace the key came
193008
+ * from).
193009
+ */
193010
+ const resolveSession = (memoryKey, resume, mint = randomUUID) => memoryKey === void 0 ? {
193011
+ sessionId: mint(),
193012
+ resume: false
193013
+ } : {
193014
+ sessionId: uuidv5(GTD_SESSION_NAMESPACE, memoryKey),
193015
+ resume
193016
+ };
193017
+ //#endregion
192893
193018
  //#region node_modules/eta/dist/index.mjs
192894
193019
  var EtaError = class extends Error {
192895
193020
  constructor(message) {
@@ -193455,13 +193580,6 @@ const withHistoryTrailer = (subject, hash) => `${subject}\n\n${HISTORY_TRAILER_P
193455
193580
  * by a later `gtd restore` command to recover the pre-squash tip.
193456
193581
  */
193457
193582
  const parseHistoryTrailer = (message) => HISTORY_TRAILER_RE.exec(message)?.[1];
193458
- /**
193459
- * Record the pre-squash tip so a later `gtd restore` can find it. A no-op
193460
- * (the git call is skipped entirely) when `tipHash === startParentHash`: an
193461
- * empty process — no turns were ever committed — has no turn chain worth
193462
- * keeping.
193463
- */
193464
- const retainHistory = (git, tipHash, startParentHash) => tipHash === startParentHash ? _void : git.updateRef(HISTORY_REF, tipHash);
193465
193583
  /** A thin wrapper over `git.readRefOption(HISTORY_REF)`. */
193466
193584
  const readRetainedHistory = (git) => git.readRefOption(HISTORY_REF);
193467
193585
  /**
@@ -193491,8 +193609,421 @@ const restorability = (git, headHash, headMessage, tipHash) => gen(function* ()
193491
193609
  reason: "HEAD has advanced past the squash — restoring would discard commits built on top of it"
193492
193610
  };
193493
193611
  });
193494
- /** Idempotent: `deleteRef` already tolerates a missing ref. */
193495
- const clearRetainedHistory = (git) => git.deleteRef(HISTORY_REF);
193612
+ //#endregion
193613
+ //#region src/GitScript.ts
193614
+ /**
193615
+ * Bash-script builders mirroring `GitWriterOperations` (`src/Git.ts`) — one
193616
+ * function per writer method, each returning the equivalent bash as a
193617
+ * `string` instead of executing it. THE emission path: every git write gtd
193618
+ * decides reaches the repository as one of these strings, assembled by
193619
+ * `src/Emit.ts` and run by an external driver — gtd itself never executes
193620
+ * them.
193621
+ *
193622
+ * Pure, like `src/PatternMachine.ts`: no git, no filesystem, no `Effect`, no
193623
+ * IO of any kind. Every export is a plain, total function of its arguments.
193624
+ * `src/Git.ts`'s doc comments on `GitWriterOperations` are the source of
193625
+ * truth for each shape below; read them for WHY, not just WHAT.
193626
+ */
193627
+ /**
193628
+ * POSIX single-quote escaping: wrap `value` in `'...'`, replacing every
193629
+ * embedded `'` with `'\''` (close quote, escaped literal quote, reopen
193630
+ * quote). The only place a raw string may reach a shell command in this
193631
+ * module — every builder below routes every interpolated value through this.
193632
+ */
193633
+ const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
193634
+ /**
193635
+ * `--allow-empty` mirrors `commitAllWithPrefix`/`commitAsIs`'s load-bearing
193636
+ * use in `src/Git.ts`: gtd's workflow commits may be empty on purpose. The
193637
+ * retry-without-hooks behavior there keys off the specific "empty git commit"
193638
+ * hook-rejection message, re-failing on anything else (`src/Git.ts`'s `run`
193639
+ * doc comment records the regression a blind retry-on-any-failure would
193640
+ * reintroduce: reporting success on a genuinely rejected commit — a lint
193641
+ * error, a rejected commit-msg hook). Bash CAN inspect the message: capture
193642
+ * the first attempt's combined output and discriminate on it in a `case`,
193643
+ * exactly like `isIndexLockError`/`error.message.includes(...)` do in
193644
+ * `src/Git.ts`, instead of retrying on the exit code alone.
193645
+ */
193646
+ const commitAllowEmpty = (message) => {
193647
+ const m = shellQuote(message);
193648
+ return [
193649
+ `if ! out=$(git commit --allow-empty -m ${m} 2>&1); then`,
193650
+ ` case "$out" in`,
193651
+ ` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} ;;`,
193652
+ ` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
193653
+ ` esac`,
193654
+ `fi`
193655
+ ].join("\n");
193656
+ };
193657
+ /**
193658
+ * `git add -A` then `git commit --allow-empty -m <message>`, retried without
193659
+ * hooks on the same hook rejection `commitAllowEmpty` guards against. Joined
193660
+ * with `&&` so a failing `git add` (e.g. a quoted path breaking it) never
193661
+ * reaches the commit — the same "lost files whose quoted paths broke a
193662
+ * swallowed `git add`" failure mode `src/Git.ts`'s `run` doc comment records.
193663
+ */
193664
+ const commitAll = (message) => `git add -A &&\n${commitAllowEmpty(message)}`;
193665
+ /** `git commit --allow-empty -m <message>` — commits the index as-is, no implicit `git add`. */
193666
+ const commitAsIs = (message) => commitAllowEmpty(message);
193667
+ /** `git reset --soft <ref>` — HEAD moves; index and working tree untouched. */
193668
+ const softResetTo = (ref) => `git reset --soft ${shellQuote(ref)}`;
193669
+ /** `git reset --mixed <ref>` — HEAD and index move; working tree untouched. */
193670
+ const mixedResetTo = (ref) => `git reset --mixed ${shellQuote(ref)}`;
193671
+ /** `git reset --hard <ref>` — HEAD, index, AND working tree all move. */
193672
+ const hardResetTo = (ref) => `git reset --hard ${shellQuote(ref)}`;
193673
+ /**
193674
+ * `git add -A` then `git reset --hard HEAD` — discards every pending change,
193675
+ * tracked or untracked. Joined with `&&`: a failed stage must not reach the
193676
+ * hard reset, or untracked survivors remain — the exact outcome this builder
193677
+ * exists to avoid.
193678
+ */
193679
+ const discardPending = () => `git add -A && git reset --hard HEAD`;
193680
+ /** `git update-ref <ref> <hash>` — point a repo-local ref at a commit. */
193681
+ const updateRef = (ref, hash) => `git update-ref ${shellQuote(ref)} ${shellQuote(hash)}`;
193682
+ /** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
193683
+ const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
193684
+ /**
193685
+ * `git restore --staged --source=<source> -- <paths…>` — tolerant of the
193686
+ * SAME two cases `src/Git.ts`'s `Effect.catchIf` tolerates (a missing ref, or
193687
+ * no path matching at `source`), and NOT tolerant of `index.lock` contention,
193688
+ * which must still fail the script rather than being swallowed here (see
193689
+ * `src/Git.ts`'s `restoreStagedFrom` doc comment and `isIndexLockError`) — a
193690
+ * lock failure silently swallowed here would mean the `.gtd/` index pin
193691
+ * silently doesn't happen, leaking `.gtd/` plumbing into the surfaced review
193692
+ * diff, the exact thing the pin exists to prevent. Captures the command's
193693
+ * output and discriminates on it, mirroring `isIndexLockError`'s two
193694
+ * substrings, exactly like `commitAllowEmpty` above. An empty `paths` emits
193695
+ * nothing at all (a harmless, syntactically valid empty script), mirroring
193696
+ * the Effect implementation's own `paths.length === 0` skip.
193697
+ */
193698
+ const restoreStagedFrom = (source, paths) => {
193699
+ if (paths.length === 0) return "";
193700
+ const pathArgs = paths.map(shellQuote).join(" ");
193701
+ return [
193702
+ `if ! out=$(git restore --staged --source=${shellQuote(source)} -- ${pathArgs} 2>&1); then`,
193703
+ ` case "$out" in`,
193704
+ ` *"index.lock"*|*"Another git process seems to be running"*) printf '%s\\n' "$out" >&2; exit 1 ;;`,
193705
+ ` *) : ;;`,
193706
+ ` esac`,
193707
+ `fi`
193708
+ ].join("\n");
193709
+ };
193710
+ //#endregion
193711
+ //#region src/OutcomeScript.ts
193712
+ /**
193713
+ * The human-facing wording for what a `gtd`-emitted script just landed —
193714
+ * printed by the DRIVER's bash when it runs `required`/`optional`, authored
193715
+ * once here in TS. Pure, like `src/GitScript.ts`: no git, no filesystem, no
193716
+ * `Effect`. `OUTCOME_PREAMBLE` defines the bash functions
193717
+ * (`gtd_report_transition`/`gtd_report_commit`/`gtd_report_note`/
193718
+ * `gtd_report_abandoned`/`gtd_report_restored`) a script calls; the builders
193719
+ * below emit the one-line calls, every argument routed through
193720
+ * `src/GitScript.ts`'s `shellQuote`.
193721
+ *
193722
+ * `noopText`/`abandonNoopText` render the shared no-op wording; both reach the
193723
+ * driver only through an emitted script now (`noteOutcome(noopText(state))`
193724
+ * for a genuine `gtd land` no-op, `abandonNoopOutcome` for `gtd abandon` with
193725
+ * nothing underway) — a no-op's script always prints itself, exactly like
193726
+ * every other outcome, rather than `src/program.ts` special-casing an empty
193727
+ * script. Their format strings live once here (`FMT_*`, with
193728
+ * `%s` placeholders) and reach both sides through `renderFormat` (the plain
193729
+ * path) and `printfLine` (the bash `printf` call embedded in
193730
+ * `OUTCOME_PREAMBLE`), so the two can never drift.
193731
+ */
193732
+ /** Substitute `args` into `fmt`'s `%s` placeholders, in order — the plain-text twin of `printfLine`. */
193733
+ const renderFormat = (fmt, ...args) => {
193734
+ let i = 0;
193735
+ return fmt.replace(/%s/g, () => args[i++] ?? "");
193736
+ };
193737
+ /**
193738
+ * A `printf '<fmt>' <args...>` bash statement — `args` are already-valid bash
193739
+ * tokens (a variable reference like `"$from"`), never re-quoted here: a value
193740
+ * reaches the script as a printf ARGUMENT, never interpolated into the format
193741
+ * itself, so a subject or state name containing `%` can never be read as a
193742
+ * conversion spec.
193743
+ */
193744
+ const printfLine = (fmt, args) => `printf ${shellQuote(fmt)} ${args.join(" ")}`;
193745
+ const FMT_NOOP = "nothing to do at \"%s\"\n";
193746
+ const FMT_ABANDON_NOOP = "no gtd process is underway (resting at \"%s\") — nothing to abandon\n";
193747
+ const FMT_ABANDONED = "abandoned the process resting at \"%s\" — HEAD is back at %s (\"%s\"), resting at \"%s\".\nEverything the process produced is kept as uncommitted changes (`git status`); discard them with `git checkout -- . && git clean -fd .gtd` for a clean tree.\n";
193748
+ const FMT_RESTORED = "restored the retained history — HEAD is back at %s (\"%s\"), resting at \"%s\". Resume with the loop, or `git reset` to any earlier turn to restart from there.\n";
193749
+ /** `nothing to do at "<state>"` — a no-op step's plain-text line, and the text a print-only script's `gtd_report_note` carries. */
193750
+ const noopText = (state) => renderFormat(FMT_NOOP, state);
193751
+ /** The initial-state collapse's own line — no commit landed, HEAD was rewound
193752
+ * to the process's start parent. Trailing newline, exactly like `noopText`:
193753
+ * printed verbatim by `program.ts` and via `noteOutcome`/`gtd_report_note` by
193754
+ * the emitted script. */
193755
+ const COLLAPSED_TEXT = "nothing to retain — rewound to the commit before the process started\n";
193756
+ /** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
193757
+ const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
193758
+ /**
193759
+ * The bash preamble every outcome-carrying script includes, ONE block (no
193760
+ * blank lines, like `src/Emit.ts`'s `RETRY_HELPER`) so `assembleScript`'s
193761
+ * blank-line-joined sections stay intact. First line is a literal comment
193762
+ * naming this module, so a reader (or the recognizer) can tell at a glance
193763
+ * where the block comes from.
193764
+ *
193765
+ * The palette/marker detection follows the `NO_COLOR` convention
193766
+ * (`[ -t 1 ] && [ -z "${NO_COLOR:-}" ]`) with the same plain-mode fallback
193767
+ * strings (`->`/`[commit]`/`...`) the now-deleted bash `bin/gtd` used to
193768
+ * render, so existing `@live` assertions on plain output kept matching once
193769
+ * this preamble took over printing it. `gtd_files` is the diff-tree read
193770
+ * (with `--root` so a repository's very first commit shows its files too)
193771
+ * plus the 3-row cap, fused into one function, since a script-side caller
193772
+ * always has the commit, never a pre-fetched file list.
193773
+ */
193774
+ const OUTCOME_PREAMBLE = [
193775
+ "# gtd: human-facing outcome rendering (see src/OutcomeScript.ts)",
193776
+ "if [ -t 1 ] && [ -z \"${NO_COLOR:-}\" ]; then",
193777
+ " gtd_c_reset=$'\\e[0m'; gtd_c_bold=$'\\e[1m'; gtd_c_dim=$'\\e[2m'",
193778
+ " gtd_c_cyan=$'\\e[36m'; gtd_c_green=$'\\e[32m'",
193779
+ " gtd_m_trans=\"➡️\"; gtd_m_commit=\"✅\"; gtd_m_ellipsis=\"…\"",
193780
+ "else",
193781
+ " gtd_c_reset=\"\"; gtd_c_bold=\"\"; gtd_c_dim=\"\"",
193782
+ " gtd_c_cyan=\"\"; gtd_c_green=\"\"",
193783
+ " gtd_m_trans=\"->\"; gtd_m_commit=\"[commit]\"; gtd_m_ellipsis=\"...\"",
193784
+ "fi",
193785
+ "gtd_files() {",
193786
+ " local sha=\"$1\" f shown=0 total=0 files",
193787
+ " files=\"$(git diff-tree --no-commit-id --name-only -r --root \"$sha\" 2>/dev/null || true)\"",
193788
+ " [ -z \"$files\" ] && return 0",
193789
+ " while IFS= read -r f; do [ -n \"$f\" ] && total=$((total + 1)); done < <(printf '%s\\n' \"$files\")",
193790
+ " while IFS= read -r f; do",
193791
+ " [ -z \"$f\" ] && continue",
193792
+ " if [ \"$shown\" -ge 3 ]; then",
193793
+ " printf ' %s%s (%d more)%s\\n' \"$gtd_c_dim\" \"$gtd_m_ellipsis\" \"$((total - 3))\" \"$gtd_c_reset\"",
193794
+ " break",
193795
+ " fi",
193796
+ " printf ' %s%s%s\\n' \"$gtd_c_dim\" \"$f\" \"$gtd_c_reset\"",
193797
+ " shown=$((shown + 1))",
193798
+ " done < <(printf '%s\\n' \"$files\")",
193799
+ "}",
193800
+ "gtd_report_transition() {",
193801
+ " local from=\"$1\" to=\"$2\"",
193802
+ " printf '%s %s%s → %s%s\\n' \"$gtd_m_trans\" \"${gtd_c_bold}${gtd_c_cyan}\" \"$from\" \"$to\" \"$gtd_c_reset\"",
193803
+ " gtd_files HEAD",
193804
+ "}",
193805
+ "gtd_report_commit() {",
193806
+ " local subject=\"$1\"",
193807
+ " printf '%s %s%s%s\\n' \"$gtd_m_commit\" \"$gtd_c_green\" \"$subject\" \"$gtd_c_reset\"",
193808
+ " gtd_files HEAD",
193809
+ "}",
193810
+ "gtd_report_note() {",
193811
+ " printf '%s\\n' \"$1\"",
193812
+ "}",
193813
+ "gtd_report_abandoned() {",
193814
+ " local from=\"$1\" head=\"$2\" state=\"$3\" short subject",
193815
+ " short=\"$(git rev-parse --short \"$head\")\"",
193816
+ " subject=\"$(git log -1 --format=%s \"$head\")\"",
193817
+ ` ${printfLine(FMT_ABANDONED, [
193818
+ "\"$from\"",
193819
+ "\"$short\"",
193820
+ "\"$subject\"",
193821
+ "\"$state\""
193822
+ ])}`,
193823
+ "}",
193824
+ "gtd_report_restored() {",
193825
+ " local to=\"$1\" state=\"$2\" short subject",
193826
+ " short=\"$(git rev-parse --short \"$to\")\"",
193827
+ " subject=\"$(git log -1 --format=%s \"$to\")\"",
193828
+ ` ${printfLine(FMT_RESTORED, [
193829
+ "\"$short\"",
193830
+ "\"$subject\"",
193831
+ "\"$state\""
193832
+ ])}`,
193833
+ "}"
193834
+ ].join("\n");
193835
+ /** `gtd_report_transition <from> <to>` — a landed self-loop/target-change transition, with its changed-file rows. */
193836
+ const transitionOutcome = (from, to) => `gtd_report_transition ${shellQuote(from)} ${shellQuote(to)}`;
193837
+ /** `gtd_report_commit <subject>` — a bare capture or a squash's final commit, with its changed-file rows. */
193838
+ const commitOutcome = (subject) => `gtd_report_commit ${shellQuote(subject)}`;
193839
+ /** `gtd_report_note <text>` — one already-rendered plain line, no marker. */
193840
+ const noteOutcome = (text) => `gtd_report_note ${shellQuote(text)}`;
193841
+ /** `gtd_report_abandoned <from> <head> <state>` — resolves the post-hoc short hash/subject from `head` (a commitish) in-script. */
193842
+ const abandonedOutcome = (from, head, state) => `gtd_report_abandoned ${shellQuote(from)} ${shellQuote(head)} ${shellQuote(state)}`;
193843
+ /** `gtd abandon`'s no-op outcome — the same wording `abandonNoopText` renders, printed via `gtd_report_note`. */
193844
+ const abandonNoopOutcome = (initial) => noteOutcome(abandonNoopText(initial));
193845
+ /** `gtd_report_restored <to> <state>` — resolves the post-hoc short hash/subject from `to` (a commitish) in-script. */
193846
+ const restoredOutcome = (to, state) => `gtd_report_restored ${shellQuote(to)} ${shellQuote(state)}`;
193847
+ //#endregion
193848
+ //#region src/Emit.ts
193849
+ /**
193850
+ * Assembles the two scripts a `gtd next`-time decision hands the external
193851
+ * driver: `required` (everything that decides what lands in git) and
193852
+ * `optional` (presentation only — e.g. opening the review checkout window so
193853
+ * an editor's diff view has something to show; a driver that skips it loses
193854
+ * nothing but that view). Pure, like `src/GitScript.ts`: no git, no
193855
+ * filesystem, no `Effect`. Every input string (a `GitScript.ts` builder's
193856
+ * output, a steering mode's already-rendered `format:`/`validate:` command)
193857
+ * arrives already rendered — this module only concatenates and wraps: once
193858
+ * for a `gitWrite` step's index-lock retry (`gtd_retry`), and once for a
193859
+ * `command` step's `onFailure` fix prompt (`failurePromptWrapper`), which
193860
+ * prints a ready-to-send instruction plus the command's captured output
193861
+ * ahead of propagating its exit code.
193862
+ *
193863
+ * Both halves are meant to stand alone: a driver may run either, both, or
193864
+ * only `required`, and a person could paste either into a terminal. That's
193865
+ * why each carries its OWN copy of the precondition asserts and the retry
193866
+ * helper, rather than the two sharing one preamble a caller has to splice
193867
+ * together correctly.
193868
+ */
193869
+ /**
193870
+ * The CLI resolves `expectedHead` (and a review window's `expectedHash`) at
193871
+ * DECIDE time; the script may RUN later, in a process that never shared
193872
+ * memory with the one that decided. This assertion closes that
193873
+ * time-of-check/time-of-use gap. A plain `[ ... ] || { ...; exit 1; }`
193874
+ * statement, not an `if`/`fi` block, so a line-oriented consumer can still
193875
+ * recognize it, and so it stays safe under `set -e`: in an OR list, `-e`
193876
+ * exempts every command except the LAST one, so the left-hand `[ ... ]`
193877
+ * failing here never trips `set -e` on its own — only the explicit `exit 1`
193878
+ * inside the right-hand group does. Exported (alongside `reviewWindowAssertion`)
193879
+ * solely so `src/testing/EmittedScriptRecognizer.ts` can re-derive and
193880
+ * string-compare this exact shape, the same "call the real builder, never
193881
+ * hand-copy its template" discipline it already applies to every
193882
+ * `GitScript.ts` builder.
193883
+ *
193884
+ * The probe is `git rev-parse --verify --quiet HEAD 2>/dev/null`, not the
193885
+ * bare `git rev-parse HEAD` a born repo's assertion could get away with —
193886
+ * load-bearing, not cosmetic: against an UNBORN HEAD (a repository with no
193887
+ * commits), the bare form prints the literal string `HEAD` to stdout (plus a
193888
+ * `fatal:` line to stderr) and exits 128, so `"$(git rev-parse HEAD)"` reads
193889
+ * as `"HEAD"`, never `""`, and an `expectedHead === ""` comparison could
193890
+ * never pass. `--verify --quiet` makes an unborn HEAD read back as an empty
193891
+ * string instead — the same idiom `reviewWindowAssertion` already uses for a
193892
+ * missing ref — so `[ "$(…)" = '' ]` is simply the unborn case of the same
193893
+ * comparison every born repo already runs; only the `printf` wording
193894
+ * branches on it, to avoid naming a hash that doesn't exist.
193895
+ */
193896
+ const headAssertion = (expectedHead) => {
193897
+ const q = shellQuote(expectedHead);
193898
+ const probe = `[ "$(git rev-parse --verify --quiet HEAD 2>/dev/null)" = ${q} ] || `;
193899
+ return expectedHead === "" ? probe + "{ printf 'gtd: repository changed since this script was generated (expected a repository with no commits yet) — re-run gtd\\n' >&2; exit 1; }" : probe + `{ printf 'gtd: repository changed since this script was generated (expected HEAD %s) — re-run gtd\\n' ${q} >&2; exit 1; }`;
193900
+ };
193901
+ /**
193902
+ * Same shape as `headAssertion`, for the saved-head ref a review window
193903
+ * pins. `--verify --quiet ... 2>/dev/null` makes a MISSING ref read as an
193904
+ * empty string rather than erroring the substitution itself — the assertion
193905
+ * only needs to compare, never to fail outright before the `[ ... ]` runs.
193906
+ */
193907
+ const reviewWindowAssertion = (ref, expectedHash) => {
193908
+ const refQ = shellQuote(ref);
193909
+ const hashQ = shellQuote(expectedHash);
193910
+ return `[ "$(git rev-parse --verify --quiet ${refQ} 2>/dev/null)" = ${hashQ} ] || { printf 'gtd: review window ref %s changed since this script was generated (expected %s) — re-run gtd\\n' ${refQ} ${hashQ} >&2; exit 1; }`;
193911
+ };
193912
+ /**
193913
+ * Wraps `command` so that a non-zero exit prints `prompt` (the ready-to-send
193914
+ * fix instruction), a blank line, then the command's own captured combined
193915
+ * output, before propagating the command's exit code — rather than letting
193916
+ * that output escape raw. The `{ … }` GROUP (not a bare command
193917
+ * substitution) is what lets a MULTI-LINE `command` be inlined verbatim
193918
+ * inside it; the assignment sits on the left of `||`, so `set -e` never trips
193919
+ * on the failing command itself. Exported (alongside `headAssertion`) solely
193920
+ * so `src/testing/EmittedScriptRecognizer.ts` can re-derive and
193921
+ * string-compare this exact shape.
193922
+ */
193923
+ const failurePromptWrapper = (command, prompt) => {
193924
+ return [
193925
+ `gtd_validate_status=0`,
193926
+ `gtd_validate_out="$( {`,
193927
+ command,
193928
+ `} 2>&1 )" || gtd_validate_status=$?`,
193929
+ `if [ "$gtd_validate_status" -ne 0 ]; then`,
193930
+ ` printf '%s\\n\\n%s\\n' ${shellQuote(prompt)} "$gtd_validate_out"`,
193931
+ ` exit "$gtd_validate_status"`,
193932
+ `fi`
193933
+ ].join("\n");
193934
+ };
193935
+ /**
193936
+ * `gtd_retry` takes ONE already-`shellQuote`d command string and `eval`s it,
193937
+ * mirroring `src/Git.ts`'s `withIndexLockRetry`: retry ONLY on the same two
193938
+ * substrings `isIndexLockError` matches, with jittered exponential backoff
193939
+ * (~10ms doubling, capped at 6 TOTAL attempts — the initial try plus 5
193940
+ * retries), and propagate any other failure on the very first attempt. Output
193941
+ * is captured combined (`2>&1`) so the discrimination can inspect it exactly
193942
+ * like `commitAllowEmpty`/`restoreStagedFrom` in `src/GitScript.ts` do for
193943
+ * their own single-shot retries — this is the same technique, generalized
193944
+ * into a loop. `awk` (not bash, which has no fractional `sleep` builtin)
193945
+ * turns the millisecond backoff into the fractional-second argument `sleep`
193946
+ * needs; `-v` passes the values in rather than interpolating them into the
193947
+ * awk program text, so a variable's value can never be mistaken for awk
193948
+ * syntax.
193949
+ */
193950
+ const RETRY_HELPER = [
193951
+ `gtd_retry() {`,
193952
+ ` local cmd=$1 attempt=1 delay_ms=10 out total_ms jitter_ms`,
193953
+ ` while true; do`,
193954
+ ` if out=$(eval "$cmd" 2>&1); then`,
193955
+ ` [ -n "$out" ] && printf '%s\\n' "$out"`,
193956
+ ` return 0`,
193957
+ ` fi`,
193958
+ ` case "$out" in`,
193959
+ ` *"index.lock"*|*"Another git process seems to be running"*) ;;`,
193960
+ ` *) printf '%s\\n' "$out" >&2; return 1 ;;`,
193961
+ ` esac`,
193962
+ ` if [ "$attempt" -ge 6 ]; then`,
193963
+ ` printf '%s\\n' "$out" >&2`,
193964
+ ` return 1`,
193965
+ ` fi`,
193966
+ ` jitter_ms=$(( RANDOM % delay_ms + 1 ))`,
193967
+ ` total_ms=$(( delay_ms + jitter_ms ))`,
193968
+ ` sleep "$(awk -v ms="$total_ms" 'BEGIN { printf "%.3f", ms / 1000 }')"`,
193969
+ ` delay_ms=$(( delay_ms * 2 ))`,
193970
+ ` attempt=$(( attempt + 1 ))`,
193971
+ ` done`,
193972
+ `}`
193973
+ ].join("\n");
193974
+ const renderStep = (step) => {
193975
+ if (step.kind === "gitWrite") return `gtd_retry ${shellQuote(step.command)}`;
193976
+ if (step.kind === "command" && step.onFailure !== void 0) return failurePromptWrapper(step.command, step.onFailure);
193977
+ return step.command;
193978
+ };
193979
+ const assembleScript = (preconditions, steps) => {
193980
+ if (steps.length === 0) return "";
193981
+ const sections = ["set -euo pipefail"];
193982
+ if (preconditions.expectedHead !== void 0) sections.push(headAssertion(preconditions.expectedHead));
193983
+ if (preconditions.reviewWindow) sections.push(reviewWindowAssertion(preconditions.reviewWindow.ref, preconditions.reviewWindow.expectedHash));
193984
+ if (steps.some((step) => step.kind === "gitWrite")) sections.push(RETRY_HELPER);
193985
+ if (steps.some((step) => step.kind === "outcome")) sections.push(OUTCOME_PREAMBLE);
193986
+ sections.push(...steps.map(renderStep));
193987
+ return sections.join("\n\n");
193988
+ };
193989
+ /**
193990
+ * Build both halves from already-rendered command strings. `required`
193991
+ * defaults to `[]` (nothing to do), `optional` to `[]` (no review window to
193992
+ * open) — either omitted argument, or an explicit empty array, produces the
193993
+ * fixed empty-string result `EmittedScripts` documents: a driver checking
193994
+ * `if [ -n "$required" ]` (or the JS equivalent) never has to special-case a
193995
+ * bare preamble with no body.
193996
+ */
193997
+ const emitScripts = (preconditions, required = [], optional = []) => ({
193998
+ required: assembleScript(preconditions, required),
193999
+ optional: assembleScript(preconditions, optional)
194000
+ });
194001
+ const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — pipe it into `bash` to land the turn";
194002
+ const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip";
194003
+ const PRESENTATION_FAILURE_WARNING = "echo \"gtd: presentation-only follow-up failed — continuing\" >&2";
194004
+ /**
194005
+ * A plain-text (no `--json`) write command's single pasteable script: `gtd
194006
+ * land | bash` lands the turn without ever separating `required` from
194007
+ * `optional`. `required` runs verbatim (its own `set -euo pipefail` aborts
194008
+ * before `optional` ever starts if it fails); `optional`, when non-empty, is
194009
+ * wrapped in a subshell whose failure is swallowed — presentation-only, so it
194010
+ * must never turn a landed turn into a non-zero exit. Empty when `required`
194011
+ * is — unreachable in practice today (every caller's `required` is always
194012
+ * non-empty, even a genuine no-op's print-only note), kept as the function's
194013
+ * own total behavior rather than assumed by every caller.
194014
+ */
194015
+ const combinedScript = (required, optional) => {
194016
+ if (required.length === 0) return "";
194017
+ if (optional.length === 0) return `${DID_NOT_RUN_COMMENT}\n\n${required}`;
194018
+ return [
194019
+ DID_NOT_RUN_COMMENT,
194020
+ "",
194021
+ required,
194022
+ "",
194023
+ PRESENTATION_ONLY_COMMENT,
194024
+ `(\n${optional}\n) || ${PRESENTATION_FAILURE_WARNING}`
194025
+ ].join("\n");
194026
+ };
193496
194027
  //#endregion
193497
194028
  //#region src/Edge.ts
193498
194029
  /**
@@ -193537,6 +194068,17 @@ const clearRetainedHistory = (git) => git.deleteRef(HISTORY_REF);
193537
194068
  * what `PatternMachine.step` consumes, despite having a single reader.
193538
194069
  */
193539
194070
  const EMPTY_TREE$2 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
194071
+ /**
194072
+ * The review checkout window's saved-head ref — duplicated from
194073
+ * `src/ReviewWindow.ts`'s own `REVIEW_HEAD_REF` rather than imported, because
194074
+ * `ReviewWindow.ts` already imports FROM this module (`currentRun`,
194075
+ * `reviewBaseFor`); importing it back here would be a real circular module
194076
+ * dependency. Mirrors `src/RetainedHistory.ts`'s own `HISTORY_REF` doc
194077
+ * comment on the same tradeoff. Read (never written) by `restAt`'s
194078
+ * window-aware branch and by the script preconditions below — this module
194079
+ * never opens or closes the window itself.
194080
+ */
194081
+ const REVIEW_HEAD_REF$1 = "refs/worktree/gtd/review-head";
193540
194082
  const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
193541
194083
  const COST_TRAILER_PREFIX = "Gtd-Cost: ";
193542
194084
  const COST_TRAILER_RE = /^Gtd-Cost:[ \t]*([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+?))?[ \t]*$/gm;
@@ -193564,8 +194106,8 @@ const parseCostTrailers = (messages) => {
193564
194106
  const REVIEW_BASE_TRAILER_PREFIX = "Gtd-Review-Base: ";
193565
194107
  const REVIEW_BASE_TRAILER_RE = /^Gtd-Review-Base:[ \t]*(\S+)[ \t]*$/m;
193566
194108
  /**
193567
- * The `Gtd-Review-Base: <hash>` trailer recorded on a `gtd step <actor>
193568
- * --entry <state>` entry commit (see `withEntryTrailers`), or `undefined`
194109
+ * The `Gtd-Review-Base: <hash>` trailer recorded on a `gtd --entry <state>`
194110
+ * entry commit (see `withEntryTrailers`), or `undefined`
193569
194111
  * when `message` carries none. Read back by `computeProcessRun` — ONLY off
193570
194112
  * the process's first (oldest) commit — to override the run's diff base.
193571
194113
  */
@@ -193654,12 +194196,33 @@ const resolveRestFrom = (def, headSubject) => {
193654
194196
  }
193655
194197
  };
193656
194198
  };
193657
- /** The working tree's pending changes vs HEAD, as the pattern grammar's `{status, path}[]`. */
193658
- const pendingChanges = (git) => git.changedPaths().pipe(map$4((entries) => entries.map((e) => ({
194199
+ /** The working tree's pending changes vs `base` (default HEAD), as the pattern grammar's `{status, path}[]`. */
194200
+ const pendingChanges = (git, base) => git.changedPaths(base).pipe(map$4((entries) => entries.map((e) => ({
193659
194201
  status: normalizeStatus(e.status),
193660
194202
  path: e.path
193661
194203
  }))));
193662
194204
  /**
194205
+ * `ProcessRun.headTurn` from an already-fetched `commitHistory` array
194206
+ * (oldest→newest) — HEAD is its last entry. `undefined` for an empty
194207
+ * history, an unparseable/foreign subject, or a subject naming a state the
194208
+ * ACTIVE definition doesn't declare (removed by a workflow change) or that
194209
+ * IS a commit state (never a real rest — see `resolveState`'s own doc
194210
+ * comment on why a commit-state subject is never trusted at face value).
194211
+ */
194212
+ const headTurnFrom = (def, history) => {
194213
+ if (history.length === 0) return void 0;
194214
+ const head = history[history.length - 1];
194215
+ const parsed = parseStateSubject(subjectOf(head.message));
194216
+ if (parsed === void 0) return void 0;
194217
+ const stateDef = def.states[parsed.state];
194218
+ if (stateDef === void 0 || isCommitState(stateDef)) return void 0;
194219
+ return {
194220
+ state: parsed.state,
194221
+ actor: parsed.actor,
194222
+ empty: head.touched.length === 0
194223
+ };
194224
+ };
194225
+ /**
193663
194226
  * The `Gtd-Review-Base:`/`Gtd-Var:` overrides carried by the process's OLDEST
193664
194227
  * commit (its entry commit, when `planEntry` started this process) — a later
193665
194228
  * turn's message is never mistaken for it. `{reviewBase: undefined, vars:
@@ -193696,19 +194259,27 @@ const parseEntryCommitOverrides = (processCommits) => {
193696
194259
  * same shape a fresh rest at a squashed boundary has always had.
193697
194260
  *
193698
194261
  * `hasCommits` is supplied by the caller (already read once — see the module
193699
- * doc comment's stage ordering) rather than re-read here.
193700
- */
193701
- const computeProcessRun = (git, def, hasCommits) => gen(function* () {
194262
+ * doc comment's stage ordering) rather than re-read here. `head` — a resolved
194263
+ * hash, never a symbolic ref — overrides the literal `HEAD` the walk would
194264
+ * otherwise end at: `restAt`'s window-aware branch passes the review
194265
+ * checkout window's saved-head hash here while the window is open, so the
194266
+ * trace still covers the commits a real `git log` would miss with HEAD
194267
+ * rewound to the review base. `undefined` (every other caller) is today's
194268
+ * exact behavior — an explicit trailing ref is passed to `git.commitHistory`
194269
+ * only when `head` differs from the literal `"HEAD"` it already defaults to.
194270
+ */
194271
+ const computeProcessRun = (git, def, hasCommits, head) => gen(function* () {
193702
194272
  if (!hasCommits) return {
193703
194273
  startHash: "",
193704
194274
  startParentHash: EMPTY_TREE$2,
193705
194275
  diffBase: EMPTY_TREE$2,
193706
194276
  trace: [],
193707
194277
  costEntries: [],
193708
- entryVars: {}
194278
+ entryVars: {},
194279
+ headTurn: void 0
193709
194280
  };
193710
194281
  const initialState = initialStateOf(def);
193711
- const history = yield* git.commitHistory();
194282
+ const history = yield* git.commitHistory(void 0, head);
193712
194283
  let i = history.length - 1;
193713
194284
  while (i >= 0) {
193714
194285
  const parsed = parseStateSubject(subjectOf(history[i].message));
@@ -193731,7 +194302,8 @@ const computeProcessRun = (git, def, hasCommits) => gen(function* () {
193731
194302
  diffBase: reviewBaseOverride ?? startParentHash,
193732
194303
  trace,
193733
194304
  costEntries,
193734
- entryVars
194305
+ entryVars,
194306
+ headTurn: headTurnFrom(def, history)
193735
194307
  };
193736
194308
  });
193737
194309
  /**
@@ -193740,12 +194312,19 @@ const computeProcessRun = (git, def, hasCommits) => gen(function* () {
193740
194312
  * since abandon IS the recovery command for that case) and the review
193741
194313
  * window's re-arm (`openReviewWindow` stays graceful there too — see its own
193742
194314
  * doc comment).
194315
+ *
194316
+ * Window-aware on exactly the same terms as `restAt`: while a review checkout
194317
+ * window is open, real HEAD is rewound to the review base, so walking the
194318
+ * trace from literal HEAD would see a process that has not started. Both
194319
+ * callers need the REAL head — abandon rewinds the reviewed branch tip, the
194320
+ * re-arm re-derives the review base — so the saved head is the walk's origin.
193743
194321
  */
193744
194322
  const currentRun = gen(function* () {
193745
194323
  const git = yield* GitService;
193746
194324
  const config = yield* (yield* ConfigService).load;
193747
194325
  const hasCommits = yield* git.hasCommits();
193748
- return yield* computeProcessRun(git, config.workflow, hasCommits);
194326
+ const windowHead = getOrUndefined(yield* git.readRefOption(REVIEW_HEAD_REF$1));
194327
+ return yield* computeProcessRun(git, config.workflow, hasCommits, windowHead);
193749
194328
  });
193750
194329
  /**
193751
194330
  * PURE: the most-recent in-process turn commit that entered a `reviewBase`
@@ -193793,6 +194372,35 @@ const memoryKeyFor = (scopes, rest, run) => {
193793
194372
  const token = entryIndex <= 0 ? run.startParentHash : run.trace[entryIndex - 1].hash;
193794
194373
  return `${scope || ROOT_MEMORY_SCOPE_NAME}#${token.slice(0, 7)}`;
193795
194374
  };
194375
+ /**
194376
+ * True iff a `prompt` rest in the CURRENT unbroken scope-run has already been
194377
+ * passed — the derivation `src/Sessions.ts`'s `resolveSession` takes its
194378
+ * `resume` flag from. `false` for any non-`prompt` rest, or when `rest.state`
194379
+ * resolves no scope at all (mirrors `memoryKeyFor`'s own two escape hatches).
194380
+ *
194381
+ * Computed over the process's rests, oldest→newest, with the process's own
194382
+ * starting state (`initialStateOf(def)`) prefixed: `run.trace`'s last entry is
194383
+ * already the current rest (a `Rest` is a snapshot of a fully-resolved state,
194384
+ * never an in-flight turn), so prefixing the ONE state before the trace began
194385
+ * is all that's missing. The prefix matters for a workflow whose
194386
+ * `entries.default` IS a prompt state — without it, the very first turn there
194387
+ * would have no predecessor to look back at.
194388
+ *
194389
+ * The root scope (`scope === ""`) matches every state, so a workflow like
194390
+ * `idle → working` puts `idle` inside `working`'s scope-run — the very first
194391
+ * agent beat of a process would otherwise claim `resume: true` for an id
194392
+ * nobody ever created. Filtering the "prior rests" slice down to `prompt`-kind
194393
+ * states (not merely non-empty) closes that hole: a message state like `idle`
194394
+ * never counts as a prior conversation turn.
194395
+ */
194396
+ const memoryResumedFor = (def, scopes, rest, run) => {
194397
+ if (contentKindOf(rest.stateDef) !== "prompt") return false;
194398
+ const rests = [initialStateOf(def), ...run.trace.map((entry) => entry.state)];
194399
+ const resolved = memoryScopeAt(scopes, rest.state, rests);
194400
+ if (resolved === void 0) return false;
194401
+ const { entryIndex } = resolved;
194402
+ return rests.slice(Math.max(entryIndex, 0), rests.length - 1).some((state) => contentKindOf(def.states[state]) === "prompt");
194403
+ };
193796
194404
  const PREFIX = "GTD_";
193797
194405
  /**
193798
194406
  * Assemble the merged `it.vars` map every template sees, from four layers
@@ -193800,7 +194408,7 @@ const PREFIX = "GTD_";
193800
194408
  * (`ConfigOperations.workflowVars`), the top-level `.gtdrc` `vars:` key
193801
194409
  * (`ConfigOperations.rcVars`), the current process's entry commit's
193802
194410
  * `Gtd-Var:` trailers (`ProcessRun.entryVars` — fixed overrides recorded at
193803
- * the moment a process like `gtd step <actor> --entry <state>` started it),
194411
+ * the moment a process like `gtd --entry <state>` started it),
193804
194412
  * and — for each name declared by any of those three layers — a
193805
194413
  * `GTD_<UPPERCASE-name>` environment variable, if defined. Unlike the first
193806
194414
  * three layers, the environment can only OVERRIDE a name some earlier layer
@@ -193941,6 +194549,20 @@ const omitUndefined = (obj) => Object.fromEntries(Object.entries(obj).filter(([,
193941
194549
  * window's saved head via `REVIEW_HEAD_REF`, or `undefined` for HEAD itself —
193942
194550
  * see `currentRest`). Assembles every stage documented in the module doc
193943
194551
  * comment, threading one `hasCommits()` read through stages 3/5/11.
194552
+ *
194553
+ * Window-aware ONLY at `ref === undefined` (`currentRest`'s own case): when
194554
+ * the review checkout window's saved-head ref (`REVIEW_HEAD_REF`) resolves,
194555
+ * its hash is used as the effective head for `lastCommitSubject` AND
194556
+ * `computeProcessRun`'s trace walk — real git HEAD has been rewound to the
194557
+ * review base while the window is open, so reading literal `HEAD` there
194558
+ * would resolve state/trace/memory against the wrong commit. An explicit
194559
+ * `ref` (`gtd visualize`'s own call pattern) never triggers this branch —
194560
+ * that path resolves exactly as it always has. Safe to land now because
194561
+ * every existing command closes the window (`program.ts`'s
194562
+ * `runInReviewWindowBracket`) before `restAt`/`currentRest` ever runs, so the
194563
+ * saved-head ref is already gone by the time this branch could fire in
194564
+ * production — see the two direct tests in `Edge.test.ts` for the only
194565
+ * coverage today.
193944
194566
  */
193945
194567
  const restAt = (ref) => gen(function* () {
193946
194568
  const git = yield* GitService;
@@ -193949,18 +194571,22 @@ const restAt = (ref) => gen(function* () {
193949
194571
  const envVars = yield* EnvVars;
193950
194572
  const def = config.workflow;
193951
194573
  const hasCommits = yield* git.hasCommits();
193952
- const headSubject = hasCommits ? yield* git.lastCommitSubject(ref) : "";
194574
+ let windowHead;
194575
+ if (ref === void 0) windowHead = getOrUndefined(yield* git.readRefOption(REVIEW_HEAD_REF$1));
194576
+ const effectiveRef = windowHead ?? ref;
194577
+ const headSubject = hasCommits ? yield* git.lastCommitSubject(effectiveRef) : "";
193953
194578
  const resolution = resolveRestFrom(def, headSubject);
193954
194579
  if (!resolution.ok) return yield* fail$2(resolution.error);
193955
194580
  const resolved = resolution.rest;
193956
- const run = yield* computeProcessRun(git, def, hasCommits);
194581
+ const run = yield* computeProcessRun(git, def, hasCommits, windowHead);
193957
194582
  const vars = resolveVars(config.workflowVars, config.rcVars, run.entryVars, envVars.all);
193958
194583
  const on = yield* renderOnEdgesOrFail(resolved.stateDef.on, vars);
193959
194584
  const stepDef = withRenderedOn(def, resolved.state, on);
193960
194585
  const reviewBase = reviewBaseFor(def, run);
193961
- const changes = yield* pendingChanges(git);
194586
+ const changes = yield* pendingChanges(git, windowHead);
193962
194587
  const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase, hasCommits);
193963
194588
  const memory = memoryKeyFor(config.stateScopes, resolved, run);
194589
+ const memoryResumed = memoryResumedFor(def, config.stateScopes, resolved, run);
193964
194590
  const hints = yield* renderHints(resolved.stateDef, context);
193965
194591
  return {
193966
194592
  ...resolved,
@@ -193970,6 +194596,7 @@ const restAt = (ref) => gen(function* () {
193970
194596
  stepDef,
193971
194597
  changes,
193972
194598
  memory,
194599
+ memoryResumed,
193973
194600
  hints,
193974
194601
  context
193975
194602
  };
@@ -194028,17 +194655,48 @@ const renderRest = (rest) => gen(function* () {
194028
194655
  content,
194029
194656
  ...rest.hints,
194030
194657
  ...rest.memory !== void 0 ? { memory: rest.memory } : {},
194658
+ memoryResumed: rest.memoryResumed,
194031
194659
  edges: rest.context.edges
194032
194660
  };
194033
194661
  });
194034
- /** The user-facing message for a `step` refusal — out-of-turn names the awaited actor, no-match names every declared pattern. */
194035
- const formatStepRefusal = (invoker, refusal) => refusal.reason === "out-of-turn" ? `gtd step ${invoker}: out of turn — "${refusal.state}" awaits ${refusal.awaits}` : `gtd step ${invoker}: no declared pattern matches the pending changes at "${refusal.state}" — declared patterns: ${refusal.patterns.length > 0 ? refusal.patterns.join(", ") : "(none)"}`;
194662
+ /**
194663
+ * Derived stall detection: PURE, no `Effect` — a plain fold over `rest`'s
194664
+ * already-resolved snapshot, restart-proof by construction (any process that
194665
+ * re-resolves the same HEAD reaches the same verdict). `true` iff ALL of:
194666
+ *
194667
+ * - the working tree is clean (a dirty tree is always an in-progress turn,
194668
+ * never a stall — this is the recovery story: a human fixing the cause out
194669
+ * of band clears the stall by touching the repo);
194670
+ * - HEAD's own commit already rests AT `rest.state` (`run.headTurn.state ===
194671
+ * rest.state` — the previous dispatch's attempt landed here) and that
194672
+ * commit's diff was EMPTY (`run.headTurn.empty`);
194673
+ * - ANOTHER dispatch right now would just repeat the same fruitless attempt
194674
+ * (`wouldAttempt` — false once a retry cap would redirect it elsewhere
194675
+ * instead, which is the escalation path OUT of a stall).
194676
+ *
194677
+ * Sticky by design (decision 4): resolving `true` twice in a row is the
194678
+ * correct, restart-proof answer, not a bug — the marker it replaces used to
194679
+ * consume itself on report; this doesn't, because there is no longer any
194680
+ * mutation to gate a "one-shot" report on.
194681
+ */
194682
+ const stalledAt = (rest) => rest.changes.length === 0 && rest.run.headTurn?.state === rest.state && rest.run.headTurn.empty && rest.run.headTurn.actor === rest.actor && wouldAttempt(rest.stepDef, rest.state, rest.run.trace.map((entry) => entry.state));
194683
+ /**
194684
+ * The user-facing message for a `land` refusal — out-of-turn names the
194685
+ * awaited actor, no-match names every declared pattern. `land` derives its
194686
+ * invoker from `rest.actor` itself (see `planStep`), so out-of-turn is
194687
+ * unreachable by construction there; this branch stays reachable only via
194688
+ * `PatternMachine.step`'s own tests (a defensive message beats a lie).
194689
+ */
194690
+ const formatStepRefusal = (refusal) => refusal.reason === "out-of-turn" ? `gtd land: out of turn — "${refusal.state}" awaits ${refusal.awaits}` : `gtd land: no declared pattern matches the pending changes at "${refusal.state}" — declared patterns: ${refusal.patterns.length > 0 ? refusal.patterns.join(", ") : "(none)"}`;
194036
194691
  /**
194037
194692
  * Build the `TemplateContext` for rendering a DIFFERENT state than `rest`'s
194038
- * own resting one, with a step's `cost`/`model` folded in — `planStep`'s
194039
- * squash branch is its only caller (the commit branch never reads a
194040
- * `TemplateContext` — see `executeDecision`), so this stays unexported per
194041
- * the module's two-caller rule.
194693
+ * own resting one, with a step's `cost`/`model` folded in. A squash's commit
194694
+ * template is the only thing that needs it (the commit branch never reads a
194695
+ * `TemplateContext` at all), and it is NOT interchangeable with `rest.context`:
194696
+ * that one is pinned to the resting state and carries `cost: 0`, so rendering
194697
+ * `it.processCost` against it silently omits the squashing step's own
194698
+ * `--cost`. Exported for `program.ts`'s `planLanding`, which assembles the
194699
+ * same script by hand and must pick the same context.
194042
194700
  */
194043
194701
  const contextAt = (rest, targetState, invoker, cost, model) => gen(function* () {
194044
194702
  const git = yield* GitService;
@@ -194048,98 +194706,220 @@ const contextAt = (rest, targetState, invoker, cost, model) => gen(function* ()
194048
194706
  return yield* buildTemplateContext(git, templateRead(files), targetState, invoker, rest.run, rest.vars, onEdges, cost ?? 0, model, reviewBaseFor(rest.def, rest.run), hasCommits);
194049
194707
  });
194050
194708
  /**
194051
- * Execute a `"commit"`/`"squash"` decision: a `"commit"` decision stages and
194052
- * commits everything pending under the decided subject, with an optional
194053
- * `Gtd-Cost: <cost>[ <model>]` trailer; a `"squash"` decision renders the
194054
- * commit-state template against the PENDING tree — a render failure REFUSES
194055
- * the step, touching nothing — then records the pre-squash tip on the
194056
- * retained-history ref (`retainHistory`, a no-op for an empty process) before
194057
- * soft-resetting to the process's start parent, writes ONE commit with the
194058
- * rendered message plus a `Gtd-History: <hash>` trailer pointing at that tip,
194059
- * and discards everything left pending (the template file included). The
194060
- * whole-process total and per-model breakdown reach the message through
194061
- * `it.processCost`/`it.processCostByModel` in the rendered template.
194062
- */
194063
- const executeDecision = (git, run, decision, context, cost, model) => gen(function* () {
194709
+ * Render a `"commit"`/`"squash"` decision as the `EmitStep`s the external
194710
+ * driver runs to produce its git effect — the ONE place a decision becomes
194711
+ * git commands. Every git call here is a READ (`git.resolveRef("HEAD")` for
194712
+ * the squash branch's pre-squash tip); nothing is written — gtd itself never
194713
+ * writes git, only a driven script does.
194714
+ *
194715
+ * The squash branch renders the commit-state template against the PENDING
194716
+ * tree — a render failure fails this Effect, touching no git. On success it
194717
+ * emits, in order: retain-history (`updateRef(HISTORY_REF, tip)`, OMITTED
194718
+ * when `tip === run.startParentHash` — an empty process retains nothing,
194719
+ * mirroring `retainHistory`'s no-op check, decided here since `tip` is
194720
+ * already known), soft-reset to the process start, commit-as-is with the
194721
+ * rendered message plus its `Gtd-History:` trailer, discard-pending, then a
194722
+ * trailing `commitOutcome` naming the rendered message's bare subject line —
194723
+ * the script's own report, printed as it runs.
194724
+ *
194725
+ * The commit branch carries one non-obvious case too: a
194726
+ * commit whose target is the workflow's INITIAL state, from a process that
194727
+ * retained nothing, is a no-op probe (`gtd --entry fix-precheck` against a
194728
+ * green suite is the shipped example) and must leave no trace at all. It
194729
+ * emits retain-history + a mixed reset to the process start instead of a
194730
+ * commit, so the entry commit and the probe collapse away together rather
194731
+ * than dirtying the log with a round trip to `idle` — this COLLAPSE case
194732
+ * prints `COLLAPSED_TEXT` via a `gtd_report_note`, never a `gtd_report_commit`:
194733
+ * no commit lands, so there is nothing to report by reading `HEAD`'s files —
194734
+ * that read would list the pre-process commit's unrelated files instead.
194735
+ * Deciding it needs the whole `rest` (its definition for the initial state,
194736
+ * its pending changes for the "retained nothing" test), which is why this
194737
+ * takes a `Rest` rather than the `ProcessRun` alone. The ordinary commit
194738
+ * path's own trailing outcome is a `transitionOutcome` when the decided
194739
+ * subject moves to a DIFFERENT state than it started from, else a
194740
+ * `commitOutcome` naming the bare (never cost-trailer'd) subject.
194741
+ */
194742
+ /** `updateRef(HISTORY_REF, tip)` as a single-element (or empty) step list — OMITTED when `tip === startParentHash`, mirroring `retainHistory`'s own no-op check. Shared by the squash branch and the commit branch's collapse case below. */
194743
+ const retainHistoryStep = (tip, startParentHash) => tip === startParentHash ? [] : [{
194744
+ kind: "gitWrite",
194745
+ command: updateRef(HISTORY_REF, tip)
194746
+ }];
194747
+ /** The commit branch's own trailing outcome: a bare `commitOutcome` for a self-loop (`from === to`), else a `transitionOutcome` naming both states. */
194748
+ const commitDecisionOutcome = (decision) => ({
194749
+ kind: "outcome",
194750
+ command: decision.from === decision.to ? commitOutcome(decision.subject) : transitionOutcome(decision.from, decision.to)
194751
+ });
194752
+ /**
194753
+ * True for a `"commit"` decision that is the "green re-entry into the initial
194754
+ * state retaining nothing" collapse — the process's target is the workflow's
194755
+ * initial state, it started somewhere other than the empty tree, and nothing
194756
+ * pending or already-committed since its start parent survives
194757
+ * (`retainsNothing`). Always `false` for a `"squash"` decision (a squash's
194758
+ * target is never the initial state by construction) and for an ATTEMPT
194759
+ * (`decision.attempt === true`, per decision 5): an agent that did nothing
194760
+ * must never rewind a process — the collapse is for a green re-entry that
194761
+ * retained nothing, not for a fruitless turn, and an attempt landing at the
194762
+ * initial state (a prompt state that also happens to be where the workflow
194763
+ * starts) would otherwise satisfy every OTHER criterion here identically to a
194764
+ * genuine collapse. The ONE predicate `renderDecision`'s emitted-script
194765
+ * branch calls — `program.ts`'s `planLanding` asks the same question,
194766
+ * through the exported `collapsesToInitialState` below, at the same moment,
194767
+ * so the two can never decide differently about the same run.
194768
+ */
194769
+ const collapsesWith = (git, rest, decision) => gen(function* () {
194770
+ if (decision.kind === "squash" || decision.attempt === true) return false;
194771
+ return parseStateSubject(decision.subject)?.state === initialStateOf(rest.def) && rest.run.startParentHash !== EMPTY_TREE$2 && (yield* retainsNothing(git, rest.run, rest.changes));
194772
+ });
194773
+ /**
194774
+ * The service-requiring twin of `collapsesWith`, for `program.ts` to ask
194775
+ * instead of reaching into `GitService` itself — the boundary AGENTS.md pins.
194776
+ * `planLanding` calls this to fill `LandResult.settled` for a `"commit"`/
194777
+ * `"squash"` plan, off the same `rest`/`decision` `renderDecision` already
194778
+ * decided against, so the two git reads (`changedPathsSince` + `resolveRef`)
194779
+ * can never disagree with each other.
194780
+ */
194781
+ const collapsesToInitialState = (rest, decision) => gen(function* () {
194782
+ const git = yield* GitService;
194783
+ return yield* collapsesWith(git, rest, decision);
194784
+ });
194785
+ const renderDecision = (git, rest, decision, context, cost, model) => gen(function* () {
194786
+ const run = rest.run;
194064
194787
  switch (decision.kind) {
194065
194788
  case "commit":
194066
- yield* git.commitAllWithPrefix(withCostTrailer(decision.subject, cost, model));
194067
- return {
194068
- kind: "commit",
194069
- subject: decision.subject
194070
- };
194789
+ if (yield* collapsesWith(git, rest, decision)) {
194790
+ const tip = yield* git.resolveRef("HEAD");
194791
+ return [
194792
+ ...retainHistoryStep(tip, run.startParentHash),
194793
+ {
194794
+ kind: "gitWrite",
194795
+ command: mixedResetTo(run.startParentHash)
194796
+ },
194797
+ {
194798
+ kind: "outcome",
194799
+ command: noteOutcome(COLLAPSED_TEXT)
194800
+ }
194801
+ ];
194802
+ }
194803
+ return [{
194804
+ kind: "gitWrite",
194805
+ command: commitAll(withCostTrailer(decision.subject, cost, model))
194806
+ }, commitDecisionOutcome(decision)];
194071
194807
  case "squash": {
194072
194808
  const message = yield* try_({
194073
194809
  try: () => renderStateTemplate(decision.template, context),
194074
194810
  catch: (e) => /* @__PURE__ */ new Error(`gtd: rendering the "${decision.state}" commit template failed — nothing was committed: ${e instanceof Error ? e.message : String(e)}`)
194075
194811
  });
194076
194812
  const tip = yield* git.resolveRef("HEAD");
194077
- yield* retainHistory(git, tip, run.startParentHash);
194078
- yield* git.softResetTo(run.startParentHash);
194079
- yield* git.commitAsIs(withHistoryTrailer(message, tip));
194080
- yield* git.discardPending();
194081
- return {
194082
- kind: "squash",
194083
- subject: subjectOf(message)
194084
- };
194813
+ return [
194814
+ ...retainHistoryStep(tip, run.startParentHash),
194815
+ {
194816
+ kind: "gitWrite",
194817
+ command: softResetTo(run.startParentHash)
194818
+ },
194819
+ {
194820
+ kind: "gitWrite",
194821
+ command: commitAsIs(withHistoryTrailer(message, tip))
194822
+ },
194823
+ {
194824
+ kind: "gitWrite",
194825
+ command: discardPending()
194826
+ },
194827
+ {
194828
+ kind: "outcome",
194829
+ command: commitOutcome(subjectOf(message))
194830
+ }
194831
+ ];
194085
194832
  }
194086
194833
  }
194087
194834
  });
194088
194835
  /**
194089
- * Decide what invoking `invoker` at `rest` does (`PatternMachine.step` over
194090
- * `rest.stepDef`/`rest.changes`/`rest.run.trace`), and — for a `"commit"`/
194091
- * `"squash"` decision — build the `perform` Effect that actually writes it.
194092
- * `opts.cost`/`opts.model` ride along for the `Gtd-Cost:` trailer and the
194093
- * squash template's folded `it.processCost`/`it.processCostByModel`.
194094
- *
194095
- * `perform` itself, in order: the "returned to the initial state retaining
194096
- * nothing" mixed-reset branch (a green re-entry into the initial state that
194097
- * kept nothing useful is rewound like `gtd abandon` rather than committed),
194098
- * else `executeDecision` — against `rest.context` for a commit (which never
194099
- * reads it) or a freshly built `contextAt` for a squash (which renders a
194100
- * DIFFERENT state's template with the step's cost folded in).
194101
- */
194102
- const planStep = (rest, invoker, opts = {}) => {
194103
- const decision = step(rest.stepDef, rest.state, invoker, {
194836
+ * The `EmitPreconditions` a step's/entry's assembled scripts assert against:
194837
+ * `expectedHead` is the resolved HEAD hash the `Rest` snapshot was already
194838
+ * taken at — `rest.context.currentCommit`, passed straight through, `""`
194839
+ * included (its own convention for "no commits yet", which `headAssertion`
194840
+ * now understands natively). When `targetState` declares `reviewWindow: true`
194841
+ * AND a window is currently open (`REVIEW_HEAD_REF` resolves), the script also
194842
+ * pins the window's saved-head hash — a later run whose window has since
194843
+ * closed or moved must re-decide rather than trust a stale script. No window,
194844
+ * or a non-review-window target, carries no `reviewWindow` precondition at
194845
+ * all.
194846
+ */
194847
+ const buildPreconditions = (git, rest, targetState) => gen(function* () {
194848
+ const expectedHead = rest.context.currentCommit;
194849
+ if (!isReviewWindowState(rest.def, targetState)) return { expectedHead };
194850
+ const windowHead = yield* git.readRefOption(REVIEW_HEAD_REF$1);
194851
+ if (isNone(windowHead)) return { expectedHead };
194852
+ return {
194853
+ expectedHead,
194854
+ reviewWindow: {
194855
+ ref: REVIEW_HEAD_REF$1,
194856
+ expectedHash: windowHead.value
194857
+ }
194858
+ };
194859
+ });
194860
+ /**
194861
+ * The `EmittedScripts` a `"commit"`/`"squash"` `StepPlan` carries alongside
194862
+ * `perform` — built from the SAME `renderDecision` output, but a render
194863
+ * failure collapses to an EMPTY script (`emitScripts` with no steps) instead
194864
+ * of failing this Effect: `perform` stays the one place a render failure is
194865
+ * ever reported to a caller, unchanged by this addition.
194866
+ */
194867
+ const buildStepScripts = (git, rest, decision, context, cost, model) => gen(function* () {
194868
+ const targetState = decision.kind === "commit" ? decision.to : decision.state;
194869
+ return emitScripts(yield* buildPreconditions(git, rest, targetState), yield* renderDecision(git, rest, decision, context, cost, model).pipe(catchAll(() => succeed$3([]))));
194870
+ });
194871
+ /**
194872
+ * A no-op is TERMINAL only at a `script` rest: gtd rendered the script, the
194873
+ * driver ran it, it left nothing any pattern claims, and re-running it can't
194874
+ * change that — the loop should exit rather than spin (`gtd land`'s exit-3
194875
+ * `settled` signal). A `prompt` rest can no longer produce a no-op at all (a
194876
+ * clean tree with no `C` row there is an ATTEMPT commit instead — see
194877
+ * `PatternMachine.step`'s doc comment — and `stalledAt` is its own, separately
194878
+ * reported signal). A no-op at a `message` rest is a human gate the loop
194879
+ * already halts on (`kind: "message"`), so it needs no signal either. This is
194880
+ * one of TWO settled shapes — the other is the initial-state collapse
194881
+ * (`collapsesWith`/`collapsesToInitialState`), decided independently by
194882
+ * `program.ts`'s `planLanding` for a `"commit"`/`"squash"` plan.
194883
+ */
194884
+ const noOpSettles = (rest) => contentKindOf(rest.stateDef) === "script";
194885
+ /**
194886
+ * Decide what landing at `rest` does (`PatternMachine.step` over
194887
+ * `rest.stepDef`/`rest.changes`/`rest.run.trace`, authenticated as
194888
+ * `rest.actor` — the state's own declared actor, so out-of-turn is
194889
+ * unreachable by construction), and — for a `"commit"`/`"squash"` decision —
194890
+ * assemble the emitted `scripts` a driver runs to land it (against
194891
+ * `rest.context` for a commit, or a freshly built `contextAt` for a squash,
194892
+ * which renders a DIFFERENT state's template with the step's cost folded
194893
+ * in). `opts.cost`/`opts.model` ride along for the `Gtd-Cost:` trailer and
194894
+ * the squash template's folded `it.processCost`/`it.processCostByModel`.
194895
+ */
194896
+ const planStep = (rest, opts = {}) => gen(function* () {
194897
+ const decision = step(rest.stepDef, rest.state, rest.actor, {
194104
194898
  changes: rest.changes,
194105
194899
  processTrace: rest.run.trace.map((entry) => entry.state)
194106
194900
  });
194107
- if (decision.kind === "refusal") return succeed$3({
194901
+ if (decision.kind === "refusal") return {
194108
194902
  kind: "refusal",
194109
- message: formatStepRefusal(invoker, decision)
194110
- });
194111
- if (decision.kind === "noop") return succeed$3({
194903
+ message: formatStepRefusal(decision)
194904
+ };
194905
+ if (decision.kind === "noop") return {
194112
194906
  kind: "noop",
194113
- state: decision.state
194114
- });
194907
+ state: decision.state,
194908
+ settled: noOpSettles(rest)
194909
+ };
194115
194910
  const { cost, model } = opts;
194116
- const perform = gen(function* () {
194117
- const git = yield* GitService;
194118
- if (decision.kind === "commit") {
194119
- const target = parseStateSubject(decision.subject)?.state;
194120
- if (target === initialStateOf(rest.def) && rest.run.startParentHash !== EMPTY_TREE$2 && (yield* retainsNothing(git, rest.run, rest.changes))) {
194121
- yield* retainHistory(git, yield* git.resolveRef("HEAD"), rest.run.startParentHash);
194122
- yield* git.mixedResetTo(rest.run.startParentHash);
194123
- return {
194124
- kind: "reset",
194125
- state: target
194126
- };
194127
- }
194128
- return yield* executeDecision(git, rest.run, decision, rest.context, cost, model);
194129
- }
194130
- const context = yield* contextAt(rest, decision.state, invoker, cost, model);
194131
- return yield* executeDecision(git, rest.run, decision, context, cost, model);
194132
- });
194133
- return succeed$3({
194911
+ const git = yield* GitService;
194912
+ const scriptContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, rest.actor, cost, model);
194913
+ const scripts = yield* buildStepScripts(git, rest, decision, scriptContext, cost, model);
194914
+ return {
194134
194915
  kind: decision.kind,
194135
194916
  state: rest.state,
194136
194917
  decision,
194137
- perform
194138
- });
194139
- };
194918
+ scripts
194919
+ };
194920
+ });
194140
194921
  /**
194141
- * `gtd step <actor> --entry <state>` (or its subcommand-less short form `gtd
194142
- * --entry <state>`): start a brand NEW process at `entry.state` — any
194922
+ * `gtd --entry <state>`: start a brand NEW process at `entry.state` — any
194143
194923
  * declared, non-commit state (see `enterableStates`) — writing an ordinary
194144
194924
  * turn commit (`gtd(<actor>): <state>`) carrying zero or more `Gtd-Var:
194145
194925
  * <name>=<value>` trailers for each `entry.vars` override, plus — when
@@ -194147,7 +194927,7 @@ const planStep = (rest, invoker, opts = {}) => {
194147
194927
  * trailer pinning the new process's diff base (rendered from that template
194148
194928
  * against the merged `it.vars`, resolved to a commit, and checked sane).
194149
194929
  * Commits via `commitAllWithPrefix` — capturing whatever the working tree
194150
- * carries at the moment of entry, exactly like an ordinary step capture,
194930
+ * carries at the moment of entry, exactly like an ordinary land capture,
194151
194931
  * rather than demanding a clean tree first.
194152
194932
  *
194153
194933
  * All validation is a REFUSAL, not an Effect failure — checked in order:
@@ -194220,18 +195000,18 @@ const planEntry = (rest, actor, entry) => gen(function* () {
194220
195000
  ...base !== void 0 ? { base } : {},
194221
195001
  vars: varOverrides
194222
195002
  });
195003
+ const scriptsGit = yield* GitService;
194223
195004
  return {
194224
195005
  kind: "entry",
194225
195006
  state: entryState,
194226
195007
  subject,
194227
- perform: gen(function* () {
194228
- yield* (yield* GitService).commitAllWithPrefix(message);
194229
- return {
194230
- kind: "entry",
194231
- state: entryState,
194232
- subject
194233
- };
194234
- })
195008
+ scripts: emitScripts(yield* buildPreconditions(scriptsGit, rest, entryState), [{
195009
+ kind: "gitWrite",
195010
+ command: commitAll(message)
195011
+ }, {
195012
+ kind: "outcome",
195013
+ command: commitOutcome(subject)
195014
+ }])
194235
195015
  };
194236
195016
  });
194237
195017
  //#endregion
@@ -194249,24 +195029,37 @@ const planEntry = (rest, actor, entry) => gen(function* () {
194249
195029
  * `PatternMachine.StateDef`) — the pure engine never observes an open window,
194250
195030
  * exactly as before.
194251
195031
  *
194252
- * Lifecycle — driven from the program edge (`src/program.ts`), bracketing
194253
- * every state subcommand:
194254
- *
194255
- * - `closeReviewWindow` runs BEFORE anything reads or mutates state, keyed
194256
- * solely on `REVIEW_HEAD_REF` existing: `git reset --mixed <that ref>`
194257
- * restores HEAD/index exactly, leaving only the reviewer's own edits dirty
194258
- * (captured by the resting state's own `on` patterns like any other pending
194259
- * change). The pure machine therefore never sees the window.
194260
- * - `openReviewWindow` runs AFTER the subcommand finishes and self-guards on
194261
- * the resolved rest declaring `reviewWindow: true`: it saves HEAD to
194262
- * `REVIEW_HEAD_REF` (the base to `REVIEW_BASE_REF`), then `git reset --mixed
194263
- * <base>`. It re-arms after read-only commands (`gtd next` / `gtd status`)
194264
- * and refused invocations too, so the editor's diff view stays consistent no
194265
- * matter which command the loop last ran.
194266
- *
194267
- * Every open/close step is idempotent under re-entry, so a crash at any point
194268
- * is recovered by the next invocation's close (the saved ref also keeps the
194269
- * real head GC-reachable for the window's whole lifetime).
195032
+ * Lifecycle — driven from the program edge (`src/program.ts`), which DECIDES
195033
+ * with this module's pure/read-only halves and assembles the actual writes
195034
+ * into the two scripts a `gtd` command hands the external driver:
195035
+ *
195036
+ * - The CLOSE sequence (`decideCloseWindow` + `buildCloseWindowScript`) is
195037
+ * read-only in this module — it decides whether a window is open and safe
195038
+ * to close, keyed solely on `REVIEW_HEAD_REF` existing (the same
195039
+ * reviewed-branch/legacy-containment guards described below) — and
195040
+ * `program.ts`'s `buildRequiredScript` splices the resulting
195041
+ * `mixedResetTo`/`deleteRef`/`deleteRef` bash in as the LEAD steps of the
195042
+ * `required` script, ahead of the commit/squash steps themselves. Nothing
195043
+ * reads or mutates state until the driver actually runs that script; once
195044
+ * it does, HEAD/index are restored exactly, leaving only the reviewer's own
195045
+ * edits dirty (captured by the resting state's own `on` patterns like any
195046
+ * other pending change) — the pure machine never sees the window.
195047
+ * - The OPEN sequence (`decideOpenWindow` + `buildOpenWindowScript`) is
195048
+ * likewise read-only — it self-guards on the STEP'S OWN target state
195049
+ * declaring `reviewWindow: true` — and `program.ts`'s `openWindowScript`
195050
+ * turns that into the `optional` script's `updateRef`/`updateRef`/
195051
+ * `mixedResetTo`/`restoreStagedFrom` bash: saving HEAD to `REVIEW_HEAD_REF`
195052
+ * (the base to `REVIEW_BASE_REF`), then rewinding to the base. The driver
195053
+ * runs it AFTER the required script has already landed the new commit
195054
+ * (`buildOpenWindowScript`'s own doc comment explains why it pins the
195055
+ * literal string `"HEAD"`, never a resolved hash). It re-arms after
195056
+ * read-only commands (`gtd next` / `gtd status`) and refused invocations
195057
+ * too, so the editor's diff view stays consistent no matter which command
195058
+ * the loop last ran.
195059
+ *
195060
+ * Every close/open sequence is idempotent under re-entry, so a crash at any
195061
+ * point is recovered by the next invocation's close (the saved ref also keeps
195062
+ * the real head GC-reachable for the window's whole lifetime).
194270
195063
  */
194271
195064
  /**
194272
195065
  * The window's two state refs live in git's PER-WORKTREE `refs/worktree/*`
@@ -194284,7 +195077,7 @@ const planEntry = (rest, actor, entry) => gen(function* () {
194284
195077
  const REVIEW_HEAD_REF = "refs/worktree/gtd/review-head";
194285
195078
  const REVIEW_BASE_REF = "refs/worktree/gtd/review-base";
194286
195079
  /**
194287
- * The pre-7.2 SHARED refs. gtd never writes them any more; `closeReviewWindow`
195080
+ * The pre-7.2 SHARED refs. gtd never writes them any more; `decideCloseWindow`
194288
195081
  * only reads them to finish a window an older gtd left open across the upgrade
194289
195082
  * — and only when HEAD is still contained in the saved head, since a shared ref
194290
195083
  * may belong to a sibling worktree (see `openWindowRefs`).
@@ -194316,28 +195109,10 @@ const openWindowRefs = (git) => gen(function* () {
194316
195109
  legacy: true
194317
195110
  });
194318
195111
  });
194319
- /**
194320
- * Restore the real head if a review checkout window is open; no-op otherwise.
194321
- * Keyed on the saved-head ref's existence (see `openWindowRefs`), NOT on any
194322
- * config or machine state, so it always runs first and the machine never sees
194323
- * the window.
194324
- *
194325
- * Fails loudly — refs left in place — when HEAD is no longer on the reviewed
194326
- * branch (the base is not an ancestor of HEAD, e.g. after a branch switch): a
194327
- * mixed reset there would rewrite the wrong branch's tip. Manual commits on
194328
- * top of the base pass the guard: the reset keeps their content in the working
194329
- * tree, where the next turn's capture picks it up as review feedback.
194330
- *
194331
- * A window found under the LEGACY shared refs gets one extra guard: a shared
194332
- * ref may have been written by a sibling worktree, so the close only proceeds
194333
- * when HEAD is contained in the saved head — the shape a window this worktree
194334
- * opened itself has. Anything else refuses with the manual recovery rather
194335
- * than resetting this branch onto another worktree's work (issue #118).
194336
- */
194337
- const closeReviewWindow = gen(function* () {
195112
+ const decideCloseWindow = gen(function* () {
194338
195113
  const git = yield* GitService;
194339
195114
  const open = yield* openWindowRefs(git);
194340
- if (isNone(open)) return { closed: false };
195115
+ if (isNone(open)) return { shouldClose: false };
194341
195116
  const refs = open.value;
194342
195117
  const base = yield* git.readRefOption(refs.baseRef);
194343
195118
  if (isSome(base) && base.value !== EMPTY_TREE$1) {
@@ -194346,44 +195121,65 @@ const closeReviewWindow = gen(function* () {
194346
195121
  if (refs.legacy) {
194347
195122
  if (!(yield* git.isAncestor("HEAD", refs.headHash))) return yield* fail$2(/* @__PURE__ */ new Error(`a review checkout window is recorded under the shared ${LEGACY_REVIEW_HEAD_REF} (written by gtd 7.1 or older) but it does not belong to this worktree — HEAD is not contained in it. gtd now scopes the window per worktree (${REVIEW_HEAD_REF}); in the worktree that owns it, run ${manualRecovery(refs)}`));
194348
195123
  }
194349
- yield* git.mixedResetTo(refs.headRef);
194350
- yield* git.deleteRef(refs.headRef);
194351
- yield* git.deleteRef(refs.baseRef);
194352
- return { closed: true };
195124
+ return {
195125
+ shouldClose: true,
195126
+ refs
195127
+ };
194353
195128
  });
194354
195129
  /**
194355
- * Open (or re-arm) the review checkout window. Self-guarded: a no-op unless
194356
- * the resolved rest's state declares `reviewWindow: true` and a distinct base
194357
- * commit exists, so the caller invokes it unconditionally after every state
194358
- * subcommand.
194359
- *
194360
- * The base is the most-recent in-process `reviewBase` commit
194361
- * (`reviewBaseHash`) or, absent any such state, the process's diff base
194362
- * (`run.diffBase` — the process start, `startParentHash`, unless a
194363
- * `gtd review <commitish>` entry commit overrode it via a `Gtd-Review-Base:`
194364
- * trailer, see `computeProcessRun`). When that resolves to the empty tree (a
194365
- * process with no prior commit) or to HEAD itself (an empty process), there
194366
- * is nothing to surface and the window stays closed.
194367
- *
194368
- * Ordering is crash-safe: base ref → head ref → mixed reset → `.gtd/` index
194369
- * pin. A crash before the head-ref write leaves only a stale base ref
194370
- * (overwritten on the next open); a crash after it leaves HEAD ==
194371
- * review-head, which the next invocation's close restores as a no-op.
194372
- */
194373
- const openReviewWindow = gen(function* () {
194374
- const git = yield* GitService;
194375
- const def = (yield* (yield* ConfigService).load).workflow;
194376
- if (!(yield* git.hasCommits())) return { opened: false };
194377
- if (!isReviewWindowState(def, resolveState(def, yield* git.lastCommitSubject()))) return { opened: false };
194378
- const base = reviewBaseFor(def, yield* currentRun);
194379
- const headHash = yield* git.resolveRef("HEAD");
194380
- if (base === EMPTY_TREE$1 || base === headHash) return { opened: false };
194381
- yield* git.updateRef(REVIEW_BASE_REF, base);
194382
- yield* git.updateRef(REVIEW_HEAD_REF, headHash);
194383
- yield* git.mixedResetTo(base);
194384
- yield* git.restoreStagedFrom(REVIEW_HEAD_REF, [".gtd"]);
194385
- return { opened: true };
194386
- });
195130
+ * PURE: the close sequence's bash text — reset to the saved head, then delete
195131
+ * both refs — built from `src/GitScript.ts`'s `mixedResetTo`/`deleteRef`.
195132
+ * Assumes its caller's guards (the reviewed-branch check, the legacy
195133
+ * containment check — see `decideCloseWindow`) have already passed; this
195134
+ * builder performs none of them, only the write sequence for the case where
195135
+ * they did.
195136
+ *
195137
+ * The reset target is `refs.headHash` — the RESOLVED hash, not `refs.headRef`
195138
+ * by name — precisely so the sequence stays idempotent once the ref itself is
195139
+ * gone: the very next statement deletes `headRef`, so a script naming the ref
195140
+ * for the reset would fail on re-entry (the ref `git reset --mixed` would
195141
+ * need is the one the script just deleted). A hash stays resolvable
195142
+ * regardless, so a second run's reset is a true no-op (HEAD is already
195143
+ * there) and both deletes tolerate an already-missing ref. `legacy` on `refs`
195144
+ * goes unused here — the script only needs `headRef`/`baseRef`/`headHash` —
195145
+ * but the parameter carries the whole `WindowRefs` record so a caller can
195146
+ * pass `decideCloseWindow`'s resolved `refs` straight through.
195147
+ */
195148
+ const buildCloseWindowScript = (refs) => [
195149
+ mixedResetTo(refs.headHash),
195150
+ deleteRef(refs.headRef),
195151
+ deleteRef(refs.baseRef)
195152
+ ].join(" &&\n");
195153
+ const decideOpenWindow = (def, target, run, head) => {
195154
+ if (!isReviewWindowState(def, target)) return { shouldOpen: false };
195155
+ const base = reviewBaseFor(def, run);
195156
+ if (base === EMPTY_TREE$1 || base === head) return { shouldOpen: false };
195157
+ return {
195158
+ shouldOpen: true,
195159
+ base,
195160
+ head
195161
+ };
195162
+ };
195163
+ /**
195164
+ * PURE: the open sequence's bash text — base-ref write, head-ref write,
195165
+ * mixed-reset-to-base, then the `.gtd/` restore-staged-from pin, built from
195166
+ * `src/GitScript.ts`'s `updateRef`/`mixedResetTo`/`restoreStagedFrom`, in the
195167
+ * same crash-safe order `openReviewWindow` uses (base ref → head ref → mixed
195168
+ * reset → `.gtd/` pin). Deliberately no `git add --intent-to-add` anywhere —
195169
+ * new files stay untracked, for the exact two reasons `openReviewWindow`'s
195170
+ * own doc comment records — and no whole-tree index write: `restoreStagedFrom`
195171
+ * is scoped to `.gtd/` alone.
195172
+ *
195173
+ * Idempotent under re-entry: `updateRef`/`mixedResetTo` re-pointing at the
195174
+ * same target is a no-op, and `restoreStagedFrom` re-pinning an already-pinned
195175
+ * path is a no-op.
195176
+ */
195177
+ const buildOpenWindowScript = (decision) => [
195178
+ updateRef(REVIEW_BASE_REF, decision.base),
195179
+ updateRef(REVIEW_HEAD_REF, decision.head),
195180
+ mixedResetTo(decision.base),
195181
+ restoreStagedFrom(REVIEW_HEAD_REF, [".gtd"])
195182
+ ].join(" &&\n");
194387
195183
  //#endregion
194388
195184
  //#region node_modules/vscode-languageserver/lib/common/utils/is.js
194389
195185
  var require_is$2 = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -205438,17 +206234,17 @@ const resolveBuiltInMode = (mode) => {
205438
206234
  };
205439
206235
  const steeringCapabilities = (resolved) => {
205440
206236
  if (resolved === void 0) return {};
206237
+ const seeded = resolved.builtIn !== void 0 && resolved.validate?.kind === "command" && isSeededValidateCommand(resolved.mode, resolved.validate.command);
206238
+ const liveValidate = resolved.builtIn !== void 0 && (resolved.validate?.kind === "builtin" || seeded) ? resolved.builtIn.validate : void 0;
205441
206239
  return {
205442
206240
  ...resolved.builtIn !== void 0 ? { format: resolved.builtIn } : {},
205443
- ...resolved.validate?.kind === "builtin" ? { liveValidate: resolved.validate.format.validate } : {},
205444
- ...resolved.validate?.kind === "command" ? { externalValidate: true } : {}
206241
+ ...liveValidate !== void 0 ? { liveValidate } : {},
206242
+ ...resolved.validate?.kind === "command" && !seeded ? { externalValidate: true } : {}
205445
206243
  };
205446
206244
  };
205447
206245
  /** The error message for a `mode:` that resolves to nothing — lists what the active workflow does know. */
205448
206246
  const unknownModeMessage = (def, state, mode) => `state "${state}": mode "${mode}" is not defined by the active workflow (known modes: ${knownModes(def).join(", ")})`;
205449
206247
  const errorText$1 = (e) => e instanceof Error ? e.message : String(e);
205450
- /** How a non-zero exit reads in a message: a status number, or a signal death. */
205451
- const exitText = (status) => status === null ? "a signal" : `status ${status}`;
205452
206248
  /** Render one command template, turning an Eta failure into a named error rather than executing anything. */
205453
206249
  const renderCommand = (mode, key, command, file, context) => try_({
205454
206250
  try: () => renderModeCommand(command, {
@@ -205457,41 +206253,22 @@ const renderCommand = (mode, key, command, file, context) => try_({
205457
206253
  }),
205458
206254
  catch: (e) => /* @__PURE__ */ new Error(`mode "${mode}": "${key}" command failed to render — ${errorText$1(e)}`)
205459
206255
  });
205460
- /** The findings a non-zero `validate` exit reports: its output lines, or a synthesized line when it said nothing. */
205461
- const findingsFrom = (mode, outcome) => {
205462
- const lines = outcome.output.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0);
205463
- if (lines.length > 0) return lines;
205464
- return [`mode "${mode}": validate command exited with ${exitText(outcome.status)} and no output`];
205465
- };
205466
- /**
205467
- * Format `file` in place by running the mode's `format:` command. A mode with
205468
- * no formatter (every mode, until a `modes:` layer declares one — gtd ships
205469
- * none) formats nothing. A failing command is a hard error: the file is left
205470
- * exactly as it was and the caller refuses, rather than validating a
205471
- * half-formatted file.
205472
- */
205473
- const formatSteeringFile = (resolved, file, context) => gen(function* () {
205474
- const command = resolved.formatCommand;
205475
- if (command === void 0) return;
205476
- const rendered = yield* renderCommand(resolved.mode, "format", command, file, context);
205477
- const outcome = yield* (yield* CommandRunner).bash(rendered);
205478
- if (outcome.status !== 0) return yield* fail$2(/* @__PURE__ */ new Error(`mode "${resolved.mode}": format command exited with ${exitText(outcome.status)}${outcome.output.trim().length > 0 ? `:\n${outcome.output.trimEnd()}` : ""}`));
205479
- });
205480
206256
  /**
205481
- * Validate `file` per its mode, returning the findings (empty = valid). A
205482
- * built-in validator runs its pure parser over `content` (the caller's already
205483
- * read this — see `src/StepGuards.ts`, which samples the file's bytes AFTER
205484
- * `formatSteeringFile` has run); a declared `validate:` command runs instead
205485
- * and reads nothing itself — the command owns how it inspects the file. A mode
205486
- * with neither reports no findings.
205487
- */
205488
- const validateSteeringFile = (resolved, file, content, context) => gen(function* () {
205489
- const validator = resolved.validate;
205490
- if (validator === void 0) return [];
205491
- if (validator.kind === "builtin") return validator.format.validate(content);
205492
- const rendered = yield* renderCommand(resolved.mode, "validate", validator.command, file, context);
205493
- const outcome = yield* (yield* CommandRunner).bash(rendered);
205494
- return outcome.status === 0 ? [] : findingsFrom(resolved.mode, outcome);
206257
+ * Render a mode's `format:`/`validate:` commands as plain strings, WITHOUT
206258
+ * running either — for a caller (an emitter that prints scripts rather than
206259
+ * executing them) that wants the commands themselves, not their outcome.
206260
+ * Unlike its siblings above, this needs no `CommandRunner`: it shares their
206261
+ * `renderCommand` helper for the rendering (and its errors) but never reaches
206262
+ * for a runner to execute the result. Order matches `formatAndValidateSteeringFile`
206263
+ * (format then validate); a half the mode doesn't declare — no `formatCommand`,
206264
+ * or a `validate` that is absent or `"builtin"` (an in-process parser has no
206265
+ * shell command to render) — is OMITTED from the result, not rendered as `""`.
206266
+ */
206267
+ const renderSteeringCommands = (resolved, file, context) => gen(function* () {
206268
+ const commands = [];
206269
+ if (resolved.formatCommand !== void 0) commands.push(yield* renderCommand(resolved.mode, "format", resolved.formatCommand, file, context));
206270
+ if (resolved.validate?.kind === "command") commands.push(yield* renderCommand(resolved.mode, "validate", resolved.validate.command, file, context));
206271
+ return commands;
205495
206272
  });
205496
206273
  //#endregion
205497
206274
  //#region src/Lsp.ts
@@ -205542,7 +206319,7 @@ const validateSteeringFile = (resolved, file, content, context) => gen(function*
205542
206319
  * mode with no built-in format at all (a workflow-only name, or a declared
205543
206320
  * `{}` entry) dispatches to no symbols, no actions, and no diagnostics. Either
205544
206321
  * way the file is still formatted and validated by `gtd validate` and the
205545
- * `gtd step` capture gate — just not live in the editor.
206322
+ * `gtd land` capture gate — just not live in the editor.
205546
206323
  *
205547
206324
  * `gtd.openSteeringFile` (an `executeCommand`) resolves the CURRENT state
205548
206325
  * exactly like the CLI (`resolveRest`/`computeProcessRun`/
@@ -206174,27 +206951,6 @@ const openInBrowser = (url) => {
206174
206951
  //#region src/StepGuards.ts
206175
206952
  /** Normalize every markdown checkbox to a single placeholder so a pure `[ ]`→`[x]` tick is invisible to a text comparison; any surviving difference is a human note. */
206176
206953
  const normalizeCheckboxes = (content) => content.replace(/\[[ xX]\]/g, "[_]");
206177
- const steeringFileGuard = {
206178
- name: "steering-file",
206179
- appliesTo: (rest) => rest.stateDef.mode !== void 0,
206180
- prepare: (ctx) => gen(function* () {
206181
- const mode = ctx.rest.stateDef.mode;
206182
- const resolved = resolveSteeringMode(ctx.rest.def, mode);
206183
- if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(ctx.rest.def, ctx.rest.state, mode)));
206184
- if ((yield* RepoFiles).working(ctx.file) === void 0) return;
206185
- yield* formatSteeringFile(resolved, ctx.file, ctx.template);
206186
- }),
206187
- check: (ctx) => gen(function* () {
206188
- const mode = ctx.rest.stateDef.mode;
206189
- const resolved = resolveSteeringMode(ctx.rest.def, mode);
206190
- if (resolved === void 0) return void 0;
206191
- const current = yield* ctx.worktree;
206192
- if (current === void 0) return void 0;
206193
- const errors = yield* validateSteeringFile(resolved, ctx.file, current, ctx.template);
206194
- if (errors.length === 0) return void 0;
206195
- return `${ctx.file} is not valid at "${ctx.rest.state}" — fix these before stepping:\n${errors.map((e) => ` - ${e}`).join("\n")}`;
206196
- })
206197
- };
206198
206954
  /** The `mode:` name of gtd's built-in REVIEW.md checkbox validator — the only mode the sign-off guard understands. */
206199
206955
  const REVIEW_MODE = "review";
206200
206956
  const reviewSignoffGuard = {
@@ -206224,9 +206980,8 @@ const feedbackProgressGuard = {
206224
206980
  };
206225
206981
  /** The `mode:` name of gtd's built-in open-questions checkbox format — the only mode the answer-completeness guard acts on. */
206226
206982
  const QA_MODE = "qa";
206227
- /** The four step-capture guards, in EVALUATION order — message precedence when two would fire is observable, so this order is the contract (matches the old hand-copied call order). */
206983
+ /** The three step-capture guards, in EVALUATION order — message precedence when two would fire is observable, so this order is the contract (matches the old hand-copied call order, minus the now-removed steering-file guard). */
206228
206984
  const stepGuards = [
206229
- steeringFileGuard,
206230
206985
  reviewSignoffGuard,
206231
206986
  feedbackProgressGuard,
206232
206987
  {
@@ -206241,15 +206996,16 @@ const stepGuards = [
206241
206996
  }
206242
206997
  ];
206243
206998
  /**
206244
- * The `gtd step` capture gate: run every guard the resolved rest's state
206245
- * applies to, formatting (write phase) before validating (read phase) — see
206999
+ * The `gtd land` capture gate: run every guard the resolved rest's state
207000
+ * applies to against ONE sample of the current committed/working bytes — see
206246
207001
  * the module docstring — and fail with the first refusal's reason, prefixed
206247
- * `gtd step <invoker>: ` (the one place that prefix, and the `cliErrorLine`
206248
- * `/^gtd[: ]/` contract, is satisfied). A no-op for a squash/no-op decision, a
206249
- * state with no `file:`, or a state no guard applies to.
207002
+ * `gtd land: ` (the one place that prefix, and the `cliErrorLine`
207003
+ * `/^gtd[: ]/` contract, is satisfied). A no-op for a squash/no-op decision, an
207004
+ * ATTEMPT commit (`input.attempt` — see its own doc comment), a state with no
207005
+ * `file:`, or a state no guard applies to.
206250
207006
  */
206251
207007
  const enforceStepGuards = (input) => gen(function* () {
206252
- if (input.kind !== "commit") return;
207008
+ if (input.kind !== "commit" || input.attempt) return;
206253
207009
  const file = input.file;
206254
207010
  if (file === void 0) return;
206255
207011
  const applicable = stepGuards.filter((g) => g.appliesTo(input.rest));
@@ -206264,7 +207020,6 @@ const enforceStepGuards = (input) => gen(function* () {
206264
207020
  hasCodeChange,
206265
207021
  template: input.context
206266
207022
  };
206267
- for (const g of applicable) if (g.prepare !== void 0) yield* g.prepare(base);
206268
207023
  const files = yield* RepoFiles;
206269
207024
  const head = yield* cached(files.committed(file));
206270
207025
  const worktree = yield* cached(try_(() => files.working(file)));
@@ -206275,41 +207030,349 @@ const enforceStepGuards = (input) => gen(function* () {
206275
207030
  };
206276
207031
  for (const g of applicable) {
206277
207032
  const refusal = yield* g.check(ctx);
206278
- if (refusal !== void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd step ${input.invoker}: ${refusal}`));
207033
+ if (refusal !== void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd land: ${refusal}`));
206279
207034
  }
206280
207035
  });
207036
+ //#endregion
207037
+ //#region src/Beat.ts
207038
+ /**
207039
+ * The beat kind for a rest, in precedence order:
207040
+ *
207041
+ * 1. `stalled` — `Edge.ts`'s `stalledAt` verdict, passed in as a plain
207042
+ * boolean (this module never resolves it itself). `stalledAt` already
207043
+ * requires a clean tree, so this can never collide with `capture` below.
207044
+ * 2. `capture` — a `message` rest with a DIRTY tree: the human already
207045
+ * acted; a driver lands it immediately.
207046
+ * 3. Otherwise, the content kind verbatim (`script`/`prompt`, or `message`
207047
+ * on a clean tree).
207048
+ */
207049
+ const beatKindOf = (input) => {
207050
+ if (input.stalled) return "stalled";
207051
+ if (input.contentKind === "message" && input.dirty) return "capture";
207052
+ return input.contentKind;
207053
+ };
207054
+ /**
207055
+ * The `stalled` beat's own content: a diagnosis naming the stuck state and
207056
+ * the three ways out. The first line is shaped so a stderr grep for
207057
+ * `stalled at "<state>"` stays a stable substring (see
207058
+ * `tests/integration/features/readme-driver.feature`).
207059
+ */
207060
+ const stallDiagnosis = (state, actor) => `stalled at "${state}": the last gtd(${actor}): ${state} turn landed an empty attempt, the tree is clean, and another dispatch would repeat it.\n\nThree ways out:\n - sharpen the state's prompt so the turn has something concrete to author\n - give the state a retry: cap, redirecting to an escalation state after\n N fruitless attempts\n - declare a "C" pattern on the state, if it can legitimately finish with\n nothing to change\n`;
207061
+ /**
207062
+ * Assemble one beat document line — the newline-terminated JSON `gtd next
207063
+ * --json` prints. Key order is the emitted order: `kind`, `content`,
207064
+ * `session`, `model`, `validate`, `log`, `state`, `actor`, `label`,
207065
+ * `memory`, `file`, `mode`, `edges`.
207066
+ *
207067
+ * `session`/`validate` are the DISPATCH BLOCK: dropped unless `kind ===
207068
+ * "prompt"`, even if the caller passed one in — the gate lives here (not
207069
+ * only in the caller that computes them) so it can never drift. `content` is
207070
+ * the rendered rest's own content, except at `kind === "stalled"`, where
207071
+ * it's `stallDiagnosis`'s text instead. `model`/`memory`/`file`/`mode`/
207072
+ * `label`/`edges` are plain facts about the resting state and are emitted at
207073
+ * EVERY kind — the same discipline the old `nextJsonOutput` had: omitted
207074
+ * (never `null`) when their source is unset.
207075
+ */
207076
+ const beatDocument = (input) => {
207077
+ const { rendered, kind, log, session, validate } = input;
207078
+ const dispatchable = kind === "prompt";
207079
+ return JSON.stringify({
207080
+ kind,
207081
+ content: kind === "stalled" ? stallDiagnosis(rendered.state, rendered.actor) : rendered.content,
207082
+ ...dispatchable && session !== void 0 ? { session: {
207083
+ id: session.id,
207084
+ resume: session.resume
207085
+ } } : {},
207086
+ ...rendered.model !== void 0 ? { model: rendered.model } : {},
207087
+ ...dispatchable && validate !== void 0 ? { validate } : {},
207088
+ log,
207089
+ state: rendered.state,
207090
+ actor: rendered.actor,
207091
+ ...rendered.label !== void 0 ? { label: rendered.label } : {},
207092
+ ...rendered.memory !== void 0 ? { memory: rendered.memory } : {},
207093
+ ...rendered.file !== void 0 ? { file: rendered.file } : {},
207094
+ ...rendered.mode !== void 0 ? { mode: rendered.mode } : {},
207095
+ ...rendered.edges.length > 0 ? { edges: rendered.edges } : {}
207096
+ }) + "\n";
207097
+ };
207098
+ //#endregion
207099
+ //#region src/WorktreeState.ts
207100
+ /**
207101
+ * The per-worktree, untracked state directory (the git dir) and the
207102
+ * driver-scoped paths gtd keeps in it — IO-shaped, so it sits at the edge
207103
+ * alongside `Edge.ts`/`RepoFiles.ts`; `PatternMachine.ts` stays pure and
207104
+ * never sees it.
207105
+ *
207106
+ * `worktreeGitDir` is resolved from the FILESYSTEM, never from a `git`
207107
+ * subprocess: `<root>/.git` is either a directory (a plain repo — the git dir
207108
+ * IS `.git`) or a `gitdir: <path>` pointer file (a linked worktree, or
207109
+ * `--separate-git-dir`). Reading it directly needs no `stat` (a failed read
207110
+ * because `.git` is a directory reads the same as a missing `.git` — both
207111
+ * fall back to `".git"`) and, unlike `git rev-parse --git-dir`, cannot be
207112
+ * diverted by a stray `GIT_DIR`/`GIT_WORK_TREE` in the ambient environment —
207113
+ * there is no subprocess for those to divert. `loopLogPath` is the ONE path
207114
+ * resolved here: gtd keeps no other per-worktree state (sessions are derived
207115
+ * from history, stall is history itself), and gtd never creates or truncates
207116
+ * even this file — the driver owns it; gtd only names it.
207117
+ */
207118
+ const GITDIR_POINTER = /^gitdir:\s*(.+)$/m;
207119
+ /**
207120
+ * The worktree's own git dir — byte-identical to `git rev-parse --git-dir`
207121
+ * for a plain repo, a linked worktree, or `--separate-git-dir`. Never fails:
207122
+ * a read failure (no `.git`, or `.git` is a real directory) or a `.git` file
207123
+ * with no `gitdir:` line both fall back to the literal `".git"`, which is
207124
+ * also the right answer against the in-memory test tier (whose fake
207125
+ * filesystem has no `.git` entry at all).
207126
+ */
207127
+ const worktreeGitDir = gen(function* () {
207128
+ const { root } = yield* Cwd;
207129
+ const content = yield* (yield* FileSystem).readFileString(join(root, ".git")).pipe(catchAll(() => succeed$3(void 0)));
207130
+ const pointer = content === void 0 ? null : GITDIR_POINTER.exec(content);
207131
+ if (pointer === null) return ".git";
207132
+ const gitdir = pointer[1].trim();
207133
+ return isAbsolute(gitdir) ? gitdir : join(root, gitdir);
207134
+ });
206281
207135
  /**
206282
- * `gtd validate`'s shared evaluation: format the resolved rest's steering
206283
- * file in place, then validate its formatted contents — the SAME
206284
- * format-then-validate pair the `steering-file` guard's `prepare`/`check`
206285
- * split runs, so both format and check the same way (an agent's fresh draft
206286
- * and a human's edit are treated alike). `present` is false — and nothing is
206287
- * formatted or validated — when the state declares no steering file, or the
206288
- * file is absent (e.g. a human deleted `.gtd/REVIEW.md` to approve).
206289
- */
206290
- const checkSteeringFile = (rest, context, file) => gen(function* () {
206291
- const mode = rest.stateDef.mode;
206292
- if (file === void 0 || mode === void 0) return {
206293
- file,
206294
- present: false,
206295
- errors: []
206296
- };
206297
- const files = yield* RepoFiles;
206298
- if (files.working(file) === void 0) return {
206299
- file,
206300
- present: false,
206301
- errors: []
206302
- };
206303
- const resolved = resolveSteeringMode(rest.def, mode);
206304
- if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
206305
- yield* formatSteeringFile(resolved, file, context);
206306
- return {
206307
- file,
206308
- present: true,
206309
- errors: yield* validateSteeringFile(resolved, file, files.working(file) ?? "", context)
206310
- };
207136
+ * The per-worktree loop log path `gtd next --json` reports as `log`.
207137
+ * `$GTD_LOOP_LOG` (non-empty) wins verbatim — no join, no normalization — so
207138
+ * a `gtd` invoked from inside a check script inherits the enclosing run's
207139
+ * log path unchanged. Otherwise `<git-dir>/gtd-loop.log`, isolating two
207140
+ * worktrees looping concurrently from each other's log by construction (each
207141
+ * has its own git dir). Advisory metadata only — gtd neither creates nor
207142
+ * truncates the file.
207143
+ */
207144
+ const loopLogPath = gen(function* () {
207145
+ const { all } = yield* EnvVars;
207146
+ const override = all.GTD_LOOP_LOG;
207147
+ if (override !== void 0 && override !== "") return override;
207148
+ return join(yield* worktreeGitDir, "gtd-loop.log");
206311
207149
  });
206312
207150
  //#endregion
207151
+ //#region src/Install.ts
207152
+ /**
207153
+ * The `gtd install` briefing DOCUMENT — string data plus `renderBriefing()`.
207154
+ * This module holds no protocol logic and nothing derives behaviour from it;
207155
+ * it exists purely so the binary can print, on demand, a complete and
207156
+ * self-contained explanation of how to build a gtd driver in any shell
207157
+ * or runtime — the self-serve version of README's "Writing your own driver"
207158
+ * chapter. `MINIMAL_DRIVER` is pinned equal to that chapter's own fenced
207159
+ * bash block by `Install.test.ts`, so the two can never drift.
207160
+ */
207161
+ const GTD_VERSION$1 = createRequire(import.meta.url)("../package.json").version;
207162
+ const MINIMAL_DRIVER = `#!/usr/bin/env bash
207163
+ set -euo pipefail
207164
+
207165
+ # The bundle plans; this executes. Emitted scripts print their own outcomes.
207166
+ # Exit 3 means SETTLED — nothing owed — so this exits the whole driver at 0.
207167
+ gtd_land() {
207168
+ local json code=0
207169
+ json="$(gtd land --json)" || code=$?
207170
+ [ "$code" = 0 ] || [ "$code" = 3 ] || return "$code"
207171
+ bash -c "$(jq -r '.required // empty' <<<"$json")" || return $?
207172
+ bash -c "$(jq -r '.optional // empty' <<<"$json")" ||
207173
+ echo "warn: presentation follow-up failed — continuing" >&2
207174
+ [ "$code" = 3 ] && exit 0
207175
+ return 0
207176
+ }
207177
+
207178
+ while :; do
207179
+ next="$(gtd next --json)" || exit 1
207180
+ kind="$(jq -r .kind <<<"$next")"
207181
+ log="$(jq -r .log <<<"$next")"
207182
+ case "$kind" in
207183
+ stalled) jq -r .content <<<"$next" >&2; exit 1 ;;
207184
+ message) jq -r .content <<<"$next"; exit 0 ;;
207185
+ capture) ;; # the human already acted — just land it
207186
+ script) bash -c "$(jq -r .content <<<"$next")" >>"$log" 2>&1 || true ;;
207187
+ prompt)
207188
+ sid="$(jq -r '.session.id // empty' <<<"$next")"
207189
+ c="$(jq -r .content <<<"$next")" model="$(jq -r '.model // empty' <<<"$next")"
207190
+ agent_turn() { claude -p "$c" "$1" "$sid" \${model:+--model "$model"} \\
207191
+ --dangerously-skip-permissions >>"$log" 2>&1; }
207192
+ if [ "$(jq -r '.session.resume // false' <<<"$next")" = true ]
207193
+ then agent_turn --resume || agent_turn --session-id
207194
+ else agent_turn --session-id || agent_turn --resume
207195
+ fi
207196
+ v="$(jq -r '.validate // empty' <<<"$next")" n=0
207197
+ while [ -n "$v" ] && ! out="$(bash -c "$v" 2>&1)"; do
207198
+ n=$((n + 1)) && [ "$n" -gt 3 ] && { printf '%s\\n' "$out" >&2; exit 1; }
207199
+ claude -p "$out" --resume "$sid" --dangerously-skip-permissions \\
207200
+ >>"$log" 2>&1 # $out IS the fix prompt, verbatim
207201
+ done ;;
207202
+ esac
207203
+ gtd_land || exit 1
207204
+ done`;
207205
+ const HEADER = () => `gtd ${GTD_VERSION$1} — driver protocol\n\ngtd decides and prints; the driver executes. gtd never runs git, never runs\nyour agent, and never runs a check script. The one subprocess gtd ever spawns\nitself is a steering mode's own format:/validate: command, during a\nland-capture guard.\n`;
207206
+ const BEAT_PROTOCOL = `
207207
+ ## The beat protocol
207208
+
207209
+ Two commands, one repeated beat: ask -> act -> land.
207210
+
207211
+ gtd next --json what now - a pure read; poll it, peek it, no writes
207212
+ gtd land --json how to record what happened - run the script it emits
207213
+
207214
+ Every beat that acts, lands. Every beat that rests, exits.
207215
+
207216
+ \`gtd next --json\` is a read-only PEEK — every field (\`.session.id\`/
207217
+ \`.session.resume\` included) is DERIVED from history, never stored, so looking
207218
+ is free: nothing distinguishes a peek from a dispatch, and calling it twice in
207219
+ a row is always safe. There is no separate claiming form.
207220
+
207221
+ The \`kind\` field selects what to do:
207222
+
207223
+ - \`stalled\` — print \`.content\` (a diagnosis) to stderr and exit non-zero.
207224
+ Terminal: another dispatch would just repeat the same fruitless turn.
207225
+ - \`message\` — print \`.content\` and exit 0. This is a human gate.
207226
+ - \`capture\` — a human gate the human already acted on (the tree is dirty).
207227
+ Land it immediately, no display needed.
207228
+ - \`script\` — run \`bash -c .content\`, appending its output to \`.log\`, and
207229
+ ignore its exit code — the outcome lives in the tree, not in the exit code.
207230
+ - \`prompt\` — send \`.content\` to your agent CLI, using the embedded
207231
+ \`.session\`/\`.model\`/\`.validate\` fields (see below).
207232
+ `;
207233
+ const JSON_FIELD_REFERENCE = `
207234
+ ## JSON field reference
207235
+
207236
+ Every field below is always present unless marked "when set".
207237
+
207238
+ ### \`gtd next --json\` — the beat document
207239
+
207240
+ - Always: \`kind\` (\`capture\`|\`message\`|\`script\`|\`prompt\`|\`stalled\`),
207241
+ \`content\`, \`log\`, \`state\`, \`actor\`
207242
+ - When set: \`model\`, \`memory\`, \`label\`, \`file\`, \`mode\`, \`edges\` — plain
207243
+ facts about the resting state, present at every kind
207244
+ - Only at \`kind: "prompt"\` (the DISPATCH BLOCK — absent at every other kind,
207245
+ a \`stalled\` beat included, by construction): \`session\` (\`{id, resume}\`,
207246
+ both DERIVED from history — a hash of the resting state's memory scope —
207247
+ never stored, so a plain peek is exactly as safe to call as a dispatch would
207248
+ be) and, when the state declares a validatable steering file, \`validate\`
207249
+ (the same script \`gtd validate --json\`'s \`.script\` emits, embedded)
207250
+
207251
+ ### \`gtd land --json\` (also \`--entry\`)
207252
+
207253
+ \`state\`, \`subject\` (\`null\` on a no-op), \`script\` (the combined form —
207254
+ \`required\` verbatim, \`optional\` wrapped non-fatally, so a
207255
+ \`jq -r .script | bash\` driver needs one \`bash\` call, not two), \`required\`,
207256
+ \`optional\`, \`settled\`; \`cost\`/\`model\` when recorded. \`--entry\` omits
207257
+ \`settled\` — read it as \`.settled // false\`.
207258
+
207259
+ ### \`gtd abandon\`/\`gtd restore --json\`
207260
+
207261
+ \`required\`, \`optional\` (always \`""\`).
207262
+
207263
+ ### \`gtd validate --json\`
207264
+
207265
+ \`state\`, \`script\` (\`""\` means nothing to validate); \`file\`/\`mode\` when the
207266
+ resolved rest declares them. On a non-zero run the script's own output IS the
207267
+ fix prompt, verbatim.
207268
+
207269
+ ### The error envelope
207270
+
207271
+ \`{"state":"error","prompt":"<message>"}\` on stdout, plus a single \`gtd: \`
207272
+ line on stderr, exit 1 — this covers usage errors and defects too.
207273
+
207274
+ ### Exit codes (\`gtd land\` only — every other command is plain 0/1)
207275
+
207276
+ | code | meaning |
207277
+ | ---- | --------------------------------------------------------------------- |
207278
+ | 0 | a script was emitted (capture, turn, attempt, squash), or a benign no-op at a clean \`message\` rest |
207279
+ | 3 | SETTLED — nothing owed: a no-op at a \`script\` rest, or the initial-state collapse. stdout still carries a script (a print-only note, or the collapse's real retain+rewind) — run it |
207280
+ | 1 | refusal or usage error — nothing was emitted |
207281
+
207282
+ With \`set -o pipefail\`, \`gtd land | bash\` propagates gtd's own exit code
207283
+ through the pipe.
207284
+ `;
207285
+ const DRIVER_OBLIGATIONS = `
207286
+ ## Driver obligations, in order
207287
+
207288
+ 1. Read exactly one \`gtd next --json\` beat document per iteration. There is
207289
+ no opening move: a human's pending edit arrives as a \`kind: "capture"\`
207290
+ beat, which you land immediately without executing anything.
207291
+ 2. A \`kind: "stalled"\` beat halts the driver with a non-zero exit; a
207292
+ \`kind: "message"\` beat prints \`content\` and exits 0.
207293
+ 3. Run scripts with their output appended to \`.log\` — gtd never creates or
207294
+ truncates that file itself; truncate it once per run. \`$GTD_LOOP_LOG\`
207295
+ overrides its path.
207296
+ 4. Map \`.session.id\`/\`.session.resume\` onto the agent CLI's own session
207297
+ flags — try \`resume\`'s hinted flag first and fall back to the other on
207298
+ failure (\`resume\` is a hint, not a contract: nothing is stored, so a
207299
+ crashed prior turn or an expired agent session recovers by itself).
207300
+ 5. After a \`prompt\` beat, run the document's own \`.validate\` script (when
207301
+ present) and re-prompt its output verbatim on failure — the DRIVER owns
207302
+ the retry cap, not gtd.
207303
+ 6. Land only a beat you acted on (\`capture\`/\`script\`/\`prompt\`) — a stray
207304
+ \`gtd land\` at a clean \`prompt\` rest authors an empty attempt on purpose
207305
+ (that IS the stall bookkeeping), so don't land beats you didn't dispatch.
207306
+ 7. Run \`gtd land --json\`, then execute its \`required\` (always) and
207307
+ \`optional\` (presentation only — its failure is a warning, not a halt).
207308
+ 8. Exit 3 (SETTLED) means there is nothing left to do — run the script first,
207309
+ then stop; exit 0 continues the loop.
207310
+ `;
207311
+ const RECOVERY = `
207312
+ ## Recovery
207313
+
207314
+ gtd exiting 1 means nothing was attempted — a refusal or a usage error. Exit
207315
+ 3 is NOT a failure: it means SETTLED (nothing owed), and stdout still carries
207316
+ a script to run. An emitted script exiting non-zero when YOU run it means
207317
+ something MAY have partially happened. All three recover the same way:
207318
+ re-invoke gtd. It re-reads the real repository state fresh every time, and
207319
+ every emitted script asserts its own HEAD precondition, so a script generated
207320
+ against a repository state that has since moved refuses loudly instead of
207321
+ corrupting anything. Scripts are re-runnable and assert their own HEAD
207322
+ precondition — a driver needs no retry logic beyond "ask gtd again".
207323
+ `;
207324
+ const referenceImplementation = () => `
207325
+ ## Building the user's driver: interview first, then adapt
207326
+
207327
+ The obligations above are the contract; the bash block below is one WORKED
207328
+ EXAMPLE of it. Do not copy it blindly, and do not guess the user's setup —
207329
+ INTERVIEW them, then build a driver shaped by their answers. Ask (offering
207330
+ the default when they have no preference):
207331
+
207332
+ 1. **Which agent should run the turns?** Whatever coding-agent CLI they
207333
+ already use (default: \`claude\`). Map \`.session.id\`/\`.session.resume\`
207334
+ onto THAT agent's continuation mechanism (obligation 4); an agent with no
207335
+ session concept just ignores them — memory is an optimization, every
207336
+ prompt is self-contained.
207337
+ 2. **Under which permission model?** Fully autonomous turns (e.g.
207338
+ \`--dangerously-skip-permissions\`), a sandbox, or the agent's default
207339
+ prompting — their risk tolerance, their call. Also whether the workflow's
207340
+ own \`.model\` hints should be honored (default: yes).
207341
+ 3. **How do they want to invoke it?** A command on PATH (default:
207342
+ \`~/.local/bin/gtd-loop\`), a project task-runner entry, a CI job step —
207343
+ or no artifact at all: YOU drive the beats yourself, following the
207344
+ obligations directly. Pick the runtime to match: bash, their language of
207345
+ choice, anything that parses JSON and spawns subprocesses.
207346
+ 4. **What should happen at the boundaries?** Where the log goes (obligation
207347
+ 3), and whether halting at a human gate should do anything richer than
207348
+ print — desktop notification, terminal-multiplexer status, editor focus —
207349
+ which belongs in their wrapper, never in gtd.
207350
+
207351
+ Then build it, and verify safely before the first real drive:
207352
+ \`gtd next --json\` is a pure read — parse it, check your kind dispatch
207353
+ against the table above, call it as often as you like. Nothing happens until
207354
+ you run an emitted script.
207355
+
207356
+ The reference rendering in bash (requires \`jq\`; the \`claude\` lines are
207357
+ what answers 1–2 replace):
207358
+
207359
+ \`\`\`bash
207360
+ ${MINIMAL_DRIVER}
207361
+ \`\`\`
207362
+ `;
207363
+ const PREREQUISITES = `
207364
+ ## Prerequisites and portability
207365
+
207366
+ - \`jq\` — to pull \`required\`/\`optional\`/the rest of the JSON fields above
207367
+ back out of gtd's \`--json\` output.
207368
+ - \`gtd\` on \`PATH\` — a seeded mode \`validate:\` command is literally the
207369
+ string \`gtd check <mode> '<file>'\`, resolved by NAME at script-run time.
207370
+ Keep one \`gtd\` on \`PATH\`, consistently.
207371
+
207372
+ Any runtime works: the contract is JSON in, subprocesses out.
207373
+ `;
207374
+ const renderBriefing = () => HEADER() + BEAT_PROTOCOL + JSON_FIELD_REFERENCE + DRIVER_OBLIGATIONS + RECOVERY + referenceImplementation() + PREREQUISITES;
207375
+ //#endregion
206313
207376
  //#region src/program.ts
206314
207377
  /**
206315
207378
  * `gtd lsp`: start the LSP server for `.gtd/` steering files over stdio. Its
@@ -206321,6 +207384,23 @@ const checkSteeringFile = (rest, context, file) => gen(function* () {
206321
207384
  */
206322
207385
  const runLspCommand = () => startLspServer();
206323
207386
  /**
207387
+ * `gtd install`: print the driver-building briefing (`src/Install.ts`'s
207388
+ * `renderBriefing()`) — a complete, self-contained explanation of how to
207389
+ * build a gtd driver in any shell or runtime, emitted by the binary
207390
+ * itself so it is always exactly as current as the gtd that prints it.
207391
+ * Writes nothing: "install" means installing knowledge into the calling
207392
+ * agent's context. Its `needs: "none"` (see `Cli.ts`'s `needsOf`, same as
207393
+ * `gtd lsp`) means it skips the repo-root guard entirely — it resolves no
207394
+ * workflow state and reads no config, so it runs from any directory, in or
207395
+ * out of a repository. `--json` and extra arguments are already rejected by
207396
+ * `Cli.ts` before a `Command` value ever reaches here (`--json` itself is
207397
+ * accepted, wrapping the same text under a `briefing` key).
207398
+ */
207399
+ const runInstallCommand = (json, write) => sync$1(() => {
207400
+ const briefing = renderBriefing();
207401
+ write(json ? JSON.stringify({ briefing }) + "\n" : briefing.endsWith("\n") ? briefing : briefing + "\n");
207402
+ });
207403
+ /**
206324
207404
  * `gtd init`: scaffold a MINIMAL `.gtdrc.json` seeding the default variables a
206325
207405
  * fresh project is most likely to change — the test command (`vars.testCommand`)
206326
207406
  * and a ready-to-edit Prettier formatting suggestion (`modes:`). It writes NO
@@ -206334,7 +207414,7 @@ const runLspCommand = () => startLspServer();
206334
207414
  * instead (`assertInitLocation`, which also allows a directory outside any
206335
207415
  * repository) since it writes `.gtdrc.json` at the root and refuses to
206336
207416
  * clobber an existing config. The file is left UNCOMMITTED, so the message
206337
- * warns to commit it before the first `gtd step` (an uncommitted config
207417
+ * warns to commit it before the first `gtd land` (an uncommitted config
206338
207418
  * counts as a pending change the initial state's `* **` edge would otherwise
206339
207419
  * capture).
206340
207420
  */
@@ -206351,79 +207431,185 @@ const runInitCommand = (json, write) => gen(function* () {
206351
207431
  written: ".gtdrc.json",
206352
207432
  inRepo
206353
207433
  }) + "\n");
206354
- else write("Wrote .gtdrc.json seeding the default variables (the test command) and a\nPrettier formatting suggestion. gtd runs its built-in workflow by default — add\na workflow: key only if you want to customize the machine itself.\n\n" + (inRepo ? "Review and commit it before starting: an uncommitted .gtdrc.json counts as a\npending change, so the initial state would capture it on the first step. Once\ncommitted, run `gtd step human` to begin.\n" : "This directory is not a git repository, so there is nothing to commit here. The\nconfig applies to any gtd repository nested below it — gtd discovers it by\nwalking up from the repository root. Run `gtd step human` from such a repo to\nbegin.\n"));
207434
+ else write("Wrote .gtdrc.json seeding the default variables (the test command) and a\nPrettier formatting suggestion. gtd runs its built-in workflow by default — add\na workflow: key only if you want to customize the machine itself.\n\n" + (inRepo ? "Review and commit it before starting: an uncommitted .gtdrc.json counts as a\npending change, so the initial state would capture it on the first landing. Once\ncommitted, run `gtd land` to begin.\n" : "This directory is not a git repository, so there is nothing to commit here. The\nconfig applies to any gtd repository nested below it — gtd discovers it by\nwalking up from the repository root. Run `gtd land` from such a repo to\nbegin.\n"));
206355
207435
  });
207436
+ /** `gtd land`'s exit code for the two SETTLED shapes (`LandResult.settled`) — nothing owed, but stdout still carries a script a piping driver must run. Every other successful landing exits 0. */
207437
+ const EXIT_SETTLED = 3;
207438
+ const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
207439
+ /** The `EmitPreconditions` every script this file assembles asserts against: the resolved HEAD hash the snapshot driving it was taken at — `""` when the repo has no commits yet, `headAssertion`'s own convention for that case. */
207440
+ const headPreconditions = (currentCommit) => ({ expectedHead: currentCommit });
206356
207441
  /**
206357
- * Authenticate `invoker` against the currently resolved rest and perform the
206358
- * one resulting transition (commit or squash) for `gtd step`. `currentRest` →
206359
- * `planStep` decides; the four capture gates sit between the plan and
206360
- * `plan.perform` (nothing has written git yet), then `perform` runs. Refusals
206361
- * fail the Effect with a formatted message; a no-op/reset returns `subject:
206362
- * null` rather than failing (exit zero, per the plan's "clean no-op exits
206363
- * zero").
207442
+ * The resting state's own steering-mode commands, embedded ahead of the
207443
+ * commit — the step script now carries its own format/validate pair instead
207444
+ * of gtd running it in process (see `src/StepGuards.ts`'s shrunk registry).
207445
+ * Empty for a state declaring no `file:`+`mode:` pair; an unknown mode name
207446
+ * is a refusal, exactly as it was when gtd ran the commands itself.
206364
207447
  */
206365
- const stepAsActor = (invoker, cost, model) => gen(function* () {
207448
+ const steeringModeSteps = (rest) => gen(function* () {
207449
+ const file = rest.hints.file;
207450
+ const mode = rest.stateDef.mode;
207451
+ if (file === void 0 || mode === void 0) return [];
207452
+ const resolved = resolveSteeringMode(rest.def, mode);
207453
+ if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
207454
+ return (yield* renderSteeringCommands(resolved, file, rest.context)).map((command) => ({
207455
+ kind: "command",
207456
+ command
207457
+ }));
207458
+ });
207459
+ /**
207460
+ * The `optional` half: re-open the review checkout window when the step lands
207461
+ * at a `reviewWindow: true` state, `""` otherwise.
207462
+ *
207463
+ * `"HEAD"` is deliberately the literal string, not a resolved hash — git
207464
+ * resolves it at the moment this script runs, which is after the required
207465
+ * script has already landed the new commit (see `ReviewWindow.ts`'s
207466
+ * `decideOpenWindow` doc comment). For the same reason it carries no
207467
+ * `expectedHead` precondition: the HEAD it will see is a hash that does not
207468
+ * exist yet at decide time (see `EmitPreconditions`).
207469
+ */
207470
+ const openWindowScript = (rest, targetState) => {
207471
+ const decision = decideOpenWindow(rest.def, targetState, rest.run, "HEAD");
207472
+ if (!decision.shouldOpen) return "";
207473
+ return emitScripts({}, [], [{
207474
+ kind: "gitWrite",
207475
+ command: buildOpenWindowScript({
207476
+ base: decision.base,
207477
+ head: "HEAD"
207478
+ })
207479
+ }]).optional;
207480
+ };
207481
+ /** True for a `"commit"` decision that is an ATTEMPT (`PatternMachine.StepCommit.attempt`) — the one flag both `enforceStepGuards` and `buildRequiredScript` bypass their own steps for (see each call site). */
207482
+ const isAttemptDecision = (decision) => decision.kind === "commit" && decision.attempt === true;
207483
+ /**
207484
+ * The subject the required script WILL produce. A `"commit"` decision's is
207485
+ * fully deterministic already; a `"squash"` decision's is only known once its
207486
+ * template is rendered. Unreachable-on-failure: `renderDecision` already
207487
+ * rendered the same template and refused the command if it threw, so this
207488
+ * second render can never be the first to fail.
207489
+ */
207490
+ const previewSubject = (decision, rest) => decision.kind === "commit" ? succeed$3(decision.subject) : try_(() => renderStateTemplate(decision.template, rest.context).split("\n")[0].trim()).pipe(catchAll(() => succeed$3(null)));
207491
+ /**
207492
+ * The `required` half: everything that decides what lands in git, in order —
207493
+ * the review-window CLOSE (when one is open), the resting state's own
207494
+ * steering-mode format/validate commands, then the commit/squash steps
207495
+ * themselves. The steering-mode step is SKIPPED for an attempt commit
207496
+ * (decision 6): there is nothing to format/validate in an empty diff, and a
207497
+ * `format:` command running ahead of the commit could dirty the tree and
207498
+ * turn an "empty" attempt non-empty, breaking the derivation `stalledAt`
207499
+ * relies on (`Edge.ts`). The review-window close still runs regardless —
207500
+ * committing with a window open would land the attempt on the review base.
207501
+ */
207502
+ const buildRequiredScript = (rest, decision, cost, model) => gen(function* () {
207503
+ const git = yield* GitService;
207504
+ const closeDecision = yield* decideCloseWindow;
207505
+ const commitContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, rest.actor, cost, model);
207506
+ const isAttempt = isAttemptDecision(decision);
207507
+ const steps = [
207508
+ ...closeDecision.shouldClose ? [{
207509
+ kind: "gitWrite",
207510
+ command: buildCloseWindowScript(closeDecision.refs)
207511
+ }] : [],
207512
+ ...isAttempt ? [] : yield* steeringModeSteps(rest),
207513
+ ...yield* renderDecision(git, rest, decision, commitContext, cost, model)
207514
+ ];
207515
+ return emitScripts(headPreconditions(rest.context.currentCommit), steps).required;
207516
+ });
207517
+ /**
207518
+ * Decide the one resulting transition (commit or squash) for `gtd land` —
207519
+ * WITHOUT performing it. `currentRest` → `planStep` decides, authenticating
207520
+ * as `rest.actor` (the resolved rest's own declared actor — landing derives
207521
+ * who acts, it never takes an actor argument); the step-capture guards
207522
+ * (`src/StepGuards.ts`) then refuse before anything is emitted. The decision
207523
+ * is assembled by hand into a
207524
+ * `required` script (the review-window CLOSE, when one is open; the resting
207525
+ * state's steering-mode format/validate commands, when it declares
207526
+ * `file:`+`mode:`; then the commit/squash steps themselves, via
207527
+ * `renderDecision`) and an `optional` one (the review-window OPEN, when the
207528
+ * target declares `reviewWindow: true`) — see the "Review-window management"
207529
+ * recipe this mirrors. Refusals fail the Effect with a formatted message; a
207530
+ * no-op returns `subject: null` and empty scripts (exit zero, nothing to
207531
+ * run).
207532
+ *
207533
+ * `plan.scripts` (built by `Edge.ts` itself) is NOT reused here: its baked-in
207534
+ * precondition uses `rest.context.currentCommit`, the LITERAL git HEAD at
207535
+ * decide time — which, while a review window is open, is the window's BASE,
207536
+ * not the real head — and it contains no window close/open steps at all. This
207537
+ * function builds its own required/optional pair to sidestep that trap.
207538
+ */
207539
+ const planLanding = (opts = {}) => gen(function* () {
206366
207540
  const rest = yield* currentRest;
206367
- const plan = yield* planStep(rest, invoker, {
206368
- ...cost !== void 0 ? { cost } : {},
206369
- ...model !== void 0 ? { model } : {}
206370
- });
207541
+ const plan = yield* planStep(rest, opts);
206371
207542
  if (plan.kind === "refusal") return yield* fail$2(new Error(plan.message));
206372
207543
  if (plan.kind === "noop") return {
206373
207544
  state: plan.state,
206374
207545
  subject: null,
206375
207546
  cost: null,
206376
- model: null
207547
+ model: null,
207548
+ required: emitScripts({}, [{
207549
+ kind: "outcome",
207550
+ command: noteOutcome(noopText(plan.state))
207551
+ }]).required,
207552
+ optional: "",
207553
+ settled: plan.settled
206377
207554
  };
207555
+ const decision = plan.decision;
207556
+ if (decision.kind !== "commit" && decision.kind !== "squash") return yield* fail$2(/* @__PURE__ */ new Error(`gtd: internal error — plan kind "${plan.kind}" but decision kind "${decision.kind}"`));
206378
207557
  yield* enforceStepGuards({
206379
207558
  rest,
206380
207559
  context: rest.context,
206381
207560
  file: rest.hints.file,
206382
207561
  changes: rest.changes,
206383
- invoker,
206384
- kind: plan.kind
207562
+ kind: decision.kind,
207563
+ attempt: isAttemptDecision(decision)
206385
207564
  });
206386
- const outcome = yield* plan.perform;
206387
- if (outcome.kind === "reset" || outcome.kind === "noop") return {
206388
- state: outcome.state,
206389
- subject: null,
206390
- cost: null,
206391
- model: null
206392
- };
207565
+ const targetState = decision.kind === "commit" ? decision.to : decision.state;
206393
207566
  return {
206394
207567
  state: rest.state,
206395
- subject: outcome.subject,
206396
- cost: cost ?? null,
206397
- model: model ?? null
207568
+ subject: yield* previewSubject(decision, rest),
207569
+ cost: opts.cost ?? null,
207570
+ model: opts.model ?? null,
207571
+ required: yield* buildRequiredScript(rest, decision, opts.cost, opts.model),
207572
+ optional: openWindowScript(rest, targetState),
207573
+ settled: yield* collapsesToInitialState(rest, decision)
206398
207574
  };
206399
207575
  });
206400
- /** Renders `stepAsActor`'s result for `gtd step`. */
206401
- const reportStepResult = (result, json, write) => {
207576
+ /** Renders `planLanding`'s result for `gtd land`. Plain text prints the combined script exactly as before (its own outcome rendering, including a genuine no-op's "nothing to do at …" note and the initial-state collapse's `COLLAPSED_TEXT` note, is already baked in — see `renderDecision`'s collapse branch); `--json` additionally carries `required`/`optional`/`script`/`settled`. */
207577
+ const reportLanding = (result, json, write) => {
207578
+ const script = combinedScript(result.required, result.optional);
206402
207579
  if (json) write(JSON.stringify({
206403
207580
  state: result.state,
206404
207581
  subject: result.subject,
206405
207582
  ...result.cost !== null ? { cost: result.cost } : {},
206406
- ...result.model !== null ? { model: result.model } : {}
207583
+ ...result.model !== null ? { model: result.model } : {},
207584
+ script,
207585
+ required: result.required,
207586
+ optional: result.optional,
207587
+ settled: result.settled
206407
207588
  }) + "\n");
206408
- else write(result.subject !== null ? `committed: ${result.subject}\n` : `nothing to do at "${result.state}"\n`);
206409
- };
206410
- /**
206411
- * `gtd step <actor> [--cost=<n>] [--model=<name>]`: authenticate as `<actor>`
206412
- * and perform the one resulting transition, recording `--cost`/`--model` as a
206413
- * `Gtd-Cost:` trailer. `--entry <state>` no longer nests inside this handler —
206414
- * `Cli.ts`'s `--entry` selector resolves that combination to its own
206415
- * `"entry"` command kind (see `runEntryCommand`) before `runCommand` ever
206416
- * dispatches here, so a `step` `Command` is always the ordinary pattern-
206417
- * matched step.
206418
- */
206419
- const runStepCommand = (actor, cost, model, json, write) => gen(function* () {
206420
- const result = yield* stepAsActor(actor, cost, model);
206421
- reportStepResult(result, json, write);
207589
+ else write(script);
207590
+ };
207591
+ /**
207592
+ * `gtd land [--cost=<n>] [--model=<name>]`: land whatever the tree now shows
207593
+ * at the currently resolved rest — authenticated as the rest's own actor,
207594
+ * never a caller-supplied one — recording `--cost`/`--model` as a `Gtd-Cost:`
207595
+ * trailer. `--entry <state>` no longer nests inside this handler — `Cli.ts`'s
207596
+ * `--entry` selector resolves that combination to its own `"entry"` command
207597
+ * kind (see `runEntryCommand`) before `runCommand` ever dispatches here, so a
207598
+ * `land` `Command` is always the ordinary pattern-matched landing.
207599
+ *
207600
+ * Returns the exit code `runCommand`/`Cli.ts`'s `runCli` should use: `0` for
207601
+ * an ordinary landing, `EXIT_SETTLED` (3) when `result.settled` — nothing
207602
+ * owed, but stdout still carries a script a piping driver must run (see
207603
+ * `EXIT_SETTLED`'s own doc comment).
207604
+ */
207605
+ const runLandCommand = (opts, json, write) => gen(function* () {
207606
+ const result = yield* planLanding(opts);
207607
+ reportLanding(result, json, write);
207608
+ return result.settled ? EXIT_SETTLED : 0;
206422
207609
  });
206423
207610
  /**
206424
- * `gtd step <actor> --entry <state> [--var <name>=<value> ...]` (or its
206425
- * subcommand-less short form `gtd --entry <state> ...`, `actor` defaulting to
206426
- * `human` there — see `Cli.ts`'s `--entry` selector): start a brand NEW process at `<state>`
207611
+ * `gtd --entry <state> [--var <name>=<value> ...]` (`actor` always `"human"`
207612
+ * — see `Cli.ts`'s `--entry` selector): start a brand NEW process at `<state>`
206427
207613
  * — any declared, non-commit state (see `PatternMachine.enterableStates`) —
206428
207614
  * replacing the two former named commands `gtd review <commitish>`/`gtd fix`
206429
207615
  * with one generic mechanism. Writes an ordinary turn commit
@@ -206434,7 +207620,7 @@ const runStepCommand = (actor, cost, model, json, write) => gen(function* () {
206434
207620
  * the merged `it.vars`, resolved to a commit, and checked sane). Unlike the
206435
207621
  * old commands (which required a clean tree and used `commitAsIs`), this
206436
207622
  * commits via `commitAllWithPrefix` — capturing whatever the working tree
206437
- * carries at the moment of entry, exactly like an ordinary `gtd step`
207623
+ * carries at the moment of entry, exactly like an ordinary `gtd land`
206438
207624
  * capture, rather than demanding a clean tree first.
206439
207625
  *
206440
207626
  * Any failure below is a plain refusal: nothing is written. Checked in order:
@@ -206461,14 +207647,14 @@ const runEntryCommand = (actor, entryState, varOverrides, json, write, commandLa
206461
207647
  vars: varOverrides
206462
207648
  });
206463
207649
  if (plan.kind === "refusal") return yield* fail$2(new Error(plan.message));
206464
- yield* plan.perform;
206465
207650
  if (json) write(JSON.stringify({
206466
207651
  state: plan.state,
206467
- subject: plan.subject
207652
+ subject: plan.subject,
207653
+ required: plan.scripts.required,
207654
+ optional: plan.scripts.optional
206468
207655
  }) + "\n");
206469
- else write(`committed: ${plan.subject}\n`);
207656
+ else write(combinedScript(plan.scripts.required, plan.scripts.optional));
206470
207657
  });
206471
- const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
206472
207658
  /**
206473
207659
  * `gtd abandon`: end the process currently underway WITHOUT completing it,
206474
207660
  * returning the machine to the workflow's initial state — the recovery path out
@@ -206500,6 +207686,13 @@ const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
206500
207686
  * that fails when there is nothing to recover is a worse tool. The one refusal
206501
207687
  * is a process whose first commit is the repository's own root commit: there is
206502
207688
  * no earlier commit to rewind to.
207689
+ *
207690
+ * The read-side refusal/no-op checks above stay direct `GitService` reads (the
207691
+ * documented exception, `AGENTS.md`) — abandon must still work when a `Rest`
207692
+ * would refuse. The actual mutation (retaining history, then the mixed reset)
207693
+ * is no longer PERFORMED here: it's emitted as a `required` bash script for
207694
+ * the external driver to run, built from `src/GitScript.ts`'s pure
207695
+ * `updateRef`/`mixedResetTo` — narrowing the exception further still.
206503
207696
  */
206504
207697
  const runAbandonCommand = (json, write) => gen(function* () {
206505
207698
  const git = yield* GitService;
@@ -206507,25 +207700,51 @@ const runAbandonCommand = (json, write) => gen(function* () {
206507
207700
  const initial = initialStateOf(def);
206508
207701
  const run = yield* currentRun;
206509
207702
  if (run.trace.length === 0) {
207703
+ const required = emitScripts({}, [{
207704
+ kind: "outcome",
207705
+ command: abandonNoopOutcome(initial)
207706
+ }]).required;
206510
207707
  if (json) write(JSON.stringify({
206511
207708
  state: initial,
206512
- abandoned: false
207709
+ abandoned: false,
207710
+ required,
207711
+ optional: ""
206513
207712
  }) + "\n");
206514
- else write(`no gtd process is underway (resting at "${initial}") — nothing to abandon\n`);
207713
+ else write(combinedScript(required, ""));
206515
207714
  return;
206516
207715
  }
206517
207716
  const restState = run.trace[run.trace.length - 1].state;
206518
207717
  if (run.startParentHash === EMPTY_TREE) return yield* fail$2(/* @__PURE__ */ new Error(`gtd abandon: the process underway (resting at "${restState}") starts at the repository's first commit — there is no earlier commit to rewind to`));
206519
- yield* retainHistory(git, yield* git.resolveRef("HEAD"), run.startParentHash);
206520
- yield* git.mixedResetTo(run.startParentHash);
206521
- const subject = yield* git.lastCommitSubject();
207718
+ const closeDecision = yield* decideCloseWindow;
207719
+ const tip = closeDecision.shouldClose ? closeDecision.refs.headHash : yield* git.resolveRef("HEAD");
207720
+ const steps = [
207721
+ ...closeDecision.shouldClose ? [{
207722
+ kind: "gitWrite",
207723
+ command: buildCloseWindowScript(closeDecision.refs)
207724
+ }] : [],
207725
+ {
207726
+ kind: "gitWrite",
207727
+ command: updateRef(HISTORY_REF, tip)
207728
+ },
207729
+ {
207730
+ kind: "gitWrite",
207731
+ command: mixedResetTo(run.startParentHash)
207732
+ },
207733
+ {
207734
+ kind: "outcome",
207735
+ command: abandonedOutcome(restState, run.startParentHash, initial)
207736
+ }
207737
+ ];
207738
+ const required = emitScripts(headPreconditions(yield* git.resolveRef("HEAD")), steps).required;
206522
207739
  if (json) write(JSON.stringify({
206523
207740
  state: initial,
206524
207741
  abandoned: true,
206525
207742
  from: restState,
206526
- head: run.startParentHash
207743
+ head: run.startParentHash,
207744
+ required,
207745
+ optional: ""
206527
207746
  }) + "\n");
206528
- else write(`abandoned the process resting at "${restState}" — HEAD is back at ${run.startParentHash.slice(0, 7)} ("${subject}"), resting at "${initial}".\nEverything the process produced is kept as uncommitted changes (\`git status\`); discard them with \`git checkout -- . && git clean -fd .gtd\` for a clean tree.\n`);
207747
+ else write(combinedScript(required, ""));
206529
207748
  });
206530
207749
  /**
206531
207750
  * `gtd restore`: hard-reset HEAD back to the pre-squash tip a squash or
@@ -206537,7 +207756,10 @@ const runAbandonCommand = (json, write) => gen(function* () {
206537
207756
  * Guarded by `restorability` so it never discards work it didn't create:
206538
207757
  * refuses on a dirty working tree, when there is no retained history, and
206539
207758
  * when HEAD has advanced past the retained tip with commits that would be
206540
- * lost by resetting.
207759
+ * lost by resetting. Those checks stay direct `GitService` reads (same
207760
+ * documented exception as `gtd abandon`); the mutation itself (the hard
207761
+ * reset, then clearing the retained-history ref) is emitted as a `required`
207762
+ * script instead of performed here.
206541
207763
  */
206542
207764
  const runRestoreCommand = (json, write) => gen(function* () {
206543
207765
  const git = yield* GitService;
@@ -206549,94 +207771,274 @@ const runRestoreCommand = (json, write) => gen(function* () {
206549
207771
  const headHash = yield* git.resolveRef("HEAD");
206550
207772
  const check = yield* restorability(git, headHash, yield* git.lastCommitMessage(), tip);
206551
207773
  if (!check.ok) return yield* fail$2(/* @__PURE__ */ new Error(`gtd restore: ${check.reason} — HEAD ${headHash.slice(0, 7)} is ahead of the retained tip ${tip.slice(0, 7)}.`));
206552
- yield* git.hardResetTo(tip);
206553
- yield* clearRetainedHistory(git);
206554
- const after = yield* currentRest;
206555
- const subject = yield* git.lastCommitSubject();
207774
+ const after = yield* restAt(tip);
207775
+ const steps = [
207776
+ {
207777
+ kind: "gitWrite",
207778
+ command: hardResetTo(tip)
207779
+ },
207780
+ {
207781
+ kind: "gitWrite",
207782
+ command: deleteRef(HISTORY_REF)
207783
+ },
207784
+ {
207785
+ kind: "outcome",
207786
+ command: restoredOutcome(tip, after.state)
207787
+ }
207788
+ ];
207789
+ const required = emitScripts(headPreconditions(headHash), steps).required;
206556
207790
  if (json) write(JSON.stringify({
206557
207791
  state: after.state,
206558
207792
  restored: true,
206559
207793
  to: tip,
206560
- from: before.state
207794
+ from: before.state,
207795
+ required,
207796
+ optional: ""
206561
207797
  }) + "\n");
206562
- else write(`restored the retained history — HEAD is back at ${tip.slice(0, 7)} ("${subject}"), resting at "${after.state}". Resume with the loop, or \`git reset\` to any earlier turn to restart from there.\n`);
207798
+ else write(combinedScript(required, ""));
207799
+ });
207800
+ /**
207801
+ * The mode's own resolved VALIDATE command, rendered against `file` — the
207802
+ * command `selfValidateInstruction` names. Resolution mirrors
207803
+ * `renderSteeringCommands`'s own layering (`src/SteeringMode.ts`) but picks
207804
+ * out the validate half alone: a declared `validate:` command (or, for
207805
+ * `qa`/`review`, `PatternConfig.ts`'s own seeded `gtd check <mode> '<file>'`
207806
+ * default — every compiled definition carries ONE of the two) renders as the
207807
+ * LAST command `renderSteeringCommands` would emit (format, if any, always
207808
+ * comes first); a mode resolving to a `"builtin"` validator (no shell command
207809
+ * at all — only reachable when `resolveSteeringMode` is asked about a mode
207810
+ * outside the active definition's own compiled `modes:`, since a real
207811
+ * definition always seeds one) or to no mode at all names the leaf
207812
+ * `gtd check <mode> '<file>'` invocation directly instead
207813
+ * (`seededValidateCommand`), so there is always something concrete to name.
207814
+ */
207815
+ const resolveSelfValidateCommand = (def, mode, file, context) => gen(function* () {
207816
+ const resolved = resolveSteeringMode(def, mode);
207817
+ if (resolved?.validate?.kind === "command") {
207818
+ const rendered = yield* renderSteeringCommands(resolved, file, context);
207819
+ const validateCommand = rendered[rendered.length - 1];
207820
+ if (validateCommand !== void 0) return validateCommand;
207821
+ }
207822
+ return yield* try_({
207823
+ try: () => renderModeCommand(seededValidateCommand(mode), {
207824
+ ...context,
207825
+ file
207826
+ }),
207827
+ catch: (e) => e instanceof Error ? e : new Error(String(e))
207828
+ });
206563
207829
  });
206564
207830
  /**
206565
207831
  * The self-validation instruction gtd APPENDS to a `prompt` rest that declares
206566
207832
  * both `file:` and `mode:` — i.e. a state whose actor hands over a steering
206567
- * file `gtd validate` formats and checks. Appended ONLY to plain `gtd next`
206568
- * output (for a human or a simple driver who reads the prompt and hands it to
206569
- * an agent, so the agent self-validates); withheld from `gtd next --json`,
206570
- * where the driving loop instead runs `gtd validate` after the turn and
206571
- * re-prompts on findings (see the `bin/gtd` loop driver). This is
206572
- * advisory: `gtd step` runs the same format-and-validate gate and REFUSES a
206573
- * turn whose steering file is invalid (see `stepAsActor`), so a malformed file
206574
- * is never captured whether or not this instruction was followed.
206575
- */
206576
- const selfValidateInstruction = (file) => `\nBefore finishing your turn, run \`gtd validate\` — it formats and checks ${file} — and fix every violation it reports until it exits cleanly. Do not finish while it still reports violations.\n`;
207833
+ * file some command validates. Appended ONLY to plain `gtd next` output (for a
207834
+ * human or a simple driver who reads the prompt and hands it to an agent, so
207835
+ * the agent self-validates); withheld from `gtd next --json`, where the
207836
+ * driving loop instead runs `gtd validate` after the turn and re-prompts on
207837
+ * findings (see the README's minimal driver, and `fixPromptInstruction` below
207838
+ * — its driver-side counterpart). This is advisory: `gtd land` embeds that same
207839
+ * command ahead of its own commit and REFUSES a turn whose steering file is
207840
+ * invalid (see `planLanding`), so a malformed file is never captured whether
207841
+ * or not this instruction was followed.
207842
+ */
207843
+ const selfValidateInstruction = (command, file) => `\nBefore finishing your turn, run \`${command}\` — it checks ${file} — and fix every violation it reports until it exits cleanly. Do not finish while it still reports violations.\n`;
207844
+ /**
207845
+ * The driver-side counterpart of `selfValidateInstruction` above — the same
207846
+ * gate, expressed for a loop that RUNS the validate script itself and
207847
+ * re-prompts the same agent session on a non-zero exit, rather than for an
207848
+ * agent that self-validates before finishing. Wrapped onto the emitted
207849
+ * validate script's last command via `Emit.ts`'s `onFailure` (see
207850
+ * `runValidateCommand`), so a driver's fix re-prompt is exactly this text
207851
+ * plus the script's own captured findings — never hand-composed in bash.
207852
+ */
207853
+ const fixPromptInstruction = (file) => `Your last turn does not pass its own validation script. Fix these format violations in ${file}, then finish:`;
206577
207854
  /** True when a rendered rest is a `prompt` turn that hands over a validatable steering file (`file:`+`mode:` both declared). */
206578
207855
  const emitsValidatablePrompt = (rendered) => rendered.kind === "prompt" && rendered.file !== void 0 && rendered.mode !== void 0;
206579
- /** `gtd next [--json]`: pure emitter of the resolved rest's rendered content (no mutation). */
206580
- /** `gtd next --json`'s single-line object — omitting each optional key (never `null`-valued) when its source is unset, exactly like `gtd status --json`. */
206581
- const nextJsonOutput = (rendered) => JSON.stringify({
206582
- state: rendered.state,
206583
- actor: rendered.actor,
206584
- kind: rendered.kind,
206585
- content: rendered.content,
206586
- ...rendered.model !== void 0 ? { model: rendered.model } : {},
206587
- ...rendered.memory !== void 0 ? { memory: rendered.memory } : {},
206588
- ...rendered.label !== void 0 ? { label: rendered.label } : {},
206589
- ...rendered.file !== void 0 ? { file: rendered.file } : {},
206590
- ...rendered.mode !== void 0 ? { mode: rendered.mode } : {},
206591
- ...rendered.edges.length > 0 ? { edges: rendered.edges } : {}
206592
- }) + "\n";
206593
- /** `gtd next`'s plain-text output: the rendered content (newline-terminated), plus the self-validation instruction when the rest is a validatable prompt (see `emitsValidatablePrompt`). */
206594
- const nextPlainOutput = (rendered) => {
207856
+ /**
207857
+ * Resolve the resting state's own steering-file validate script — the SAME
207858
+ * script both `gtd validate --json` (`runValidateCommand`, its thin caller
207859
+ * now) and `gtd next --json`'s embedded `validate` field emit, from one
207860
+ * shared resolver so the two surfaces can't drift. `undefined` = nothing to
207861
+ * validate: no `file:`+`mode:` declared, or the declared file is absent from
207862
+ * the working tree. An unknown `mode:` FAILS this Effect (exactly as
207863
+ * `runValidateCommand` always has) — `runNextCommand` is the one caller that
207864
+ * degrades that failure to omitting `validate`, mirroring how the plain-text
207865
+ * self-validation instruction already degrades on the same failure.
207866
+ */
207867
+ const resolveValidateScript = (rest) => gen(function* () {
207868
+ const file = rest.hints.file;
207869
+ const mode = rest.stateDef.mode;
207870
+ if (file === void 0 || mode === void 0) return void 0;
207871
+ const fs = yield* FileSystem;
207872
+ const toError = (e) => e instanceof Error ? e : new Error(String(e));
207873
+ if (!(yield* fs.exists(file).pipe(mapError$1(toError)))) return void 0;
207874
+ const resolved = resolveSteeringMode(rest.def, mode);
207875
+ if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
207876
+ const commands = yield* renderSteeringCommands(resolved, file, rest.context);
207877
+ const lastIndex = commands.length - 1;
207878
+ const steps = commands.map((command, index) => ({
207879
+ kind: "command",
207880
+ command,
207881
+ ...index === lastIndex && resolved.validate?.kind === "command" ? { onFailure: fixPromptInstruction(file) } : {}
207882
+ }));
207883
+ return {
207884
+ file,
207885
+ mode,
207886
+ script: emitScripts(headPreconditions(rest.context.currentCommit), steps).required
207887
+ };
207888
+ });
207889
+ /** `gtd next`'s plain-text output: the rendered content (newline-terminated), plus the self-validation instruction (naming `selfValidateCommand`, when resolved) when the rest is a validatable prompt. */
207890
+ const nextPlainOutput = (rendered, selfValidateCommand) => {
206595
207891
  const base = rendered.content.endsWith("\n") ? rendered.content : rendered.content + "\n";
206596
- return emitsValidatablePrompt(rendered) ? base + selfValidateInstruction(rendered.file) : base;
206597
- };
207892
+ return emitsValidatablePrompt(rendered) && selfValidateCommand !== void 0 ? base + selfValidateInstruction(selfValidateCommand, rendered.file) : base;
207893
+ };
207894
+ /**
207895
+ * `gtd next --json`'s output: the whole BEAT DOCUMENT (`src/Beat.ts`'s
207896
+ * `beatDocument`) for the resolved rest — one self-describing, POLLABLE line.
207897
+ * `stalledAt` (a pure read off history) and the dirty-tree test both feed
207898
+ * `beatKindOf`, which picks the driver-facing `kind`; the dispatch block
207899
+ * (`session`/`validate`) is resolved only when `kind === "prompt"`, since
207900
+ * `beatDocument` drops it at every other kind regardless. Nothing is written
207901
+ * by any of this — `resolveSession` derives the same id for a peek as it
207902
+ * would for a dispatch, and `resolveValidateScript` only READS the file/mode
207903
+ * it inspects — so there is nothing left for a separate claiming form to
207904
+ * protect (see `runNextCommand` below).
207905
+ */
207906
+ const nextBeatOutput = (rest, rendered) => gen(function* () {
207907
+ const kind = beatKindOf({
207908
+ contentKind: rendered.kind,
207909
+ dirty: rest.changes.length > 0,
207910
+ stalled: stalledAt(rest)
207911
+ });
207912
+ const session = kind === "prompt" ? resolveSession(rendered.memory, rendered.memoryResumed) : void 0;
207913
+ const resolvedValidate = kind === "prompt" ? yield* resolveValidateScript(rest).pipe(catchAll(() => succeed$3(void 0))) : void 0;
207914
+ const validate = resolvedValidate !== void 0 && resolvedValidate.script.length > 0 ? resolvedValidate.script : void 0;
207915
+ return beatDocument({
207916
+ rendered,
207917
+ kind,
207918
+ log: yield* loopLogPath,
207919
+ ...session !== void 0 ? { session: {
207920
+ id: session.sessionId,
207921
+ resume: session.resume
207922
+ } } : {},
207923
+ ...validate !== void 0 ? { validate } : {}
207924
+ });
207925
+ });
207926
+ /**
207927
+ * `gtd next`: pure emitter of the resolved rest's rendered content — no
207928
+ * mutation at all. Plain-text output is the human rendering, unchanged: the
207929
+ * rendered content plus (when resolvable) the self-validation instruction;
207930
+ * `--json` hands off to `nextBeatOutput` above. Nothing is written either
207931
+ * way, so a peek and a would-be dispatch are the same call — there is no
207932
+ * separate claiming form any more.
207933
+ */
206598
207934
  const runNextCommand = (json, write) => gen(function* () {
206599
- const rendered = yield* renderRest(yield* currentRest);
206600
- write(json ? nextJsonOutput(rendered) : nextPlainOutput(rendered));
207935
+ const rest = yield* currentRest;
207936
+ const rendered = yield* renderRest(rest);
207937
+ if (json) {
207938
+ write(yield* nextBeatOutput(rest, rendered));
207939
+ return;
207940
+ }
207941
+ if (rendered.kind === "prompt" && rest.changes.length === 0 && stalledAt(rest)) {
207942
+ write(stallDiagnosis(rendered.state, rendered.actor));
207943
+ return;
207944
+ }
207945
+ const selfValidateCommand = emitsValidatablePrompt(rendered) ? yield* resolveSelfValidateCommand(rest.def, rendered.mode, rendered.file, rest.context).pipe(catchAll(() => succeed$3(void 0))) : void 0;
207946
+ write(nextPlainOutput(rendered, selfValidateCommand));
206601
207947
  });
206602
207948
  /**
206603
- * `gtd validate [--json]`: format (in place) then validate the steering file
206604
- * the resolved rest declares (`file:` rendered, `mode:` selecting how), over
206605
- * its WORKING-TREE contents. A state with no `file:`/`mode:`, or an absent
206606
- * file, has nothing to validate (exit 0). A clean verdict exits 0; violations
206607
- * FAIL the Effect with the findings (one per line), so the process exits
206608
- * non-zero — the signal a producing agent (or the driver) loops on until the
206609
- * file is valid. A built-in mode reuses the canonical `OpenQuestions`/
206610
- * `ReviewDoc` parsers (the same the LSP publishes), so there is one source of
206611
- * truth per format and no bash port; any `modes:`-declared command runs
206612
- * instead (or, for `format:`, in addition).
207949
+ * `gtd validate [--json]`: emit the script that would format (in place) then
207950
+ * validate the steering file the resolved rest declares (`file:` rendered,
207951
+ * `mode:` selecting how) — the SAME commands `gtd land`'s capture guard now
207952
+ * embeds ahead of its own commit (see `planLanding`), rendered here for a
207953
+ * human or agent to run directly. gtd itself reads no file and executes
207954
+ * nothing: a state with no `file:`/`mode:`, or a file absent from the working
207955
+ * tree, has nothing to validate (`script: ""`, exit 0 either way) — the
207956
+ * verdict now lives in the emitted script's own future exit code, not this
207957
+ * command's, so this never fails on a bad file. `RepoFiles`/`FileSystem` is
207958
+ * used only to check the file's PRESENCE, never to read or judge its content.
207959
+ * When the resolved mode's validate half is a `command` (never a `"builtin"`
207960
+ * validator, which renders no shell command at all), the LAST rendered
207961
+ * command carries `onFailure: fixPromptInstruction(file)` (`Emit.ts`'s
207962
+ * `failurePromptWrapper`) — so a non-zero exit from the script prints the
207963
+ * COMPLETE ready-to-send fix prompt (instruction + findings) rather than bare
207964
+ * findings, letting a driver treat the script's captured output as opaque
207965
+ * prompt text (see the README's minimal driver).
206613
207966
  */
206614
207967
  const runValidateCommand = (json, write) => gen(function* () {
206615
207968
  const rest = yield* currentRest;
206616
- const { file, present, errors } = yield* checkSteeringFile(rest, rest.context, rest.hints.file);
206617
- if (!present) {
207969
+ const file = rest.hints.file;
207970
+ const mode = rest.stateDef.mode;
207971
+ const emitNothingToValidate = () => {
206618
207972
  if (json) write(JSON.stringify({
206619
207973
  state: rest.state,
206620
- valid: true,
206621
- errors: []
207974
+ ...file !== void 0 ? { file } : {},
207975
+ ...mode !== void 0 ? { mode } : {},
207976
+ script: ""
206622
207977
  }) + "\n");
206623
207978
  else write(`nothing to validate at "${rest.state}"\n`);
207979
+ };
207980
+ const resolved = yield* resolveValidateScript(rest);
207981
+ if (resolved === void 0) {
207982
+ emitNothingToValidate();
206624
207983
  return;
206625
207984
  }
207985
+ if (json) write(JSON.stringify({
207986
+ state: rest.state,
207987
+ ...resolved
207988
+ }) + "\n");
207989
+ else write(resolved.script.length > 0 ? `${resolved.script}\n` : `nothing to validate at "${rest.state}"\n`);
207990
+ });
207991
+ /**
207992
+ * `gtd check <mode> <file>`: read `<file>` and run the BUILT-IN steering
207993
+ * format named `<mode>`'s pure parser over its contents, printing each
207994
+ * finding one per line and exiting non-zero when there are any. Unlike `gtd
207995
+ * validate` (which resolves the currently resting state's own `file:`/`mode:`
207996
+ * and formats before validating), this resolves NO workflow state and reads
207997
+ * NO config — both `mode` and `file` are given explicitly, so it needs only
207998
+ * `FileSystem.FileSystem` and runs from any directory. This is what a
207999
+ * workflow's seeded `validate:` command (`SteeringFormats.ts`'s
208000
+ * `seededValidateCommand`) invokes as a leaf step; it does no formatting
208001
+ * in-place — an emitted script has no template context to format against.
208002
+ *
208003
+ * An absent file mirrors `gtd validate`'s own absent-file behavior: exit 0,
208004
+ * no output in plain mode, `{valid: true, errors: []}` under `--json`. A
208005
+ * clean parse is the same shape. A non-clean parse writes `--json`'s
208006
+ * `{valid: false, errors}` body (or each finding, one per line, in plain
208007
+ * mode) BEFORE failing the Effect. In PLAIN mode the failure then carries only
208008
+ * a summary message, landing on stderr alone (`cliErrorLine`'s line) — the
208009
+ * findings are already on stdout. Under `--json`, `Cli.ts`'s single envelope
208010
+ * writer (`report`) ALSO puts its own `{state:"error",prompt}` object on
208011
+ * stdout for any failing Effect, so a `--json` invocation with findings emits
208012
+ * stdout as two newline-delimited JSON documents — this handler's own
208013
+ * `{valid,errors}` body first, then the generic error envelope — rather than
208014
+ * one; a consumer must read line-delimited JSON, not `JSON.parse(stdout)`
208015
+ * whole. Kept as the pragmatic resolution of a real tension in this command's
208016
+ * two governing requirements ("exits non-zero" and "`--json` reports
208017
+ * `{valid,errors}`"): the alternative was piping a `--json` failure through a
208018
+ * NEW exit-code channel bypassing `Cli.ts`'s shared envelope entirely, which
208019
+ * every other command's `--json` failure goes through today.
208020
+ */
208021
+ const runCheckCommand = (mode, file, json, write) => gen(function* () {
208022
+ const format = steeringFormatFor(mode);
208023
+ if (format === void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd check: unknown mode "${mode}" — known modes: ${builtInModeNames().join(", ")}`));
208024
+ const fs = yield* FileSystem;
208025
+ const toError = (e) => e instanceof Error ? e : new Error(String(e));
208026
+ const errors = (yield* fs.exists(file).pipe(mapError$1(toError))) ? format.validate(yield* fs.readFileString(file).pipe(mapError$1(toError))) : [];
206626
208027
  if (errors.length === 0) {
206627
208028
  if (json) write(JSON.stringify({
206628
- state: rest.state,
206629
- file,
206630
- mode: rest.stateDef.mode,
206631
208029
  valid: true,
206632
208030
  errors: []
206633
208031
  }) + "\n");
206634
- else write(`${file}: valid\n`);
206635
208032
  return;
206636
208033
  }
206637
- return yield* fail$2(/* @__PURE__ */ new Error(`${file} is not valid:\n${errors.map((e) => ` - ${e}`).join("\n")}`));
208034
+ if (json) write(JSON.stringify({
208035
+ valid: false,
208036
+ errors
208037
+ }) + "\n");
208038
+ else write(errors.join("\n") + "\n");
208039
+ return yield* fail$2(/* @__PURE__ */ new Error(`gtd check: ${file} is not valid under mode "${mode}" (${errors.length} finding(s))`));
206638
208040
  });
206639
- /** Which declared `on` pattern (if any) each pending change matches — the pure computation `gtd status` reports (both plain and `--json`). `onEdges` is ALREADY RENDERED against `it.vars` (`renderOnEdges`) — the reported pattern is the one a real `gtd step` would match against. */
208041
+ /** Which declared `on` pattern (if any) each pending change matches — the pure computation `gtd status` reports (both plain and `--json`). `onEdges` is ALREADY RENDERED against `it.vars` (`renderOnEdges`) — the reported pattern is the one a real `gtd land` would match against. */
206640
208042
  const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
206641
208043
  const matchedRow = onEdges.find(([patternStr]) => {
206642
208044
  const parsed = parsePattern(patternStr);
@@ -206840,48 +208242,59 @@ const assertInitLocation = (git, fs) => gen(function* () {
206840
208242
  */
206841
208243
  const needsOf = (kind) => {
206842
208244
  switch (kind) {
206843
- case "lsp": return "none";
206844
- case "init": return "fs";
208245
+ case "lsp":
208246
+ case "install": return "none";
208247
+ case "init":
208248
+ case "check": return "fs";
206845
208249
  case "visualize": return "config";
206846
208250
  default: return "state";
206847
208251
  }
206848
208252
  };
206849
- /** Dispatches to the named `run*Command` handler for every `Command.kind` — the counterpart of `Cli.ts`'s `parseArgv`, which has already validated every field a handler receives. */
206850
- const dispatchCommand = (command, json, write) => {
208253
+ /** Dispatches to the named `run*Command` handler for every `Command.kind` EXCEPT `"land"` (which alone returns a non-zero exit code — see `dispatchCommand`) — the counterpart of `Cli.ts`'s `parseArgv`, which has already validated every field a handler receives. */
208254
+ const dispatchVoidCommand = (command, json, write) => {
206851
208255
  switch (command.kind) {
206852
208256
  case "lsp": return runLspCommand();
206853
208257
  case "init": return runInitCommand(json, write);
206854
208258
  case "visualize": return runVisualizeCommand(command.port, command.open, json, write);
206855
- case "step": return runStepCommand(command.actor, command.cost, command.model, json, write);
206856
208259
  case "entry": return runEntryCommand(command.actor, command.state, command.vars, json, write, command.label);
206857
208260
  case "abandon": return runAbandonCommand(json, write);
206858
208261
  case "restore": return runRestoreCommand(json, write);
206859
208262
  case "next": return runNextCommand(json, write);
206860
208263
  case "status": return runStatusCommand(json, write);
206861
208264
  case "validate": return runValidateCommand(json, write);
208265
+ case "check": return runCheckCommand(command.mode, command.file, json, write);
208266
+ case "install": return runInstallCommand(json, write);
206862
208267
  }
206863
208268
  };
206864
208269
  /**
206865
- * Runs `command` inside the bracket every state-touching command shares: the
206866
- * repo-root guard, then close-any-open-review-window before the command sees
206867
- * HEAD, then re-arm it afterward whether `command` succeeded or refused (see
206868
- * `closeReviewWindow`/`openReviewWindow`).
208270
+ * Dispatches every `Command` to its exit code — `"land"` alone can return
208271
+ * non-zero (`EXIT_SETTLED`, via `runLandCommand`); every other kind always
208272
+ * succeeds at `0` (`dispatchVoidCommand`, piped through `Effect.as(0)`).
208273
+ * Splitting the exhaustive switch out to `dispatchVoidCommand` keeps this
208274
+ * function's own branching to the one kind that actually varies.
206869
208275
  */
206870
- const runInReviewWindowBracket = (git, fs, command) => gen(function* () {
206871
- yield* assertRunningFromRepoRoot(git, fs);
206872
- yield* closeReviewWindow;
206873
- const outcome = yield* either(command);
206874
- const rearm = yield* either(openReviewWindow);
206875
- if (isLeft(outcome)) return yield* fail$2(outcome.left);
206876
- if (isLeft(rearm)) return yield* fail$2(rearm.left);
206877
- });
208276
+ const dispatchCommand = (command, json, write) => {
208277
+ if (command.kind === "land") return runLandCommand({
208278
+ ...command.cost !== void 0 ? { cost: command.cost } : {},
208279
+ ...command.model !== void 0 ? { model: command.model } : {}
208280
+ }, json, write);
208281
+ return dispatchVoidCommand(command, json, write).pipe(as(0));
208282
+ };
206878
208283
  /**
206879
208284
  * The one entry point `Cli.ts`'s `runCli` calls for a resolved `Command`:
206880
208285
  * dispatches to the matching `run*Command` handler (see `dispatchCommand`),
206881
- * wrapped in the repo-root guard + review-window bracket exactly when
206882
- * `needsOf(command.kind) === "state"` — `lsp`/`init`/`visualize` (the
206883
- * `standaloneKinds`) run bare. `Cli.ts` has already validated every field on
206884
- * `command` (arity, flag scope, decoding), so nothing here re-parses argv.
208286
+ * wrapped in the repo-root guard exactly when `needsOf(command.kind) ===
208287
+ * "state"` — `lsp`/`init`/`visualize`/`check` (the `standaloneKinds`) run
208288
+ * bare. `Cli.ts` has already validated every field on `command` (arity, flag
208289
+ * scope, decoding), so nothing here re-parses argv. Returns the exit code
208290
+ * `Cli.ts`'s `runCli` should use — `0` for every command except a `land` that
208291
+ * settles (`EXIT_SETTLED`).
208292
+ *
208293
+ * No review-window close/open bracket runs here any more: every state-command
208294
+ * handler (see `planLanding`) now decides for itself, off `ReviewWindow.ts`'s
208295
+ * pure `decideCloseWindow`/`decideOpenWindow`, whether a close/open belongs in
208296
+ * the script it emits — there is no in-process HEAD to rewind ahead of time
208297
+ * (gtd itself no longer writes git for a state command at all).
206885
208298
  */
206886
208299
  const runCommand = (command, json, write) => {
206887
208300
  const dispatch = dispatchCommand(command, json, write);
@@ -206889,7 +208302,8 @@ const runCommand = (command, json, write) => {
206889
208302
  return gen(function* () {
206890
208303
  const git = yield* GitService;
206891
208304
  const fs = yield* FileSystem;
206892
- yield* runInReviewWindowBracket(git, fs, dispatch);
208305
+ yield* assertRunningFromRepoRoot(git, fs);
208306
+ return yield* dispatch;
206893
208307
  });
206894
208308
  };
206895
208309
  //#endregion
@@ -206948,21 +208362,21 @@ const FLAGS = [
206948
208362
  name: "--cost",
206949
208363
  arity: 1,
206950
208364
  repeatable: false,
206951
- scope: (kind) => kind === "step",
208365
+ scope: (kind) => kind === "land",
206952
208366
  decode: ([raw]) => nonNegativeNumber(raw ?? "", "--cost"),
206953
- scopeError: "gtd: --cost is only valid for `gtd step` without --entry — an entry is not a metered agent turn",
208367
+ scopeError: "gtd: --cost is only valid for `gtd land` — an entry is not a metered agent turn",
206954
208368
  valueHint: "<n>",
206955
- help: ["(gtd step only) record the invocation's token cost"]
208369
+ help: ["(gtd land only) record the invocation's token cost"]
206956
208370
  },
206957
208371
  {
206958
208372
  name: "--model",
206959
208373
  arity: 1,
206960
208374
  repeatable: false,
206961
- scope: (kind) => kind === "step",
208375
+ scope: (kind) => kind === "land",
206962
208376
  decode: ([raw]) => raw === void 0 || raw.trim() === "" || /[\r\n]/.test(raw) ? left("gtd: --model must be a non-empty, single-line value") : right(raw),
206963
- scopeError: "gtd: --model is only valid for `gtd step` without --entry — an entry is not a metered agent turn",
208377
+ scopeError: "gtd: --model is only valid for `gtd land` — an entry is not a metered agent turn",
206964
208378
  valueHint: "<name>",
206965
- help: ["(gtd step only, with --cost) tag that cost's model"]
208379
+ help: ["(gtd land only, with --cost) tag that cost's model"]
206966
208380
  },
206967
208381
  {
206968
208382
  name: "--entry",
@@ -206970,14 +208384,12 @@ const FLAGS = [
206970
208384
  repeatable: false,
206971
208385
  scope: (kind) => kind === "entry",
206972
208386
  decode: ([raw]) => right(raw ?? ""),
206973
- scopeError: "gtd: --entry is only valid for `gtd step` or the bare `gtd --entry <state>` form",
208387
+ scopeError: "gtd: --entry is only valid with no other command — use the bare `gtd --entry <state>` form; landing and entering are different verbs",
206974
208388
  valueHint: "<state>",
206975
208389
  help: [
206976
- "(gtd step, or with no command at all) start a brand new",
206977
- "process at <state> — any declared, non-commit state —",
206978
- "instead of stepping the one currently resting. Not",
206979
- "combinable with --cost/--model (an entry is not a metered",
206980
- "agent turn)"
208390
+ "(with no command at all) start a brand new process at",
208391
+ "<state> — any declared, non-commit state — authenticated",
208392
+ "as human"
206981
208393
  ]
206982
208394
  },
206983
208395
  {
@@ -207031,20 +208443,22 @@ const COMMAND_ROWS = [
207031
208443
  ]
207032
208444
  },
207033
208445
  {
207034
- token: "step",
207035
- kind: "step",
207036
- arity: { name: "actor" },
208446
+ token: "land",
208447
+ kind: "land",
208448
+ arity: "none",
207037
208449
  details: [
207038
- "Authenticate as <actor>, match the resolved rest's",
207039
- "declared patterns against the pending changes, and commit",
207040
- "(or squash) the one resulting transition. Pass",
207041
- "--cost=<n> (optionally --model=<name>) to record the",
207042
- "just-finished invocation's token cost and model on the",
207043
- "turn commit (summed into it.processCost/processCostByModel).",
207044
- "Pass --entry <state> to start a brand NEW process at",
207045
- "<state> instead — any declared, non-commit state — with",
207046
- "repeatable --var <name>=<value> supplying that new",
207047
- "process's fixed it.vars overrides"
208450
+ "Land whatever the tree now shows at the currently",
208451
+ "resolved rest — a human capture, an agent/check turn, an",
208452
+ "empty attempt (a fruitless prompt turn), or a squash — and",
208453
+ "print the script that records it; a driver runs the",
208454
+ "script, e.g. `gtd land | bash`. Pass --cost=<n>",
208455
+ "(optionally --model=<name>) to record the just-finished",
208456
+ "invocation's token cost and model on the turn commit",
208457
+ "(summed into it.processCost/processCostByModel). Exits 0",
208458
+ "when a script is emitted (or a benign no-op at a clean",
208459
+ "message rest), 3 when SETTLED — nothing owed: a no-op at a",
208460
+ "script rest, or the initial-state collapse (stdout still",
208461
+ "carries the script) — 1 on any refusal"
207048
208462
  ]
207049
208463
  },
207050
208464
  {
@@ -207076,7 +208490,14 @@ const COMMAND_ROWS = [
207076
208490
  token: "next",
207077
208491
  kind: "next",
207078
208492
  arity: "none",
207079
- details: ["Print the resolved rest's rendered script/prompt/message (no mutation)"]
208493
+ details: [
208494
+ "Print the resolved rest's rendered script/prompt/message",
208495
+ "(no mutation, safe to poll). --json emits the whole beat",
208496
+ "document instead: kind (capture|message|script|prompt|",
208497
+ "stalled) selects what a driver does, content is what it",
208498
+ "runs or shows, plus the prompt session, model, validate",
208499
+ "script, log path and the resting state's own fields"
208500
+ ]
207080
208501
  },
207081
208502
  {
207082
208503
  token: "status",
@@ -207089,9 +208510,17 @@ const COMMAND_ROWS = [
207089
208510
  kind: "validate",
207090
208511
  arity: "none",
207091
208512
  details: [
207092
- "Format and validate the steering file the resolved rest",
207093
- "declares, with its mode's commands (its file:/mode:);",
207094
- "exits non-zero with the findings when it is invalid"
208513
+ "Print the script that formats (when declared) then",
208514
+ "validates the resolved rest's steering file, using its",
208515
+ "mode's commands (its file:/mode:), instead of running it —",
208516
+ "a driver runs the script and reads the findings from its",
208517
+ "own exit code/output. Always exits 0; --json emits",
208518
+ "{state, file?, mode?, script} (script is \"\" when there is",
208519
+ "nothing to validate; plain text prints \"nothing to",
208520
+ "validate\" in that case). On a non-zero validate exit",
208521
+ "the emitted script prints a ready-to-send fix prompt",
208522
+ "(instruction + findings) and exits with the",
208523
+ "validator's own code"
207095
208524
  ]
207096
208525
  },
207097
208526
  {
@@ -207109,6 +208538,33 @@ const COMMAND_ROWS = [
207109
208538
  "local web server (--port <n>, --no-open; --json prints the",
207110
208539
  "model and exits)"
207111
208540
  ]
208541
+ },
208542
+ {
208543
+ token: "check",
208544
+ kind: "check",
208545
+ arity: { names: ["mode", "file"] },
208546
+ details: [
208547
+ "Read <file> and run the built-in steering format named",
208548
+ "<mode> (see `gtd validate`'s modes: qa, review) over its",
208549
+ "contents, printing each finding one per line and exiting",
208550
+ "non-zero when there are any. Resolves no workflow state and",
208551
+ "reads no config — standalone, runnable from any directory",
208552
+ "with <mode>/<file> given explicitly. This is what a",
208553
+ "workflow's emitted validation script invokes as a leaf step"
208554
+ ]
208555
+ },
208556
+ {
208557
+ token: "install",
208558
+ kind: "install",
208559
+ arity: "none",
208560
+ details: [
208561
+ "Print a complete, self-contained briefing that teaches an",
208562
+ "agent (or a human) to build a gtd driver in any shell or",
208563
+ "runtime — the self-serve version of README's 'Writing",
208564
+ "your own driver'. Writes nothing: this installs knowledge",
208565
+ "into the calling agent's context, not files on disk. Runs",
208566
+ "from any directory, in or out of a repository"
208567
+ ]
207112
208568
  }
207113
208569
  ];
207114
208570
  const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
@@ -207121,8 +208577,10 @@ const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
207121
208577
  * enterable states.
207122
208578
  */
207123
208579
  const REMOVED = {
207124
- review: "gtd: `gtd review <commitish>` is gone — this workflow's own state names aren't known to gtd; run `gtd step <actor> --entry <review-state> --var <name>=<value> ...` instead — run it with an unknown <review-state> to see this workflow's own enterable states",
207125
- fix: "gtd: `gtd fix` is gone — this workflow's own state names aren't known to gtd; run `gtd step <actor> --entry <fix-state>` instead — run it with an unknown <fix-state> to see this workflow's own enterable states"
208580
+ step: "gtd: `gtd step <actor>` is gone — landing is actorless; run `gtd land` instead (`gtd --entry <state>` for entries)",
208581
+ review: "gtd: `gtd review <commitish>` is gone — this workflow's own state names aren't known to gtd; run `gtd --entry <review-state> --var <name>=<value> ...` instead — run it with an unknown <review-state> to see this workflow's own enterable states",
208582
+ fix: "gtd: `gtd fix` is gone — this workflow's own state names aren't known to gtd; run `gtd --entry <fix-state>` instead — run it with an unknown <fix-state> to see this workflow's own enterable states",
208583
+ loop: "gtd: `gtd loop` is gone — gtd decides and prints, a driver executes. Copy the driver from the README's \"A complete minimal driver\" section and run that instead"
207126
208584
  };
207127
208585
  const COLUMN = 19;
207128
208586
  const renderBlock = (header, lines) => {
@@ -207130,14 +208588,7 @@ const renderBlock = (header, lines) => {
207130
208588
  const headerCell = ` ${header}`;
207131
208589
  return (headerCell.length < COLUMN ? `${headerCell.padEnd(COLUMN)}${first ?? ""}\n` : `${headerCell}\n${" ".repeat(COLUMN)}${first ?? ""}\n`) + rest.map((l) => `${" ".repeat(COLUMN)}${l}\n`).join("");
207132
208590
  };
207133
- const PRELUDE_LOOP = renderBlock("(no command), loop", [
207134
- "Launch the loop driver (bin/gtd), which repeatedly drives",
207135
- "an agent through gtd next/gtd step calls until the",
207136
- "workflow rests at a human gate (a non-autonomous state)",
207137
- "or settles. A bare gtd invocation and gtd loop both",
207138
- "launch it identically"
207139
- ]);
207140
- const ENTRY_SHORT_FORM = renderBlock("(no command) --entry <state>", ["Short form of 'step human --entry <state>' — starts a new", "process authenticated as human, e.g. 'gtd --entry <state>'"]);
208591
+ const ENTRY_SHORT_FORM = renderBlock("(no command) --entry <state>", ["Starts a new process authenticated as human, e.g.", "'gtd --entry <state>'"]);
207141
208592
  const VERSION_BLOCK = renderBlock("version", ["Print version and exit"]);
207142
208593
  const HELP_BLOCK = renderBlock("help", ["Print this help and exit"]);
207143
208594
  const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--entry", "--var"]);
@@ -207147,10 +208598,17 @@ const flagHeader = (row) => {
207147
208598
  };
207148
208599
  const renderHelp = () => {
207149
208600
  const commandBlocks = COMMAND_ROWS.map((row) => {
207150
- const header = row.arity === "none" ? row.token : `${row.token} <${row.arity.name}>`;
208601
+ const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : `${row.token} <${row.arity.name}>`;
207151
208602
  return renderBlock(header, row.details);
207152
208603
  });
207153
- return `Usage: gtd [command] [options]\n\nCommands:\n${PRELUDE_LOOP + commandBlocks[0] + commandBlocks[1] + ENTRY_SHORT_FORM + commandBlocks.slice(2).join("") + VERSION_BLOCK + HELP_BLOCK}\nOptions:\n${FLAGS.map((row) => renderBlock(flagHeader(row), row.help)).join("")}${renderBlock("--version, -v", ["Print version and exit"]) + renderBlock("--help, -h", ["Print this help and exit"])}`;
208604
+ return `Usage: gtd [command] [options]\n\nCommands:\n${[
208605
+ commandBlocks[0],
208606
+ commandBlocks[1],
208607
+ ENTRY_SHORT_FORM,
208608
+ ...commandBlocks.slice(2),
208609
+ VERSION_BLOCK,
208610
+ HELP_BLOCK
208611
+ ].join("")}\nOptions:\n${FLAGS.map((row) => renderBlock(flagHeader(row), row.help)).join("")}${renderBlock("--version, -v", ["Print version and exit"]) + renderBlock("--help, -h", ["Print this help and exit"])}`;
207154
208612
  };
207155
208613
  /**
207156
208614
  * The stderr line for a CLI error: a `gtd: ` prefix UNLESS the message
@@ -207233,6 +208691,12 @@ const scopeViolation = (kind, present) => {
207233
208691
  };
207234
208692
  const arityError = (cmd, rest, arity) => {
207235
208693
  if (arity === "none") return rest.length > 0 ? `gtd ${cmd}: too many arguments — expected none, got: ${rest.join(", ")}` : void 0;
208694
+ if ("names" in arity) {
208695
+ const [first, second] = arity.names;
208696
+ if (rest.length < 2) return rest.length === 0 ? `gtd ${cmd}: missing ${first} and ${second} arguments` : `gtd ${cmd}: missing ${second} argument`;
208697
+ if (rest.length > 2) return `gtd ${cmd}: too many arguments — expected ${first} and ${second}, got: ${rest.join(", ")}`;
208698
+ return;
208699
+ }
207236
208700
  if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`;
207237
208701
  if (rest.length > 1) return `gtd ${cmd}: too many arguments — expected one ${arity.name}, got: ${rest.join(", ")}`;
207238
208702
  };
@@ -207251,6 +208715,12 @@ const decodeFlags = (byFlag) => {
207251
208715
  }
207252
208716
  return right(bag);
207253
208717
  };
208718
+ /** Assembles a `land` `Command` from its already-scope-checked flag bag — split out of `parseArgv` so its two independent, omit-when-absent optional fields don't inflate that function's own branching. */
208719
+ const buildLandCommand = (bag) => ({
208720
+ kind: "land",
208721
+ ...bag["--cost"] !== void 0 ? { cost: bag["--cost"] } : {},
208722
+ ...bag["--model"] !== void 0 ? { model: bag["--model"] } : {}
208723
+ });
207254
208724
  const parseArgv = (argv) => {
207255
208725
  if (argv.includes("--help") || argv.includes("-h")) return {
207256
208726
  kind: "output",
@@ -207276,12 +208746,12 @@ const parseArgv = (argv) => {
207276
208746
  };
207277
208747
  const row = first === void 0 ? void 0 : commandByToken(first);
207278
208748
  const removedMessage = first === void 0 ? void 0 : REMOVED[first];
207279
- const selectsEntry = entryPresent && (first === void 0 || row?.kind === "step");
208749
+ const selectsEntry = entryPresent && first === void 0;
207280
208750
  const kind = selectsEntry ? "entry" : row?.kind;
207281
208751
  if (row === void 0 && removedMessage === void 0 && !selectsEntry) {
207282
208752
  const violation = scopeViolation(void 0, present);
207283
208753
  if (violation !== void 0) return usagePlan(violation, jsonSeen);
207284
- if (first === void 0) return usagePlan("gtd: missing command — see usage above (`gtd --help`)", jsonSeen, renderHelp());
208754
+ if (first === void 0) return usagePlan("gtd: missing command — gtd decides and prints, a driver executes; copy one from the README's \"A complete minimal driver\" section and run that, or see usage above (`gtd --help`)", jsonSeen, renderHelp());
207285
208755
  return usagePlan(`unknown command '${first}'`, jsonSeen);
207286
208756
  }
207287
208757
  if (removedMessage !== void 0 && !selectsEntry) {
@@ -207292,8 +208762,7 @@ const parseArgv = (argv) => {
207292
208762
  if (kind === "entry" && first === void 0) {
207293
208763
  if (restPositionals.length > 0) return usagePlan(`gtd: too many arguments — expected none, got: ${restPositionals.join(", ")}`, jsonSeen);
207294
208764
  } else {
207295
- const arityRow = kind === "entry" ? commandByToken("step") : row;
207296
- const arityMsg = arityError(first, restPositionals, arityRow.arity);
208765
+ const arityMsg = arityError(first, restPositionals, row.arity);
207297
208766
  if (arityMsg !== void 0) return usagePlan(arityMsg, jsonSeen);
207298
208767
  }
207299
208768
  const violation = scopeViolation(kind, present);
@@ -207302,27 +208771,21 @@ const parseArgv = (argv) => {
207302
208771
  if (isLeft(decoded)) return usagePlan(decoded.left, jsonSeen);
207303
208772
  const bag = decoded.right;
207304
208773
  const json = present.has("--json");
207305
- if (kind === "step") {
208774
+ if (kind === "land") {
207306
208775
  if (bag["--model"] !== void 0 && bag["--cost"] === void 0) return usagePlan("gtd: --model requires --cost — it tags the recorded cost with the model that ran", json);
207307
208776
  return {
207308
208777
  kind: "command",
207309
- command: {
207310
- kind: "step",
207311
- actor: restPositionals[0],
207312
- ...bag["--cost"] !== void 0 ? { cost: bag["--cost"] } : {},
207313
- ...bag["--model"] !== void 0 ? { model: bag["--model"] } : {}
207314
- },
208778
+ command: buildLandCommand(bag),
207315
208779
  json
207316
208780
  };
207317
208781
  }
207318
208782
  if (kind === "entry") {
207319
- const actor = first === void 0 ? "human" : restPositionals[0];
207320
- const label = first === void 0 ? `gtd --entry ${entryRaw}` : `gtd step ${actor} --entry ${entryRaw}`;
208783
+ const label = `gtd --entry ${entryRaw}`;
207321
208784
  return {
207322
208785
  kind: "command",
207323
208786
  command: {
207324
208787
  kind: "entry",
207325
- actor,
208788
+ actor: "human",
207326
208789
  state: entryRaw,
207327
208790
  vars: bag["--var"] ?? {},
207328
208791
  label
@@ -207339,6 +208802,15 @@ const parseArgv = (argv) => {
207339
208802
  },
207340
208803
  json
207341
208804
  };
208805
+ if (kind === "check") return {
208806
+ kind: "command",
208807
+ command: {
208808
+ kind: "check",
208809
+ mode: restPositionals[0],
208810
+ file: restPositionals[1]
208811
+ },
208812
+ json
208813
+ };
207342
208814
  return {
207343
208815
  kind: "command",
207344
208816
  command: { kind },
@@ -207387,7 +208859,9 @@ const runCli = (argv, io) => {
207387
208859
  io.stderr(`${cliErrorLine(plan.message)}\n`);
207388
208860
  io.exit(1);
207389
208861
  });
207390
- return runCommand(plan.command, plan.json, io.stdout).pipe(provide$2(io.layers()), sandbox, catchAll((cause) => isInterruptedOnly(cause) ? failCause$2(cause) : report(io, plan.json)(cause)));
208862
+ return runCommand(plan.command, plan.json, io.stdout).pipe(map$4((code) => {
208863
+ if (code !== 0) io.exit(code);
208864
+ }), provide$2(io.layers()), sandbox, catchAll((cause) => isInterruptedOnly(cause) ? failCause$2(cause) : report(io, plan.json)(cause)));
207391
208865
  };
207392
208866
  //#endregion
207393
208867
  //#region src/main.ts