@pmelab/gtd 8.6.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 +367 -179
  2. package/dist/gtd.bundle.mjs +1234 -352
  3. package/package.json +2 -3
  4. package/bin/gtd +0 -878
@@ -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
@@ -191441,7 +191474,7 @@ const isSeededValidateCommand = (mode, command) => command === seededValidateCom
191441
191474
  * label: <string> # optional, opaque display name — never on a commit state
191442
191475
  * file: <string> # optional, an Eta template naming the state's steering file — never on a commit state
191443
191476
  * mode: <modeName> # optional, requires "file" — a built-in (qa/review) or a `modes:` entry; never on a commit state
191444
- * 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
191445
191478
  * <local>: { machine: <name>, with: { ... } } # a REFERENCE — instantiates <name> as a child, see src/Machines.ts
191446
191479
  * ```
191447
191480
  *
@@ -192154,7 +192187,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
192154
192187
  };
192155
192188
  //#endregion
192156
192189
  //#region src/workflows/unified.yaml
192157
- 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";
192158
192191
  //#endregion
192159
192192
  //#region src/workflows/templates.ts
192160
192193
  /**
@@ -192798,6 +192831,7 @@ const makeGitImpl = (executor, root) => {
192798
192831
  readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
192799
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))),
192800
192833
  topLevel: () => exec("git", "rev-parse", "--show-toplevel").pipe(map$4((s) => s.trim())),
192834
+ gitDir: () => exec("git", "rev-parse", "--absolute-git-dir").pipe(map$4((s) => s.trim())),
192801
192835
  commitHistory: (base, head = "HEAD") => {
192802
192836
  const range = base !== void 0 ? `${base}..${head}` : head !== "HEAD" ? head : void 0;
192803
192837
  const args = [
@@ -192908,6 +192942,79 @@ var CommandRunner = class CommandRunner extends Tag("CommandRunner")() {
192908
192942
  }));
192909
192943
  };
192910
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
192911
193018
  //#region node_modules/eta/dist/index.mjs
192912
193019
  var EtaError = class extends Error {
192913
193020
  constructor(message) {
@@ -193473,13 +193580,6 @@ const withHistoryTrailer = (subject, hash) => `${subject}\n\n${HISTORY_TRAILER_P
193473
193580
  * by a later `gtd restore` command to recover the pre-squash tip.
193474
193581
  */
193475
193582
  const parseHistoryTrailer = (message) => HISTORY_TRAILER_RE.exec(message)?.[1];
193476
- /**
193477
- * Record the pre-squash tip so a later `gtd restore` can find it. A no-op
193478
- * (the git call is skipped entirely) when `tipHash === startParentHash`: an
193479
- * empty process — no turns were ever committed — has no turn chain worth
193480
- * keeping.
193481
- */
193482
- const retainHistory = (git, tipHash, startParentHash) => tipHash === startParentHash ? _void : git.updateRef(HISTORY_REF, tipHash);
193483
193583
  /** A thin wrapper over `git.readRefOption(HISTORY_REF)`. */
193484
193584
  const readRetainedHistory = (git) => git.readRefOption(HISTORY_REF);
193485
193585
  /**
@@ -193514,9 +193614,10 @@ const restorability = (git, headHash, headMessage, tipHash) => gen(function* ()
193514
193614
  /**
193515
193615
  * Bash-script builders mirroring `GitWriterOperations` (`src/Git.ts`) — one
193516
193616
  * function per writer method, each returning the equivalent bash as a
193517
- * `string` instead of executing it. Scaffolding for a later change that will
193518
- * emit these scripts rather than run git directly through the `Effect`
193519
- * `CommandExecutor`; nothing calls this module yet.
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.
193520
193621
  *
193521
193622
  * Pure, like `src/PatternMachine.ts`: no git, no filesystem, no `Effect`, no
193522
193623
  * IO of any kind. Every export is a plain, total function of its arguments.
@@ -193607,6 +193708,143 @@ const restoreStagedFrom = (source, paths) => {
193607
193708
  ].join("\n");
193608
193709
  };
193609
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
193610
193848
  //#region src/Emit.ts
193611
193849
  /**
193612
193850
  * Assembles the two scripts a `gtd next`-time decision hands the external
@@ -193616,7 +193854,11 @@ const restoreStagedFrom = (source, paths) => {
193616
193854
  * nothing but that view). Pure, like `src/GitScript.ts`: no git, no
193617
193855
  * filesystem, no `Effect`. Every input string (a `GitScript.ts` builder's
193618
193856
  * output, a steering mode's already-rendered `format:`/`validate:` command)
193619
- * arrives already rendered — this module only concatenates and wraps.
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.
193620
193862
  *
193621
193863
  * Both halves are meant to stand alone: a driver may run either, both, or
193622
193864
  * only `required`, and a person could paste either into a terminal. That's
@@ -193668,6 +193910,29 @@ const reviewWindowAssertion = (ref, expectedHash) => {
193668
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; }`;
193669
193911
  };
193670
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
+ /**
193671
193936
  * `gtd_retry` takes ONE already-`shellQuote`d command string and `eval`s it,
193672
193937
  * mirroring `src/Git.ts`'s `withIndexLockRetry`: retry ONLY on the same two
193673
193938
  * substrings `isIndexLockError` matches, with jittered exponential backoff
@@ -193706,13 +193971,18 @@ const RETRY_HELPER = [
193706
193971
  ` done`,
193707
193972
  `}`
193708
193973
  ].join("\n");
193709
- const renderStep = (step) => step.kind === "gitWrite" ? `gtd_retry ${shellQuote(step.command)}` : step.command;
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
+ };
193710
193979
  const assembleScript = (preconditions, steps) => {
193711
193980
  if (steps.length === 0) return "";
193712
193981
  const sections = ["set -euo pipefail"];
193713
193982
  if (preconditions.expectedHead !== void 0) sections.push(headAssertion(preconditions.expectedHead));
193714
193983
  if (preconditions.reviewWindow) sections.push(reviewWindowAssertion(preconditions.reviewWindow.ref, preconditions.reviewWindow.expectedHash));
193715
193984
  if (steps.some((step) => step.kind === "gitWrite")) sections.push(RETRY_HELPER);
193985
+ if (steps.some((step) => step.kind === "outcome")) sections.push(OUTCOME_PREAMBLE);
193716
193986
  sections.push(...steps.map(renderStep));
193717
193987
  return sections.join("\n\n");
193718
193988
  };
@@ -193728,6 +193998,32 @@ const emitScripts = (preconditions, required = [], optional = []) => ({
193728
193998
  required: assembleScript(preconditions, required),
193729
193999
  optional: assembleScript(preconditions, optional)
193730
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
+ };
193731
194027
  //#endregion
193732
194028
  //#region src/Edge.ts
193733
194029
  /**
@@ -193810,8 +194106,8 @@ const parseCostTrailers = (messages) => {
193810
194106
  const REVIEW_BASE_TRAILER_PREFIX = "Gtd-Review-Base: ";
193811
194107
  const REVIEW_BASE_TRAILER_RE = /^Gtd-Review-Base:[ \t]*(\S+)[ \t]*$/m;
193812
194108
  /**
193813
- * The `Gtd-Review-Base: <hash>` trailer recorded on a `gtd step <actor>
193814
- * --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`
193815
194111
  * when `message` carries none. Read back by `computeProcessRun` — ONLY off
193816
194112
  * the process's first (oldest) commit — to override the run's diff base.
193817
194113
  */
@@ -193906,6 +194202,27 @@ const pendingChanges = (git, base) => git.changedPaths(base).pipe(map$4((entries
193906
194202
  path: e.path
193907
194203
  }))));
193908
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
+ /**
193909
194226
  * The `Gtd-Review-Base:`/`Gtd-Var:` overrides carried by the process's OLDEST
193910
194227
  * commit (its entry commit, when `planEntry` started this process) — a later
193911
194228
  * turn's message is never mistaken for it. `{reviewBase: undefined, vars:
@@ -193958,7 +194275,8 @@ const computeProcessRun = (git, def, hasCommits, head) => gen(function* () {
193958
194275
  diffBase: EMPTY_TREE$2,
193959
194276
  trace: [],
193960
194277
  costEntries: [],
193961
- entryVars: {}
194278
+ entryVars: {},
194279
+ headTurn: void 0
193962
194280
  };
193963
194281
  const initialState = initialStateOf(def);
193964
194282
  const history = yield* git.commitHistory(void 0, head);
@@ -193984,7 +194302,8 @@ const computeProcessRun = (git, def, hasCommits, head) => gen(function* () {
193984
194302
  diffBase: reviewBaseOverride ?? startParentHash,
193985
194303
  trace,
193986
194304
  costEntries,
193987
- entryVars
194305
+ entryVars,
194306
+ headTurn: headTurnFrom(def, history)
193988
194307
  };
193989
194308
  });
193990
194309
  /**
@@ -194053,6 +194372,35 @@ const memoryKeyFor = (scopes, rest, run) => {
194053
194372
  const token = entryIndex <= 0 ? run.startParentHash : run.trace[entryIndex - 1].hash;
194054
194373
  return `${scope || ROOT_MEMORY_SCOPE_NAME}#${token.slice(0, 7)}`;
194055
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
+ };
194056
194404
  const PREFIX = "GTD_";
194057
194405
  /**
194058
194406
  * Assemble the merged `it.vars` map every template sees, from four layers
@@ -194060,7 +194408,7 @@ const PREFIX = "GTD_";
194060
194408
  * (`ConfigOperations.workflowVars`), the top-level `.gtdrc` `vars:` key
194061
194409
  * (`ConfigOperations.rcVars`), the current process's entry commit's
194062
194410
  * `Gtd-Var:` trailers (`ProcessRun.entryVars` — fixed overrides recorded at
194063
- * the moment a process like `gtd step <actor> --entry <state>` started it),
194411
+ * the moment a process like `gtd --entry <state>` started it),
194064
194412
  * and — for each name declared by any of those three layers — a
194065
194413
  * `GTD_<UPPERCASE-name>` environment variable, if defined. Unlike the first
194066
194414
  * three layers, the environment can only OVERRIDE a name some earlier layer
@@ -194238,6 +194586,7 @@ const restAt = (ref) => gen(function* () {
194238
194586
  const changes = yield* pendingChanges(git, windowHead);
194239
194587
  const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase, hasCommits);
194240
194588
  const memory = memoryKeyFor(config.stateScopes, resolved, run);
194589
+ const memoryResumed = memoryResumedFor(def, config.stateScopes, resolved, run);
194241
194590
  const hints = yield* renderHints(resolved.stateDef, context);
194242
194591
  return {
194243
194592
  ...resolved,
@@ -194247,6 +194596,7 @@ const restAt = (ref) => gen(function* () {
194247
194596
  stepDef,
194248
194597
  changes,
194249
194598
  memory,
194599
+ memoryResumed,
194250
194600
  hints,
194251
194601
  context
194252
194602
  };
@@ -194305,11 +194655,39 @@ const renderRest = (rest) => gen(function* () {
194305
194655
  content,
194306
194656
  ...rest.hints,
194307
194657
  ...rest.memory !== void 0 ? { memory: rest.memory } : {},
194658
+ memoryResumed: rest.memoryResumed,
194308
194659
  edges: rest.context.edges
194309
194660
  };
194310
194661
  });
194311
- /** The user-facing message for a `step` refusal — out-of-turn names the awaited actor, no-match names every declared pattern. */
194312
- 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)"}`;
194313
194691
  /**
194314
194692
  * Build the `TemplateContext` for rendering a DIFFERENT state than `rest`'s
194315
194693
  * own resting one, with a step's `cost`/`model` folded in. A squash's commit
@@ -194317,7 +194695,7 @@ const formatStepRefusal = (invoker, refusal) => refusal.reason === "out-of-turn"
194317
194695
  * `TemplateContext` at all), and it is NOT interchangeable with `rest.context`:
194318
194696
  * that one is pinned to the resting state and carries `cost: 0`, so rendering
194319
194697
  * `it.processCost` against it silently omits the squashing step's own
194320
- * `--cost`. Exported for `program.ts`'s `stepAsActor`, which assembles the
194698
+ * `--cost`. Exported for `program.ts`'s `planLanding`, which assembles the
194321
194699
  * same script by hand and must pick the same context.
194322
194700
  */
194323
194701
  const contextAt = (rest, targetState, invoker, cost, model) => gen(function* () {
@@ -194328,117 +194706,129 @@ const contextAt = (rest, targetState, invoker, cost, model) => gen(function* ()
194328
194706
  return yield* buildTemplateContext(git, templateRead(files), targetState, invoker, rest.run, rest.vars, onEdges, cost ?? 0, model, reviewBaseFor(rest.def, rest.run), hasCommits);
194329
194707
  });
194330
194708
  /**
194331
- * Execute a `"commit"`/`"squash"` decision: a `"commit"` decision stages and
194332
- * commits everything pending under the decided subject, with an optional
194333
- * `Gtd-Cost: <cost>[ <model>]` trailer; a `"squash"` decision renders the
194334
- * commit-state template against the PENDING tree — a render failure REFUSES
194335
- * the step, touching nothing — then records the pre-squash tip on the
194336
- * retained-history ref (`retainHistory`, a no-op for an empty process) before
194337
- * soft-resetting to the process's start parent, writes ONE commit with the
194338
- * rendered message plus a `Gtd-History: <hash>` trailer pointing at that tip,
194339
- * and discards everything left pending (the template file included). The
194340
- * whole-process total and per-model breakdown reach the message through
194341
- * `it.processCost`/`it.processCostByModel` in the rendered template.
194342
- */
194343
- const executeDecision = (git, run, decision, context, cost, model) => gen(function* () {
194344
- switch (decision.kind) {
194345
- case "commit":
194346
- yield* git.commitAllWithPrefix(withCostTrailer(decision.subject, cost, model));
194347
- return {
194348
- kind: "commit",
194349
- subject: decision.subject
194350
- };
194351
- case "squash": {
194352
- const message = yield* try_({
194353
- try: () => renderStateTemplate(decision.template, context),
194354
- catch: (e) => /* @__PURE__ */ new Error(`gtd: rendering the "${decision.state}" commit template failed — nothing was committed: ${e instanceof Error ? e.message : String(e)}`)
194355
- });
194356
- const tip = yield* git.resolveRef("HEAD");
194357
- yield* retainHistory(git, tip, run.startParentHash);
194358
- yield* git.softResetTo(run.startParentHash);
194359
- yield* git.commitAsIs(withHistoryTrailer(message, tip));
194360
- yield* git.discardPending();
194361
- return {
194362
- kind: "squash",
194363
- subject: subjectOf(message)
194364
- };
194365
- }
194366
- }
194367
- });
194368
- /**
194369
194709
  * Render a `"commit"`/`"squash"` decision as the `EmitStep`s the external
194370
- * driver would run to produce the SAME effect `executeDecision` performs
194371
- * directly — same switch, same subjects, same trailers, same ordering. Every
194372
- * git call here is a READ (`git.resolveRef("HEAD")` for the squash branch's
194373
- * pre-squash tip); nothing is written. `planStep`/`planEntry`'s script
194374
- * assembly (`buildStepScripts`) and `Edge.test.ts`'s render/perform
194375
- * equivalence tests are its two callers.
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.
194376
194714
  *
194377
194715
  * The squash branch renders the commit-state template against the PENDING
194378
- * tree — a render failure fails this Effect exactly like `executeDecision`'s
194379
- * own (same message), touching no git. On success it emits, in order:
194380
- * retain-history (`updateRef(HISTORY_REF, tip)`, OMITTED when `tip ===
194381
- * run.startParentHash` — an empty process retains nothing, mirroring
194382
- * `retainHistory`'s own no-op check, decided here since `tip` is already
194383
- * known), soft-reset to the process start, commit-as-is with the rendered
194384
- * message plus its `Gtd-History:` trailer, then discard-pending.
194385
- *
194386
- * The commit branch carries `executeDecision`'s one non-obvious case too: a
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
194387
194726
  * commit whose target is the workflow's INITIAL state, from a process that
194388
194727
  * retained nothing, is a no-op probe (`gtd --entry fix-precheck` against a
194389
194728
  * green suite is the shipped example) and must leave no trace at all. It
194390
194729
  * emits retain-history + a mixed reset to the process start instead of a
194391
194730
  * commit, so the entry commit and the probe collapse away together rather
194392
- * than dirtying the log with a round trip to `idle`. Deciding it needs the
194393
- * whole `rest` (its definition for the initial state, its pending changes for
194394
- * the "retained nothing" test), which is why this takes a `Rest` rather than
194395
- * the `ProcessRun` alone.
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.
194396
194780
  */
194781
+ const collapsesToInitialState = (rest, decision) => gen(function* () {
194782
+ const git = yield* GitService;
194783
+ return yield* collapsesWith(git, rest, decision);
194784
+ });
194397
194785
  const renderDecision = (git, rest, decision, context, cost, model) => gen(function* () {
194398
194786
  const run = rest.run;
194399
194787
  switch (decision.kind) {
194400
194788
  case "commit":
194401
- if (parseStateSubject(decision.subject)?.state === initialStateOf(rest.def) && run.startParentHash !== EMPTY_TREE$2 && (yield* retainsNothing(git, run, rest.changes))) {
194789
+ if (yield* collapsesWith(git, rest, decision)) {
194402
194790
  const tip = yield* git.resolveRef("HEAD");
194403
- const steps = [];
194404
- if (tip !== run.startParentHash) steps.push({
194405
- kind: "gitWrite",
194406
- command: updateRef(HISTORY_REF, tip)
194407
- });
194408
- steps.push({
194409
- kind: "gitWrite",
194410
- command: mixedResetTo(run.startParentHash)
194411
- });
194412
- return steps;
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
+ ];
194413
194802
  }
194414
194803
  return [{
194415
194804
  kind: "gitWrite",
194416
194805
  command: commitAll(withCostTrailer(decision.subject, cost, model))
194417
- }];
194806
+ }, commitDecisionOutcome(decision)];
194418
194807
  case "squash": {
194419
194808
  const message = yield* try_({
194420
194809
  try: () => renderStateTemplate(decision.template, context),
194421
194810
  catch: (e) => /* @__PURE__ */ new Error(`gtd: rendering the "${decision.state}" commit template failed — nothing was committed: ${e instanceof Error ? e.message : String(e)}`)
194422
194811
  });
194423
194812
  const tip = yield* git.resolveRef("HEAD");
194424
- const steps = [];
194425
- if (tip !== run.startParentHash) steps.push({
194426
- kind: "gitWrite",
194427
- command: updateRef(HISTORY_REF, tip)
194428
- });
194429
- steps.push({
194430
- kind: "gitWrite",
194431
- command: softResetTo(run.startParentHash)
194432
- });
194433
- steps.push({
194434
- kind: "gitWrite",
194435
- command: commitAsIs(withHistoryTrailer(message, tip))
194436
- });
194437
- steps.push({
194438
- kind: "gitWrite",
194439
- command: discardPending()
194440
- });
194441
- return steps;
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
+ ];
194442
194832
  }
194443
194833
  }
194444
194834
  });
@@ -194479,64 +194869,57 @@ const buildStepScripts = (git, rest, decision, context, cost, model) => gen(func
194479
194869
  return emitScripts(yield* buildPreconditions(git, rest, targetState), yield* renderDecision(git, rest, decision, context, cost, model).pipe(catchAll(() => succeed$3([]))));
194480
194870
  });
194481
194871
  /**
194482
- * Decide what invoking `invoker` at `rest` does (`PatternMachine.step` over
194483
- * `rest.stepDef`/`rest.changes`/`rest.run.trace`), and — for a `"commit"`/
194484
- * `"squash"` decision — build the `perform` Effect that actually writes it.
194485
- * `opts.cost`/`opts.model` ride along for the `Gtd-Cost:` trailer and the
194486
- * squash template's folded `it.processCost`/`it.processCostByModel`.
194487
- *
194488
- * `perform` itself, in order: the "returned to the initial state retaining
194489
- * nothing" mixed-reset branch (a green re-entry into the initial state that
194490
- * kept nothing useful is rewound like `gtd abandon` rather than committed),
194491
- * else `executeDecision` — against `rest.context` for a commit (which never
194492
- * reads it) or a freshly built `contextAt` for a squash (which renders a
194493
- * DIFFERENT state's template with the step's cost folded in).
194494
- */
194495
- const planStep = (rest, invoker, opts = {}) => gen(function* () {
194496
- const decision = step(rest.stepDef, rest.state, invoker, {
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, {
194497
194898
  changes: rest.changes,
194498
194899
  processTrace: rest.run.trace.map((entry) => entry.state)
194499
194900
  });
194500
194901
  if (decision.kind === "refusal") return {
194501
194902
  kind: "refusal",
194502
- message: formatStepRefusal(invoker, decision)
194903
+ message: formatStepRefusal(decision)
194503
194904
  };
194504
194905
  if (decision.kind === "noop") return {
194505
194906
  kind: "noop",
194506
- state: decision.state
194907
+ state: decision.state,
194908
+ settled: noOpSettles(rest)
194507
194909
  };
194508
194910
  const { cost, model } = opts;
194509
- const perform = gen(function* () {
194510
- const git = yield* GitService;
194511
- if (decision.kind === "commit") {
194512
- const target = parseStateSubject(decision.subject)?.state;
194513
- if (target === initialStateOf(rest.def) && rest.run.startParentHash !== EMPTY_TREE$2 && (yield* retainsNothing(git, rest.run, rest.changes))) {
194514
- yield* retainHistory(git, yield* git.resolveRef("HEAD"), rest.run.startParentHash);
194515
- yield* git.mixedResetTo(rest.run.startParentHash);
194516
- return {
194517
- kind: "reset",
194518
- state: target
194519
- };
194520
- }
194521
- return yield* executeDecision(git, rest.run, decision, rest.context, cost, model);
194522
- }
194523
- const context = yield* contextAt(rest, decision.state, invoker, cost, model);
194524
- return yield* executeDecision(git, rest.run, decision, context, cost, model);
194525
- });
194526
194911
  const git = yield* GitService;
194527
- const scriptContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, invoker, cost, model);
194912
+ const scriptContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, rest.actor, cost, model);
194528
194913
  const scripts = yield* buildStepScripts(git, rest, decision, scriptContext, cost, model);
194529
194914
  return {
194530
194915
  kind: decision.kind,
194531
194916
  state: rest.state,
194532
194917
  decision,
194533
- perform,
194534
194918
  scripts
194535
194919
  };
194536
194920
  });
194537
194921
  /**
194538
- * `gtd step <actor> --entry <state>` (or its subcommand-less short form `gtd
194539
- * --entry <state>`): start a brand NEW process at `entry.state` — any
194922
+ * `gtd --entry <state>`: start a brand NEW process at `entry.state` — any
194540
194923
  * declared, non-commit state (see `enterableStates`) — writing an ordinary
194541
194924
  * turn commit (`gtd(<actor>): <state>`) carrying zero or more `Gtd-Var:
194542
194925
  * <name>=<value>` trailers for each `entry.vars` override, plus — when
@@ -194544,7 +194927,7 @@ const planStep = (rest, invoker, opts = {}) => gen(function* () {
194544
194927
  * trailer pinning the new process's diff base (rendered from that template
194545
194928
  * against the merged `it.vars`, resolved to a commit, and checked sane).
194546
194929
  * Commits via `commitAllWithPrefix` — capturing whatever the working tree
194547
- * 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,
194548
194931
  * rather than demanding a clean tree first.
194549
194932
  *
194550
194933
  * All validation is a REFUSAL, not an Effect failure — checked in order:
@@ -194617,23 +195000,17 @@ const planEntry = (rest, actor, entry) => gen(function* () {
194617
195000
  ...base !== void 0 ? { base } : {},
194618
195001
  vars: varOverrides
194619
195002
  });
194620
- const perform = gen(function* () {
194621
- yield* (yield* GitService).commitAllWithPrefix(message);
194622
- return {
194623
- kind: "entry",
194624
- state: entryState,
194625
- subject
194626
- };
194627
- });
194628
195003
  const scriptsGit = yield* GitService;
194629
195004
  return {
194630
195005
  kind: "entry",
194631
195006
  state: entryState,
194632
195007
  subject,
194633
- perform,
194634
195008
  scripts: emitScripts(yield* buildPreconditions(scriptsGit, rest, entryState), [{
194635
195009
  kind: "gitWrite",
194636
195010
  command: commitAll(message)
195011
+ }, {
195012
+ kind: "outcome",
195013
+ command: commitOutcome(subject)
194637
195014
  }])
194638
195015
  };
194639
195016
  });
@@ -205942,7 +206319,7 @@ const renderSteeringCommands = (resolved, file, context) => gen(function* () {
205942
206319
  * mode with no built-in format at all (a workflow-only name, or a declared
205943
206320
  * `{}` entry) dispatches to no symbols, no actions, and no diagnostics. Either
205944
206321
  * way the file is still formatted and validated by `gtd validate` and the
205945
- * `gtd step` capture gate — just not live in the editor.
206322
+ * `gtd land` capture gate — just not live in the editor.
205946
206323
  *
205947
206324
  * `gtd.openSteeringFile` (an `executeCommand`) resolves the CURRENT state
205948
206325
  * exactly like the CLI (`resolveRest`/`computeProcessRun`/
@@ -206619,15 +206996,16 @@ const stepGuards = [
206619
206996
  }
206620
206997
  ];
206621
206998
  /**
206622
- * The `gtd step` capture gate: run every guard the resolved rest's state
206999
+ * The `gtd land` capture gate: run every guard the resolved rest's state
206623
207000
  * applies to against ONE sample of the current committed/working bytes — see
206624
207001
  * the module docstring — and fail with the first refusal's reason, prefixed
206625
- * `gtd step <invoker>: ` (the one place that prefix, and the `cliErrorLine`
206626
- * `/^gtd[: ]/` contract, is satisfied). A no-op for a squash/no-op decision, a
206627
- * 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.
206628
207006
  */
206629
207007
  const enforceStepGuards = (input) => gen(function* () {
206630
- if (input.kind !== "commit") return;
207008
+ if (input.kind !== "commit" || input.attempt) return;
206631
207009
  const file = input.file;
206632
207010
  if (file === void 0) return;
206633
207011
  const applicable = stepGuards.filter((g) => g.appliesTo(input.rest));
@@ -206652,10 +207030,349 @@ const enforceStepGuards = (input) => gen(function* () {
206652
207030
  };
206653
207031
  for (const g of applicable) {
206654
207032
  const refusal = yield* g.check(ctx);
206655
- 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}`));
206656
207034
  }
206657
207035
  });
206658
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
+ });
207135
+ /**
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");
207149
+ });
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
206659
207376
  //#region src/program.ts
206660
207377
  /**
206661
207378
  * `gtd lsp`: start the LSP server for `.gtd/` steering files over stdio. Its
@@ -206667,6 +207384,23 @@ const enforceStepGuards = (input) => gen(function* () {
206667
207384
  */
206668
207385
  const runLspCommand = () => startLspServer();
206669
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
+ /**
206670
207404
  * `gtd init`: scaffold a MINIMAL `.gtdrc.json` seeding the default variables a
206671
207405
  * fresh project is most likely to change — the test command (`vars.testCommand`)
206672
207406
  * and a ready-to-edit Prettier formatting suggestion (`modes:`). It writes NO
@@ -206680,7 +207414,7 @@ const runLspCommand = () => startLspServer();
206680
207414
  * instead (`assertInitLocation`, which also allows a directory outside any
206681
207415
  * repository) since it writes `.gtdrc.json` at the root and refuses to
206682
207416
  * clobber an existing config. The file is left UNCOMMITTED, so the message
206683
- * 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
206684
207418
  * counts as a pending change the initial state's `* **` edge would otherwise
206685
207419
  * capture).
206686
207420
  */
@@ -206697,8 +207431,10 @@ const runInitCommand = (json, write) => gen(function* () {
206697
207431
  written: ".gtdrc.json",
206698
207432
  inRepo
206699
207433
  }) + "\n");
206700
- 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"));
206701
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;
206702
207438
  const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
206703
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. */
206704
207440
  const headPreconditions = (currentCommit) => ({ expectedHead: currentCommit });
@@ -206742,6 +207478,8 @@ const openWindowScript = (rest, targetState) => {
206742
207478
  })
206743
207479
  }]).optional;
206744
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;
206745
207483
  /**
206746
207484
  * The subject the required script WILL produce. A `"commit"` decision's is
206747
207485
  * fully deterministic already; a `"squash"` decision's is only known once its
@@ -206754,33 +207492,35 @@ const previewSubject = (decision, rest) => decision.kind === "commit" ? succeed$
206754
207492
  * The `required` half: everything that decides what lands in git, in order —
206755
207493
  * the review-window CLOSE (when one is open), the resting state's own
206756
207494
  * steering-mode format/validate commands, then the commit/squash steps
206757
- * themselves.
206758
- */
206759
- const buildRequiredScript = (rest, decision, invoker, cost, model) => gen(function* () {
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* () {
206760
207503
  const git = yield* GitService;
206761
207504
  const closeDecision = yield* decideCloseWindow;
206762
- const commitContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, invoker, cost, model);
207505
+ const commitContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, rest.actor, cost, model);
207506
+ const isAttempt = isAttemptDecision(decision);
206763
207507
  const steps = [
206764
207508
  ...closeDecision.shouldClose ? [{
206765
207509
  kind: "gitWrite",
206766
207510
  command: buildCloseWindowScript(closeDecision.refs)
206767
207511
  }] : [],
206768
- ...yield* steeringModeSteps(rest),
207512
+ ...isAttempt ? [] : yield* steeringModeSteps(rest),
206769
207513
  ...yield* renderDecision(git, rest, decision, commitContext, cost, model)
206770
207514
  ];
206771
207515
  return emitScripts(headPreconditions(rest.context.currentCommit), steps).required;
206772
207516
  });
206773
- /** A step's `--cost`/`--model` as `planStep` options — each key OMITTED (never `undefined`-valued) when the flag was not given. */
206774
- const stepOptions = (cost, model) => ({
206775
- ...cost !== void 0 ? { cost } : {},
206776
- ...model !== void 0 ? { model } : {}
206777
- });
206778
207517
  /**
206779
- * Authenticate `invoker` against the currently resolved rest and decide the
206780
- * one resulting transition (commit or squash) for `gtd step` — WITHOUT
206781
- * performing it. `currentRest` → `planStep` decides; the step-capture guards
206782
- * (`src/StepGuards.ts`) then refuse before anything is emitted, exactly as
206783
- * before. What used to be `plan.perform` is now assembled by hand into a
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
206784
207524
  * `required` script (the review-window CLOSE, when one is open; the resting
206785
207525
  * state's steering-mode format/validate commands, when it declares
206786
207526
  * `file:`+`mode:`; then the commit/squash steps themselves, via
@@ -206796,17 +207536,21 @@ const stepOptions = (cost, model) => ({
206796
207536
  * not the real head — and it contains no window close/open steps at all. This
206797
207537
  * function builds its own required/optional pair to sidestep that trap.
206798
207538
  */
206799
- const stepAsActor = (invoker, cost, model) => gen(function* () {
207539
+ const planLanding = (opts = {}) => gen(function* () {
206800
207540
  const rest = yield* currentRest;
206801
- const plan = yield* planStep(rest, invoker, stepOptions(cost, model));
207541
+ const plan = yield* planStep(rest, opts);
206802
207542
  if (plan.kind === "refusal") return yield* fail$2(new Error(plan.message));
206803
207543
  if (plan.kind === "noop") return {
206804
207544
  state: plan.state,
206805
207545
  subject: null,
206806
207546
  cost: null,
206807
207547
  model: null,
206808
- required: "",
206809
- optional: ""
207548
+ required: emitScripts({}, [{
207549
+ kind: "outcome",
207550
+ command: noteOutcome(noopText(plan.state))
207551
+ }]).required,
207552
+ optional: "",
207553
+ settled: plan.settled
206810
207554
  };
206811
207555
  const decision = plan.decision;
206812
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}"`));
@@ -206815,48 +207559,57 @@ const stepAsActor = (invoker, cost, model) => gen(function* () {
206815
207559
  context: rest.context,
206816
207560
  file: rest.hints.file,
206817
207561
  changes: rest.changes,
206818
- invoker,
206819
- kind: decision.kind
207562
+ kind: decision.kind,
207563
+ attempt: isAttemptDecision(decision)
206820
207564
  });
206821
207565
  const targetState = decision.kind === "commit" ? decision.to : decision.state;
206822
207566
  return {
206823
207567
  state: rest.state,
206824
207568
  subject: yield* previewSubject(decision, rest),
206825
- cost: cost ?? null,
206826
- model: model ?? null,
206827
- required: yield* buildRequiredScript(rest, decision, invoker, cost, model),
206828
- optional: openWindowScript(rest, targetState)
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)
206829
207574
  };
206830
207575
  });
206831
- /** Renders `stepAsActor`'s result for `gtd step`. Plain text names the (previewed) resulting subject exactly as before; `--json` additionally carries `required`/`optional` — the bash the external driver runs to actually land it. */
206832
- 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);
206833
207579
  if (json) write(JSON.stringify({
206834
207580
  state: result.state,
206835
207581
  subject: result.subject,
206836
207582
  ...result.cost !== null ? { cost: result.cost } : {},
206837
207583
  ...result.model !== null ? { model: result.model } : {},
207584
+ script,
206838
207585
  required: result.required,
206839
- optional: result.optional
207586
+ optional: result.optional,
207587
+ settled: result.settled
206840
207588
  }) + "\n");
206841
- else write(result.subject !== null ? `committed: ${result.subject}\n` : `nothing to do at "${result.state}"\n`);
206842
- };
206843
- /**
206844
- * `gtd step <actor> [--cost=<n>] [--model=<name>]`: authenticate as `<actor>`
206845
- * and perform the one resulting transition, recording `--cost`/`--model` as a
206846
- * `Gtd-Cost:` trailer. `--entry <state>` no longer nests inside this handler —
206847
- * `Cli.ts`'s `--entry` selector resolves that combination to its own
206848
- * `"entry"` command kind (see `runEntryCommand`) before `runCommand` ever
206849
- * dispatches here, so a `step` `Command` is always the ordinary pattern-
206850
- * matched step.
206851
- */
206852
- const runStepCommand = (actor, cost, model, json, write) => gen(function* () {
206853
- const result = yield* stepAsActor(actor, cost, model);
206854
- 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;
206855
207609
  });
206856
207610
  /**
206857
- * `gtd step <actor> --entry <state> [--var <name>=<value> ...]` (or its
206858
- * subcommand-less short form `gtd --entry <state> ...`, `actor` defaulting to
206859
- * `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>`
206860
207613
  * — any declared, non-commit state (see `PatternMachine.enterableStates`) —
206861
207614
  * replacing the two former named commands `gtd review <commitish>`/`gtd fix`
206862
207615
  * with one generic mechanism. Writes an ordinary turn commit
@@ -206867,7 +207620,7 @@ const runStepCommand = (actor, cost, model, json, write) => gen(function* () {
206867
207620
  * the merged `it.vars`, resolved to a commit, and checked sane). Unlike the
206868
207621
  * old commands (which required a clean tree and used `commitAsIs`), this
206869
207622
  * commits via `commitAllWithPrefix` — capturing whatever the working tree
206870
- * 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`
206871
207624
  * capture, rather than demanding a clean tree first.
206872
207625
  *
206873
207626
  * Any failure below is a plain refusal: nothing is written. Checked in order:
@@ -206900,7 +207653,7 @@ const runEntryCommand = (actor, entryState, varOverrides, json, write, commandLa
206900
207653
  required: plan.scripts.required,
206901
207654
  optional: plan.scripts.optional
206902
207655
  }) + "\n");
206903
- else write(`committed: ${plan.subject}\n`);
207656
+ else write(combinedScript(plan.scripts.required, plan.scripts.optional));
206904
207657
  });
206905
207658
  /**
206906
207659
  * `gtd abandon`: end the process currently underway WITHOUT completing it,
@@ -206947,18 +207700,23 @@ const runAbandonCommand = (json, write) => gen(function* () {
206947
207700
  const initial = initialStateOf(def);
206948
207701
  const run = yield* currentRun;
206949
207702
  if (run.trace.length === 0) {
207703
+ const required = emitScripts({}, [{
207704
+ kind: "outcome",
207705
+ command: abandonNoopOutcome(initial)
207706
+ }]).required;
206950
207707
  if (json) write(JSON.stringify({
206951
207708
  state: initial,
206952
- abandoned: false
207709
+ abandoned: false,
207710
+ required,
207711
+ optional: ""
206953
207712
  }) + "\n");
206954
- else write(`no gtd process is underway (resting at "${initial}") — nothing to abandon\n`);
207713
+ else write(combinedScript(required, ""));
206955
207714
  return;
206956
207715
  }
206957
207716
  const restState = run.trace[run.trace.length - 1].state;
206958
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`));
206959
207718
  const closeDecision = yield* decideCloseWindow;
206960
207719
  const tip = closeDecision.shouldClose ? closeDecision.refs.headHash : yield* git.resolveRef("HEAD");
206961
- const subject = yield* git.lastCommitSubject(run.startParentHash);
206962
207720
  const steps = [
206963
207721
  ...closeDecision.shouldClose ? [{
206964
207722
  kind: "gitWrite",
@@ -206971,6 +207729,10 @@ const runAbandonCommand = (json, write) => gen(function* () {
206971
207729
  {
206972
207730
  kind: "gitWrite",
206973
207731
  command: mixedResetTo(run.startParentHash)
207732
+ },
207733
+ {
207734
+ kind: "outcome",
207735
+ command: abandonedOutcome(restState, run.startParentHash, initial)
206974
207736
  }
206975
207737
  ];
206976
207738
  const required = emitScripts(headPreconditions(yield* git.resolveRef("HEAD")), steps).required;
@@ -206982,7 +207744,7 @@ const runAbandonCommand = (json, write) => gen(function* () {
206982
207744
  required,
206983
207745
  optional: ""
206984
207746
  }) + "\n");
206985
- 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, ""));
206986
207748
  });
206987
207749
  /**
206988
207750
  * `gtd restore`: hard-reset HEAD back to the pre-squash tip a squash or
@@ -207010,14 +207772,20 @@ const runRestoreCommand = (json, write) => gen(function* () {
207010
207772
  const check = yield* restorability(git, headHash, yield* git.lastCommitMessage(), tip);
207011
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)}.`));
207012
207774
  const after = yield* restAt(tip);
207013
- const subject = yield* git.lastCommitSubject(tip);
207014
- const steps = [{
207015
- kind: "gitWrite",
207016
- command: hardResetTo(tip)
207017
- }, {
207018
- kind: "gitWrite",
207019
- command: deleteRef(HISTORY_REF)
207020
- }];
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
+ ];
207021
207789
  const required = emitScripts(headPreconditions(headHash), steps).required;
207022
207790
  if (json) write(JSON.stringify({
207023
207791
  state: after.state,
@@ -207027,7 +207795,7 @@ const runRestoreCommand = (json, write) => gen(function* () {
207027
207795
  required,
207028
207796
  optional: ""
207029
207797
  }) + "\n");
207030
- 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, ""));
207031
207799
  });
207032
207800
  /**
207033
207801
  * The mode's own resolved VALIDATE command, rendered against `file` — the
@@ -207066,50 +207834,135 @@ const resolveSelfValidateCommand = (def, mode, file, context) => gen(function* (
207066
207834
  * human or a simple driver who reads the prompt and hands it to an agent, so
207067
207835
  * the agent self-validates); withheld from `gtd next --json`, where the
207068
207836
  * driving loop instead runs `gtd validate` after the turn and re-prompts on
207069
- * findings (see the `bin/gtd` loop driver). This is advisory: `gtd step`
207070
- * embeds that same command ahead of its own commit and REFUSES a turn whose
207071
- * steering file is invalid (see `stepAsActor`), so a malformed file is never
207072
- * captured whether or not this instruction was followed.
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.
207073
207842
  */
207074
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:`;
207075
207854
  /** True when a rendered rest is a `prompt` turn that hands over a validatable steering file (`file:`+`mode:` both declared). */
207076
207855
  const emitsValidatablePrompt = (rendered) => rendered.kind === "prompt" && rendered.file !== void 0 && rendered.mode !== void 0;
207077
- /** `gtd next [--json]`: pure emitter of the resolved rest's rendered content (no mutation). */
207078
- /** `gtd next --json`'s single-line object — omitting each optional key (never `null`-valued) when its source is unset, exactly like `gtd status --json`. */
207079
- const nextJsonOutput = (rendered) => JSON.stringify({
207080
- state: rendered.state,
207081
- actor: rendered.actor,
207082
- kind: rendered.kind,
207083
- content: rendered.content,
207084
- ...rendered.model !== void 0 ? { model: rendered.model } : {},
207085
- ...rendered.memory !== void 0 ? { memory: rendered.memory } : {},
207086
- ...rendered.label !== void 0 ? { label: rendered.label } : {},
207087
- ...rendered.file !== void 0 ? { file: rendered.file } : {},
207088
- ...rendered.mode !== void 0 ? { mode: rendered.mode } : {},
207089
- ...rendered.edges.length > 0 ? { edges: rendered.edges } : {}
207090
- }) + "\n";
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
+ });
207091
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. */
207092
207890
  const nextPlainOutput = (rendered, selfValidateCommand) => {
207093
207891
  const base = rendered.content.endsWith("\n") ? rendered.content : rendered.content + "\n";
207094
207892
  return emitsValidatablePrompt(rendered) && selfValidateCommand !== void 0 ? base + selfValidateInstruction(selfValidateCommand, rendered.file) : base;
207095
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
+ */
207096
207934
  const runNextCommand = (json, write) => gen(function* () {
207097
207935
  const rest = yield* currentRest;
207098
207936
  const rendered = yield* renderRest(rest);
207099
- const selfValidateCommand = !json && emitsValidatablePrompt(rendered) ? yield* resolveSelfValidateCommand(rest.def, rendered.mode, rendered.file, rest.context).pipe(catchAll(() => succeed$3(void 0))) : void 0;
207100
- write(json ? nextJsonOutput(rendered) : nextPlainOutput(rendered, selfValidateCommand));
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));
207101
207947
  });
207102
207948
  /**
207103
207949
  * `gtd validate [--json]`: emit the script that would format (in place) then
207104
207950
  * validate the steering file the resolved rest declares (`file:` rendered,
207105
- * `mode:` selecting how) — the SAME commands `gtd step`'s capture guard now
207106
- * embeds ahead of its own commit (see `stepAsActor`), rendered here for a
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
207107
207953
  * human or agent to run directly. gtd itself reads no file and executes
207108
207954
  * nothing: a state with no `file:`/`mode:`, or a file absent from the working
207109
207955
  * tree, has nothing to validate (`script: ""`, exit 0 either way) — the
207110
207956
  * verdict now lives in the emitted script's own future exit code, not this
207111
207957
  * command's, so this never fails on a bad file. `RepoFiles`/`FileSystem` is
207112
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).
207113
207966
  */
207114
207967
  const runValidateCommand = (json, write) => gen(function* () {
207115
207968
  const rest = yield* currentRest;
@@ -207124,30 +207977,16 @@ const runValidateCommand = (json, write) => gen(function* () {
207124
207977
  }) + "\n");
207125
207978
  else write(`nothing to validate at "${rest.state}"\n`);
207126
207979
  };
207127
- if (file === void 0 || mode === void 0) {
207980
+ const resolved = yield* resolveValidateScript(rest);
207981
+ if (resolved === void 0) {
207128
207982
  emitNothingToValidate();
207129
207983
  return;
207130
207984
  }
207131
- const fs = yield* FileSystem;
207132
- const toError = (e) => e instanceof Error ? e : new Error(String(e));
207133
- if (!(yield* fs.exists(file).pipe(mapError$1(toError)))) {
207134
- emitNothingToValidate();
207135
- return;
207136
- }
207137
- const resolved = resolveSteeringMode(rest.def, mode);
207138
- if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
207139
- const commands = yield* renderSteeringCommands(resolved, file, rest.context);
207140
- const script = emitScripts(headPreconditions(rest.context.currentCommit), commands.map((command) => ({
207141
- kind: "command",
207142
- command
207143
- }))).required;
207144
207985
  if (json) write(JSON.stringify({
207145
207986
  state: rest.state,
207146
- file,
207147
- mode,
207148
- script
207987
+ ...resolved
207149
207988
  }) + "\n");
207150
- else write(script.length > 0 ? `${script}\n` : `nothing to validate at "${rest.state}"\n`);
207989
+ else write(resolved.script.length > 0 ? `${resolved.script}\n` : `nothing to validate at "${rest.state}"\n`);
207151
207990
  });
207152
207991
  /**
207153
207992
  * `gtd check <mode> <file>`: read `<file>` and run the BUILT-IN steering
@@ -207199,7 +208038,7 @@ const runCheckCommand = (mode, file, json, write) => gen(function* () {
207199
208038
  else write(errors.join("\n") + "\n");
207200
208039
  return yield* fail$2(/* @__PURE__ */ new Error(`gtd check: ${file} is not valid under mode "${mode}" (${errors.length} finding(s))`));
207201
208040
  });
207202
- /** 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. */
207203
208042
  const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
207204
208043
  const matchedRow = onEdges.find(([patternStr]) => {
207205
208044
  const parsed = parsePattern(patternStr);
@@ -207403,20 +208242,20 @@ const assertInitLocation = (git, fs) => gen(function* () {
207403
208242
  */
207404
208243
  const needsOf = (kind) => {
207405
208244
  switch (kind) {
207406
- case "lsp": return "none";
208245
+ case "lsp":
208246
+ case "install": return "none";
207407
208247
  case "init":
207408
208248
  case "check": return "fs";
207409
208249
  case "visualize": return "config";
207410
208250
  default: return "state";
207411
208251
  }
207412
208252
  };
207413
- /** 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. */
207414
- 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) => {
207415
208255
  switch (command.kind) {
207416
208256
  case "lsp": return runLspCommand();
207417
208257
  case "init": return runInitCommand(json, write);
207418
208258
  case "visualize": return runVisualizeCommand(command.port, command.open, json, write);
207419
- case "step": return runStepCommand(command.actor, command.cost, command.model, json, write);
207420
208259
  case "entry": return runEntryCommand(command.actor, command.state, command.vars, json, write, command.label);
207421
208260
  case "abandon": return runAbandonCommand(json, write);
207422
208261
  case "restore": return runRestoreCommand(json, write);
@@ -207424,18 +208263,35 @@ const dispatchCommand = (command, json, write) => {
207424
208263
  case "status": return runStatusCommand(json, write);
207425
208264
  case "validate": return runValidateCommand(json, write);
207426
208265
  case "check": return runCheckCommand(command.mode, command.file, json, write);
208266
+ case "install": return runInstallCommand(json, write);
207427
208267
  }
207428
208268
  };
207429
208269
  /**
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.
208275
+ */
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
+ };
208283
+ /**
207430
208284
  * The one entry point `Cli.ts`'s `runCli` calls for a resolved `Command`:
207431
208285
  * dispatches to the matching `run*Command` handler (see `dispatchCommand`),
207432
208286
  * wrapped in the repo-root guard exactly when `needsOf(command.kind) ===
207433
208287
  * "state"` — `lsp`/`init`/`visualize`/`check` (the `standaloneKinds`) run
207434
208288
  * bare. `Cli.ts` has already validated every field on `command` (arity, flag
207435
- * scope, decoding), so nothing here re-parses argv.
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`).
207436
208292
  *
207437
208293
  * No review-window close/open bracket runs here any more: every state-command
207438
- * handler (see `stepAsActor`) now decides for itself, off `ReviewWindow.ts`'s
208294
+ * handler (see `planLanding`) now decides for itself, off `ReviewWindow.ts`'s
207439
208295
  * pure `decideCloseWindow`/`decideOpenWindow`, whether a close/open belongs in
207440
208296
  * the script it emits — there is no in-process HEAD to rewind ahead of time
207441
208297
  * (gtd itself no longer writes git for a state command at all).
@@ -207447,7 +208303,7 @@ const runCommand = (command, json, write) => {
207447
208303
  const git = yield* GitService;
207448
208304
  const fs = yield* FileSystem;
207449
208305
  yield* assertRunningFromRepoRoot(git, fs);
207450
- yield* dispatch;
208306
+ return yield* dispatch;
207451
208307
  });
207452
208308
  };
207453
208309
  //#endregion
@@ -207506,21 +208362,21 @@ const FLAGS = [
207506
208362
  name: "--cost",
207507
208363
  arity: 1,
207508
208364
  repeatable: false,
207509
- scope: (kind) => kind === "step",
208365
+ scope: (kind) => kind === "land",
207510
208366
  decode: ([raw]) => nonNegativeNumber(raw ?? "", "--cost"),
207511
- 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",
207512
208368
  valueHint: "<n>",
207513
- help: ["(gtd step only) record the invocation's token cost"]
208369
+ help: ["(gtd land only) record the invocation's token cost"]
207514
208370
  },
207515
208371
  {
207516
208372
  name: "--model",
207517
208373
  arity: 1,
207518
208374
  repeatable: false,
207519
- scope: (kind) => kind === "step",
208375
+ scope: (kind) => kind === "land",
207520
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),
207521
- 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",
207522
208378
  valueHint: "<name>",
207523
- help: ["(gtd step only, with --cost) tag that cost's model"]
208379
+ help: ["(gtd land only, with --cost) tag that cost's model"]
207524
208380
  },
207525
208381
  {
207526
208382
  name: "--entry",
@@ -207528,14 +208384,12 @@ const FLAGS = [
207528
208384
  repeatable: false,
207529
208385
  scope: (kind) => kind === "entry",
207530
208386
  decode: ([raw]) => right(raw ?? ""),
207531
- 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",
207532
208388
  valueHint: "<state>",
207533
208389
  help: [
207534
- "(gtd step, or with no command at all) start a brand new",
207535
- "process at <state> — any declared, non-commit state —",
207536
- "instead of stepping the one currently resting. Not",
207537
- "combinable with --cost/--model (an entry is not a metered",
207538
- "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"
207539
208393
  ]
207540
208394
  },
207541
208395
  {
@@ -207589,20 +208443,22 @@ const COMMAND_ROWS = [
207589
208443
  ]
207590
208444
  },
207591
208445
  {
207592
- token: "step",
207593
- kind: "step",
207594
- arity: { name: "actor" },
208446
+ token: "land",
208447
+ kind: "land",
208448
+ arity: "none",
207595
208449
  details: [
207596
- "Authenticate as <actor>, match the resolved rest's",
207597
- "declared patterns against the pending changes, and commit",
207598
- "(or squash) the one resulting transition. Pass",
207599
- "--cost=<n> (optionally --model=<name>) to record the",
207600
- "just-finished invocation's token cost and model on the",
207601
- "turn commit (summed into it.processCost/processCostByModel).",
207602
- "Pass --entry <state> to start a brand NEW process at",
207603
- "<state> instead — any declared, non-commit state — with",
207604
- "repeatable --var <name>=<value> supplying that new",
207605
- "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"
207606
208462
  ]
207607
208463
  },
207608
208464
  {
@@ -207634,7 +208490,14 @@ const COMMAND_ROWS = [
207634
208490
  token: "next",
207635
208491
  kind: "next",
207636
208492
  arity: "none",
207637
- 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
+ ]
207638
208501
  },
207639
208502
  {
207640
208503
  token: "status",
@@ -207654,7 +208517,10 @@ const COMMAND_ROWS = [
207654
208517
  "own exit code/output. Always exits 0; --json emits",
207655
208518
  "{state, file?, mode?, script} (script is \"\" when there is",
207656
208519
  "nothing to validate; plain text prints \"nothing to",
207657
- "validate\" in that case)"
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"
207658
208524
  ]
207659
208525
  },
207660
208526
  {
@@ -207686,6 +208552,19 @@ const COMMAND_ROWS = [
207686
208552
  "with <mode>/<file> given explicitly. This is what a",
207687
208553
  "workflow's emitted validation script invokes as a leaf step"
207688
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
+ ]
207689
208568
  }
207690
208569
  ];
207691
208570
  const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
@@ -207698,8 +208577,10 @@ const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
207698
208577
  * enterable states.
207699
208578
  */
207700
208579
  const REMOVED = {
207701
- 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",
207702
- 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"
207703
208584
  };
207704
208585
  const COLUMN = 19;
207705
208586
  const renderBlock = (header, lines) => {
@@ -207707,14 +208588,7 @@ const renderBlock = (header, lines) => {
207707
208588
  const headerCell = ` ${header}`;
207708
208589
  return (headerCell.length < COLUMN ? `${headerCell.padEnd(COLUMN)}${first ?? ""}\n` : `${headerCell}\n${" ".repeat(COLUMN)}${first ?? ""}\n`) + rest.map((l) => `${" ".repeat(COLUMN)}${l}\n`).join("");
207709
208590
  };
207710
- const PRELUDE_LOOP = renderBlock("(no command), loop", [
207711
- "Launch the loop driver (bin/gtd), which repeatedly drives",
207712
- "an agent through gtd next/gtd step calls until the",
207713
- "workflow rests at a human gate (a non-autonomous state)",
207714
- "or settles. A bare gtd invocation and gtd loop both",
207715
- "launch it identically"
207716
- ]);
207717
- 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>'"]);
207718
208592
  const VERSION_BLOCK = renderBlock("version", ["Print version and exit"]);
207719
208593
  const HELP_BLOCK = renderBlock("help", ["Print this help and exit"]);
207720
208594
  const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--entry", "--var"]);
@@ -207727,7 +208601,14 @@ const renderHelp = () => {
207727
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}>`;
207728
208602
  return renderBlock(header, row.details);
207729
208603
  });
207730
- 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"])}`;
207731
208612
  };
207732
208613
  /**
207733
208614
  * The stderr line for a CLI error: a `gtd: ` prefix UNLESS the message
@@ -207834,6 +208715,12 @@ const decodeFlags = (byFlag) => {
207834
208715
  }
207835
208716
  return right(bag);
207836
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
+ });
207837
208724
  const parseArgv = (argv) => {
207838
208725
  if (argv.includes("--help") || argv.includes("-h")) return {
207839
208726
  kind: "output",
@@ -207859,12 +208746,12 @@ const parseArgv = (argv) => {
207859
208746
  };
207860
208747
  const row = first === void 0 ? void 0 : commandByToken(first);
207861
208748
  const removedMessage = first === void 0 ? void 0 : REMOVED[first];
207862
- const selectsEntry = entryPresent && (first === void 0 || row?.kind === "step");
208749
+ const selectsEntry = entryPresent && first === void 0;
207863
208750
  const kind = selectsEntry ? "entry" : row?.kind;
207864
208751
  if (row === void 0 && removedMessage === void 0 && !selectsEntry) {
207865
208752
  const violation = scopeViolation(void 0, present);
207866
208753
  if (violation !== void 0) return usagePlan(violation, jsonSeen);
207867
- 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());
207868
208755
  return usagePlan(`unknown command '${first}'`, jsonSeen);
207869
208756
  }
207870
208757
  if (removedMessage !== void 0 && !selectsEntry) {
@@ -207875,8 +208762,7 @@ const parseArgv = (argv) => {
207875
208762
  if (kind === "entry" && first === void 0) {
207876
208763
  if (restPositionals.length > 0) return usagePlan(`gtd: too many arguments — expected none, got: ${restPositionals.join(", ")}`, jsonSeen);
207877
208764
  } else {
207878
- const arityRow = kind === "entry" ? commandByToken("step") : row;
207879
- const arityMsg = arityError(first, restPositionals, arityRow.arity);
208765
+ const arityMsg = arityError(first, restPositionals, row.arity);
207880
208766
  if (arityMsg !== void 0) return usagePlan(arityMsg, jsonSeen);
207881
208767
  }
207882
208768
  const violation = scopeViolation(kind, present);
@@ -207885,27 +208771,21 @@ const parseArgv = (argv) => {
207885
208771
  if (isLeft(decoded)) return usagePlan(decoded.left, jsonSeen);
207886
208772
  const bag = decoded.right;
207887
208773
  const json = present.has("--json");
207888
- if (kind === "step") {
208774
+ if (kind === "land") {
207889
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);
207890
208776
  return {
207891
208777
  kind: "command",
207892
- command: {
207893
- kind: "step",
207894
- actor: restPositionals[0],
207895
- ...bag["--cost"] !== void 0 ? { cost: bag["--cost"] } : {},
207896
- ...bag["--model"] !== void 0 ? { model: bag["--model"] } : {}
207897
- },
208778
+ command: buildLandCommand(bag),
207898
208779
  json
207899
208780
  };
207900
208781
  }
207901
208782
  if (kind === "entry") {
207902
- const actor = first === void 0 ? "human" : restPositionals[0];
207903
- const label = first === void 0 ? `gtd --entry ${entryRaw}` : `gtd step ${actor} --entry ${entryRaw}`;
208783
+ const label = `gtd --entry ${entryRaw}`;
207904
208784
  return {
207905
208785
  kind: "command",
207906
208786
  command: {
207907
208787
  kind: "entry",
207908
- actor,
208788
+ actor: "human",
207909
208789
  state: entryRaw,
207910
208790
  vars: bag["--var"] ?? {},
207911
208791
  label
@@ -207979,7 +208859,9 @@ const runCli = (argv, io) => {
207979
208859
  io.stderr(`${cliErrorLine(plan.message)}\n`);
207980
208860
  io.exit(1);
207981
208861
  });
207982
- 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)));
207983
208865
  };
207984
208866
  //#endregion
207985
208867
  //#region src/main.ts