@pmelab/gtd 9.12.0 → 9.14.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.
@@ -30940,19 +30940,26 @@ const make$1 = (layer, memoMap) => {
30940
30940
  const make = make$1;
30941
30941
  //#endregion
30942
30942
  //#region src/Commentary.ts
30943
- /** One line of stderr commentary — gated entirely by how the layer was built, never by the call site. */
30943
+ /**
30944
+ * One line of stderr commentary — `narrate` is gated entirely by how the
30945
+ * layer was built, never by the call site; `warn` is its ungated
30946
+ * counterpart, always written regardless of verbosity.
30947
+ */
30944
30948
  var Narrator = class Narrator extends Tag("Narrator")() {
30945
30949
  /**
30946
30950
  * `write` is the raw stderr sink (`CliIo.stderr` in production, so the
30947
30951
  * in-memory `CliIo` used by `@inmem` e2e scenarios captures narration into
30948
30952
  * the exact same buffer it already captures errors into). `verbose` gates
30949
- * every line at construction time — the resulting service is a no-op
30950
- * writer when `false`, never a per-call check a narrating call site has to
30951
- * remember to make.
30953
+ * every `narrate` line at construction time — the resulting service is a
30954
+ * no-op writer when `false`, never a per-call check a narrating call site
30955
+ * has to remember to make. `warn` bypasses that gate unconditionally.
30952
30956
  */
30953
- static layer = (write, verbose) => succeed$2(Narrator, { narrate: (line) => sync$1(() => {
30954
- if (verbose) write(`${line}\n`);
30955
- }) });
30957
+ static layer = (write, verbose) => succeed$2(Narrator, {
30958
+ narrate: (line) => sync$1(() => {
30959
+ if (verbose) write(`${line}\n`);
30960
+ }),
30961
+ warn: (line) => sync$1(() => write(`${line}\n`))
30962
+ });
30956
30963
  };
30957
30964
  /**
30958
30965
  * An error carrying REMEDIATION alongside its message — the offending config
@@ -189713,7 +189720,7 @@ const STATE_FIELDS = {
189713
189720
  fileRef: true,
189714
189721
  rest: "rendered",
189715
189722
  viz: "field",
189716
- doc: "The agent harness system prompt stamped onto every one of this machine's own `prompt` states — emitted verbatim in `gtd next --json`/`--sh` for the driver to pass to its agent CLI, never interpreted by gtd. Machine-level only: a machine's `prompt` states share one resumed session, and a system prompt must be identical across every call of it. A `./` or `../` value is inlined from the declaring config file's directory. A machine declaring this with no `prompt` state is a config error."
189723
+ doc: "The agent harness system prompt stamped onto every one of this machine's own `prompt` states — emitted verbatim in `gtd next --json` for the driver to pass to its agent CLI, never interpreted by gtd. Machine-level only: a machine's `prompt` states share one resumed session, and a system prompt must be identical across every call of it. A `./` or `../` value is inlined from the declaring config file's directory. A machine declaring this with no `prompt` state is a config error."
189717
189724
  },
189718
189725
  /** The raw-state-name fallback lives in the consumer (driver/viewer), not here — gtd simply omits the field when unset. */
189719
189726
  label: {
@@ -189744,15 +189751,7 @@ const STATE_FIELDS = {
189744
189751
  rest: "verbatim",
189745
189752
  doc: "The steering file's format: the name of a built-in mode (qa/review, validated in-process by gtd) or of a `modes:` entry. gtd formats and validates the file with that mode before capturing a turn out of this state, and the LSP dispatches live diagnostics on the built-in names. Requires a sibling `file:`."
189746
189753
  },
189747
- /** This module's pure functions never read this flag — the window is opened/closed entirely at the edge (`src/ReviewWindow.ts`). */
189748
- reviewWindow: {
189749
- kind: "flag",
189750
- surface: "def",
189751
- authored: "state",
189752
- viz: "flag",
189753
- doc: "When true, gtd opens a review checkout window while the machine rests here — HEAD/index are rewound to the review base so the whole base..HEAD diff surfaces as uncommitted changes in the editor."
189754
- },
189755
- /** Like `reviewWindow`, the engine never reads this — it's history-derived edge data. `isReviewBaseState` never treats a string value as the `true`/window-anchor form. */
189754
+ /** The engine never reads this — it's history-derived edge data. `isReviewBaseState` never treats a string value as the `true`/window-anchor form. */
189756
189755
  reviewBase: {
189757
189756
  kind: "flagOrTemplate",
189758
189757
  surface: "def",
@@ -189840,9 +189839,7 @@ const contentKindOf = (state) => {
189840
189839
  };
189841
189840
  /** The raw template source a state's own content kind carries — `script`/`prompt`/`message`. */
189842
189841
  const contentOf = (state) => state.script ?? state.prompt ?? state.message;
189843
- /** True when a rest at `state` should open the review checkout window. Safe for an unknown state name (returns `false`). */
189844
- const isReviewWindowState = (def, state) => def.states[state]?.reviewWindow === true;
189845
- /** True when `state` anchors the review window's diff base. Safe for an unknown state name (returns `false`). The string/template form of `reviewBase` is NOT a window anchor — this stays narrowed to `=== true` on purpose, never truthy. */
189842
+ /** True when `state` anchors the review's diff base. Safe for an unknown state name (returns `false`). The string/template form of `reviewBase` is NOT this anchor form — this stays narrowed to `=== true` on purpose, never truthy. */
189846
189843
  const isReviewBaseState = (def, state) => def.states[state]?.reviewBase === true;
189847
189844
  /** The named state's `reviewBase` when it is a STRING (an Eta template rendering a commitish that fixes the whole process's diff base) — `undefined` when `reviewBase` is `true`, absent, or `state` doesn't exist. Pure accessor only: rendering the template is an edge concern. */
189848
189845
  const entryBaseTemplateOf = (def, state) => {
@@ -190264,25 +190261,64 @@ const validateState = (def, name, names) => {
190264
190261
  ];
190265
190262
  };
190266
190263
  /**
190264
+ * A `script`/`message` state with no `C` row commits nothing on a clean tree
190265
+ * (the documented no-op default) — that's often deliberate, but silent for a
190266
+ * state whose author never considered the clean case. Warn, don't error.
190267
+ * Three exemptions, all load-bearing:
190268
+ *
190269
+ * - The workflow's initial state — a `C` row there would author a commit on
190270
+ * every bare driver invocation.
190271
+ * - A `prompt` state — its clean step is an ATTEMPT by design, not a no-op.
190272
+ * - A `human`-actor state — `docs/driver.md`'s driver protocol lands a human
190273
+ * gate's OPENING beat unconditionally on every restart while a process
190274
+ * rests there ("Some gates could accept by INACTION..."), specifically
190275
+ * because today that's a harmless no-op when the state has no `C` row. A
190276
+ * `C` row on a human gate would turn every such restart into a real commit
190277
+ * before the human has acted at all — the exact hazard the initial-state
190278
+ * exemption above already exists to avoid, generalized to any human gate a
190279
+ * process can rest at more than once.
190280
+ *
190281
+ * No exemption for a bare `"* **"` catch-all row or a declared `file:` — a
190282
+ * `diff` pattern (including `"* **"`) never matches a clean tree
190283
+ * (`matchesPattern`), so neither says anything about the clean case; a state
190284
+ * with either and no `C` row still no-ops on a clean tree exactly like one
190285
+ * with neither.
190286
+ */
190287
+ const validateHasCRow = (def, name, state) => {
190288
+ if (name === initialStateOf(def)) return [];
190289
+ if (state.actor === "human") return [];
190290
+ const kind = contentKindOf(state);
190291
+ if (kind !== "script" && kind !== "message") return [];
190292
+ if ((state.on ?? []).some(([pattern]) => pattern === "C")) return [];
190293
+ return [`state "${name}" declares no "C" row`];
190294
+ };
190295
+ /**
190267
190296
  * Validate a `WorkflowDefinition`, returning human-readable error strings
190268
- * (empty = valid). Pure — called at config-load time. Checks at least one
190269
- * state, `entries` shape (`validateEntries`), per-state content/actor/mode/
190270
- * file shape (`validateState`), `modes:` shape, and reachability from an
190271
- * entry root (`validateReachability`, only when entries validated clean).
190272
- * Every per-field rule not listed above (`on`/`retry` targets resolving,
190273
- * `mode` naming a known vocabulary, etc.) is declared once in
190274
- * `src/StateFields.ts`'s `STATE_FIELDS` table instead.
190297
+ * (empty = valid) plus non-fatal warning strings. Pure — called at
190298
+ * config-load time. Checks at least one state, `entries` shape
190299
+ * (`validateEntries`), per-state content/actor/mode/file shape
190300
+ * (`validateState`), `modes:` shape, and reachability from an entry root
190301
+ * (`validateReachability`, only when entries validated clean). Every
190302
+ * per-field rule not listed above (`on`/`retry` targets resolving, `mode`
190303
+ * naming a known vocabulary, etc.) is declared once in `src/StateFields.ts`'s
190304
+ * `STATE_FIELDS` table instead.
190275
190305
  */
190276
190306
  const validateDefinition = (def) => {
190277
190307
  const names = Object.keys(def.states);
190278
- if (names.length === 0) return ["workflow must declare at least one state"];
190308
+ if (names.length === 0) return {
190309
+ errors: ["workflow must declare at least one state"],
190310
+ warnings: []
190311
+ };
190279
190312
  const entriesErrors = validateEntries(def, names);
190280
- return [
190281
- ...entriesErrors,
190282
- ...validateModes(def),
190283
- ...names.flatMap((name) => validateState(def, name, names)),
190284
- ...entriesErrors.length === 0 ? validateReachability(def, names) : []
190285
- ];
190313
+ return {
190314
+ errors: [
190315
+ ...entriesErrors,
190316
+ ...validateModes(def),
190317
+ ...names.flatMap((name) => validateState(def, name, names)),
190318
+ ...entriesErrors.length === 0 ? validateReachability(def, names) : []
190319
+ ],
190320
+ warnings: names.flatMap((name) => validateHasCRow(def, name, def.states[name]))
190321
+ };
190286
190322
  };
190287
190323
  //#endregion
190288
190324
  //#region src/Machines.ts
@@ -191873,19 +191909,20 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
191873
191909
  ...summary !== void 0 ? { summary } : {}
191874
191910
  };
191875
191911
  assertScopesCoverStates(Object.keys(states), flattened.scopes, errors);
191876
- const definitionErrors = validateDefinition(definition);
191877
- const allErrors = Array.from(/* @__PURE__ */ new Set([...errors, ...definitionErrors]));
191912
+ const definitionResult = validateDefinition(definition);
191913
+ const allErrors = Array.from(/* @__PURE__ */ new Set([...errors, ...definitionResult.errors]));
191878
191914
  if (allErrors.length > 0) throw new Error(formatErrors(allErrors));
191879
191915
  return {
191880
191916
  definition,
191881
191917
  vars,
191882
191918
  tree: flattened.tree,
191883
- scopes: flattened.scopes
191919
+ scopes: flattened.scopes,
191920
+ warnings: definitionResult.warnings
191884
191921
  };
191885
191922
  };
191886
191923
  //#endregion
191887
191924
  //#region src/workflows/unified.yaml
191888
- var unified_default = "# gtd's BUILT-IN DEFAULT workflow — used when no `workflow:` key is\n# configured anywhere in the cwd→home config chain; `gtd init` seeds only\n# vars.testCommand + a modes: suggestion, never this file. Compiled through\n# the same `compileWorkflowConfig` a user's own `workflow:` goes through — no\n# privileged path. See skills/authoring/SKILL.md for how to read or edit it\n# (machines, patterns, templates) and docs/driver.md for how a driver runs it.\n#\n# One flow: any change to the tree starts the process. `idle` -> `unwind`\n# reverts that diff (intent survives only in history) -> a green-baseline\n# gate -> `design.triage` groups the reverted diff into ordered, classified\n# concerns and resolves the PRODUCT ones' open questions (`design.gate`) ->\n# `architecture.author` works out the *how* and resolves the TECHNICAL ones\n# (`architecture.gate`) -> `architecture.decompose` writes one package file\n# per concern -> the per-package queue (`packages.*`) builds and reviews each\n# -> the shared tail (`build.review.*`) decides sign-off (-> `idle`, landing\n# an ordinary commit that closes the process — every per-turn commit stays on\n# the branch; run `gtd summary` afterward for a closing-message prompt) vs.\n# actionable feedback (-> `re-unwind` -> back to `design.triage`, a full\n# re-plan lap that never builds on the human's hand-edit). `--entry\n# review-gate.check --var reviewBase=<commitish>` and `--entry fix-precheck`\n# are the two other entries into this same flow.\nvars:\n testCommand: npm test\n # plannerModel: the heavier tier for one-shot triage/design/review turns.\n # coderModel: the tier for build/fix turns. Both repointable via a\n # top-level `vars:` key or a `GTD_<NAME>` env var, like any other var.\n plannerModel: smart\n coderModel: base\n # Declared (not left undeclared) so `--var reviewBase=...` passes the CLI's\n # declared-name check; blank renders empty, which the review entry refuses.\n reviewBase: \"\"\n # gtd's own voice for generated files — adapted from the \"Spartan\" output\n # style (https://github.com/alexgreensh/attention-span, AGPL-3.0, version 0.6),\n # rewritten in gtd's own words for deliverables rather than chat replies;\n # no upstream text ships. A point-in-time derivation with no refresh\n # mechanism.\n styleBlock: |-\n This file is a deliverable, not a chat reply — size follows the work, and\n padding is the target of any cut. Lead with the answer immediately; never\n circle back to restate it once it has landed.\n\n Write flat, commanding sentences — commit to the claim, do not hedge it.\n Favor everyday vocabulary; where a term is unavoidable, define it in five\n words or fewer on first use.\n\n In markdown, bold carries the load: bold the claim, not the sentence around\n it — reading only the bold text must yield the full point and every risk.\n\n Never trim a risk, a number, a threshold, or a scoped condition to save\n space. This outranks every other rule here.\n\n Ship the artifact bare — no lead-in, no sign-off wrapped around it.\n Compressing is not dropping: a task with three load-bearing parts ships all\n three, each shorter, never two of three.\n\n One idea per block; break when the idea shifts. Flag risk in one blunt\n line rather than hedged prose. Never narrate the work — do it.\n styleFormatContract: |-\n This file is machine-read; its format contract outranks every style rule\n above it: keep every `##`/`###` heading, every `- [ ]` checkbox row, and\n every marker line exactly as specified — do not renumber or rename a\n heading or replace a checkbox list with prose. A parser reads these\n literally; a violation refuses the turn outright.\n\n Voice rules above govern the prose between these elements, never the\n elements themselves.\n\n # One persona per prompt-bearing machine below, stamped as its `system:`\n # (see src/StateFields.ts's `system`). `--system-prompt` REPLACES a\n # harness's default system prompt — including its injected cwd/env/\n # git-status block — so each persona is written self-contained, restating\n # that context explicitly rather than relying on it.\n\n # Shared conduct tail, appended after every persona's own role paragraph:\n # use tools without asking, orient yourself with git, and go inspect what\n # the turn's message names. Deliberately NOT named `*Persona`:\n # `templates.test.ts` derives its six-name persona set from\n # `it\\.vars\\.(\\w+Persona)`, so that suffix would silently grow the set.\n agentConduct: |-\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. This runs unattended: there is\n no one to grant permission, confirm a command, or approve a plan before\n you act. Investigate with real commands rather than assuming what a file,\n a commit, or a decision's status actually is, then act on what you find.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block — no cwd, no branch, no recent history, no tree state. Run\n `git status`, `git log`, `ls` and the like yourself before you do\n anything else; that is the normal first move of this turn, not an extra\n step.\n\n The message you receive each turn names a commit, a range, or a file for\n you to go inspect yourself — `git show`, `git diff`, reading the file\n directly — never a diff or a summary rendered into the conversation. Go\n look before you act. Across the several turns one conversation may span,\n work from what you already read and settled earlier rather than\n re-deriving it from a cold read every time, unless the turn's own\n instructions tell you this is a fresh, cold pass.\n designPersona: |-\n You are the product-facing planning voice in an autonomous build pipeline\n called gtd. Your job is to turn a raw, half-formed sketch — a hand-edit to\n real code, a scratch note, or both — into an ordered, classified list of\n concerns a build can execute, and to carry a running conversation with a\n human about the product decisions that list depends on. You are read by\n that human, so write plainly and decisively; you are not read by a parser\n except where a state's own instructions say otherwise.\n architectPersona: |-\n You are the technical planning voice in an autonomous build pipeline\n called gtd. You take over once the product-facing concerns are settled,\n reading them cold — you do not carry over the product conversation, only\n its written, settled output. Your job is to work out the technical *how*\n for each settled concern — file and module structure, data models,\n library and tech-stack choices, error-handling strategy — raise the\n technical open questions a human still needs to decide, and once every\n question is answered, write one package spec file per concern with no\n further judgement call: the grouping and ordering were already decided\n upstream, and this final step is a mechanical write-out of a plan you\n already converged on.\n reviewerPersona: |-\n You are the independent reviewing mind in an autonomous build pipeline\n called gtd — deliberately a separate identity from whichever agent wrote\n the code under review, so you bring no attachment to how it got built.\n Your job splits into two distinct turns: first, writing a structured\n review document that groups a diff into reviewable chunks for a human to\n read through and comment on; later, reading back a round of that human's\n feedback — a note, a hand-edit — and classifying whether it is genuinely\n actionable or just an approving remark, without fixing anything yourself\n either way. You judge and you classify; you never build.\n specReviewerPersona: |-\n You are the adversarial spec-conformance checker in an autonomous build\n pipeline called gtd, reviewing one freshly-built work package against the\n written spec it was built from. Your only job is to verify: are the\n package's tasks actually done, its acceptance criteria actually met, and\n the code sound and consistent with the rest of the codebase? You write\n concrete, actionable feedback only when something is genuinely wrong —\n and when nothing is, you write nothing at all, because a clean turn from\n you IS the approval. You never fix a problem you find; naming it\n precisely enough for someone else's fix turn to act on is the whole job.\n builderPersona: |-\n You are the TDD implementer in an autonomous build pipeline called gtd.\n Your job is to build exactly one package's declared scope end to end,\n tests first, and to come back and fix things when redirected: a failing\n check's output, or a reviewer's concrete feedback. Stay strictly inside\n the package in front of you, touching neither another package's files nor\n code outside the current task. Treat feedback you're given as the\n diagnosis to act on; discard it, with reason, only when it is simply\n wrong.\n finisherPersona: |-\n You are the closing identity in an autonomous build pipeline called gtd —\n the last coder this process hands work to before it closes. You are\n handed a late-breaking failing check after sign-off; fix it, focused and\n minimal, before the process can close. Every turn you land stays its own\n commit on the branch — nothing here gets squashed away.\n\n # The scratchpad-opener sentence shared by every prompt-content state (see\n # STATE_FIELDS's `prompt`) — this workflow's own state files are its private\n # working notes, never project code or documentation a human reads. Injected\n # as the leading line of every prompt body that touches a `.gtd/` file\n # directly; each state's own \"the only state file this turn touches is X\"\n # sentence stays local, immediately after the tag. Some states splice a\n # role clause into this same opening sentence (`build.review.collecting`,\n # `packages.item.spec.review`) — those hoist the clause to its own leading\n # sentence ahead of the tag instead, keeping every word.\n stateFileRules: |-\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.\n # The open-questions warrant test, decide-it-yourself sink, and `## Open\n # Questions` checkbox shape shared by `design.triage` and\n # `architecture.author` — written phase-agnostic (no PRODUCT/TECHNICAL\n # bake-in) so both sites can inject it verbatim. Each site's own\n # phase-scope sentence (triage: product-only, technical waits; architecture:\n # every point here is TECHNICAL) stays local around the tag — the two\n # genuinely disagree on scope, so that sentence is never shared.\n # `questionBarReturn` is the return-lap half — split out so each site can\n # inject it under its own `## Return lap` heading instead of burying\n # return-lap behaviour inside the first lap's own section.\n questionBar: |-\n For every open point, a question is warranted only when all three hold:\n the answer is a genuine fork with divergent outcomes — user-visible for a\n product question, materially different implementations for a technical\n one; the sketch, the entry diff, and history do not already settle it\n (treat anything the entry diff states explicitly — a directive in\n `.gtd/TODO.md`, a committed choice in a hand-edit — as already settled,\n and on a review loop-back lap treat whatever the human's own review-round\n edit or note states explicitly the same way); and getting it wrong would\n survive all the way to the human review tail.\n\n Anything below that bar, decide yourself: record the decision under\n `## Answered Questions` — the same section a human-answered question\n lands in — as `### <the point phrased as a question>` followed by the\n decision and a one-line rationale in prose, no checkboxes.\n\n A question that clears the bar is written under a `## Open Questions`\n heading near the top and takes this shape: one `### <question>`\n sub-heading each, followed by a checkbox list of candidate answers — 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.\n questionBarReturn: |-\n The human has ticked one option per question (or replaced\n `_your answer_` with their own text): fold each chosen answer into its\n concern's prose, then move the question into a `## Answered Questions`\n section as `### <question>` followed by the resolved answer in plain\n prose (drop the checkboxes). Never re-raise a deleted question; treat a\n deleted `## Open Questions` section as acceptance. Omit `## Open\n Questions` entirely once there are none left.\n # The body `packages.item.fix-suite` and `build.fix` share byte for byte —\n # both fix a red `.gtd/FEEDBACK.md`. `fix-suite` appends its own\n # cross-package paragraph right after this tag (before the shared\n # \"Leave everything uncommitted\" close, which stays local at both sites so\n # the appended paragraph lands in the same position it renders in today);\n # `build.fix` renders the tag alone.\n fixFeedbackPrompt: |-\n The only state file this turn touches is `.gtd/FEEDBACK.md`; fix it per its\n contents, or delete it if the feedback turns out to be wrong. Do not\n create other files to hold notes or output.\n\n Read `.gtd/FEEDBACK.md` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `.gtd/FEEDBACK.md` instead of \"fixing\" a non-issue.\n\n# `gtd summary`'s prompt — printed cold, no session identity, no diff inlined.\n# Names the hashes/range for the agent to inspect itself; renders the same\n# it.processCost/it.processCostByModel the old squash finale used to.\nsummary: |\n <%~ it.vars.styleBlock %>\n\n\n Write the closing message for the process HEAD closes or currently sits\n inside — the text a driver may use for a squash, an amend, or a pull\n request body. You are starting cold: read every decision back out of the\n commits named below rather than assuming any prior context.\n\n Cover the initial motivation, the important decisions, and the trade-offs\n made, and the high-level architectural changes — never which files\n changed; that is what `git diff --stat` is for, not this message.\n\n The process's entry commit is `<%= it.entryCommit %>`. <% if\n (it.humanCommits.length > 0) { %>The human contributed at these commits,\n oldest to newest:\n <% it.humanCommits.forEach(function (c) { %>\n - `<%= c.hash %>` (entering `<%= c.state %>`)\n <% }) %><% } else { %>The human left no comment or edit this process —\n every commit is machine-authored.<% } %>\n\n Inspect the range yourself: `git log <%= it.processBase %>..<%= it.processTip %>`\n and `git diff <%= it.processBase %> <%= it.processTip %>`. That range is\n exactly what a squash or PR body should describe.\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n\n Print the closing message and stop — this command writes nothing itself.\n\nentry:\n default: unified\n\nmachines:\n # Shared green-baseline gate — start-gate/review-gate/fix-precheck all\n # alias this script via &suiteCheck.\n entryGate:\n params: [onGreen, blockedMessage, blockedDescribe, checkLabel, blockedLabel, reviewBase]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: &suiteCheck |\n #!/usr/bin/env sh\n set +e\n mkdir -p .gtd\n # Sweep a raw review capture an earlier, abandoned process may have\n # left behind — no ordinary path from deciding/collecting reaches\n # this check.\n rm -f .gtd/REVIEW_RAW.md\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output .gtd/FEEDBACK.md\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > .gtd/FEEDBACK.md\n fi\n # Stamp with HEAD so a repeat identical failure still re-registers\n # as an M/A edit instead of looking byte-identical (GREEN).\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> .gtd/FEEDBACK.md\n else\n rm -f .gtd/.check-output\n rm -f .gtd/FEEDBACK.md\n fi\n # Marks BOTH entryGate instances (start-gate/review-gate); only\n # review-gate strictly needs it, but the shared dedup makes marking\n # one mark both — both ARE entry gates.\n entry: true\n # Only review-gate binds a real reviewBase (via --var); start-gate\n # binds \"\" (no base), since it's reached from idle, never entered.\n reviewBase: $reviewBase\n on:\n \"A .gtd/FEEDBACK.md\": blocked\n \"M .gtd/FEEDBACK.md\": blocked\n \"D .gtd/REVIEW_RAW.md\": $onGreen\n \"D .gtd/FEEDBACK.md\": $onGreen\n \"C\": $onGreen\n blocked:\n actor: human\n label: $blockedLabel\n file: FEEDBACK.md\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $blockedDescribe\n\n # Shared check/escalate pair for build.health.*/packages.item.health.* —\n # the caller's own coder fix-state ($onRed), not this machine, handles red.\n healthGate:\n params: [onGreen, onRed, escalateDescribe, checkLabel, escalateLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # See the shared suite check on `entryGate.check` above.\n script: *suiteCheck\n on:\n \"A .gtd/FEEDBACK.md\": $onRed\n \"M .gtd/FEEDBACK.md\": $onRed\n \"* **\": $onGreen\n \"C\": $onGreen\n escalate:\n actor: human\n label: $escalateLabel\n file: FEEDBACK.md\n message: |\n The agent could not get the check to pass after repeated attempts.\n `.gtd/FEEDBACK.md` holds the last failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $escalateDescribe\n\n # Shared check/answer pair for design.gate/architecture.gate. No `file:`\n # PARAM for the probe script itself: machine `$param` substitution is\n # whole-value only, so a path can't be spliced into a shared script body —\n # `file` here is instead the `answer` param's own `file:` (the human gate).\n questionGate:\n params: [file, message, onNone, onRevise, checkLabel, answerLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: |\n #!/usr/bin/env sh\n # Exactly one of REQUIREMENTS.md/ARCHITECTURE.md exists on disk at\n # a time — architecture.author deletes the former in the same turn\n # it writes the latter — so this probe is unambiguous either way.\n set +e\n mkdir -p .gtd\n file=.gtd/REQUIREMENTS.md\n [ -f \"$file\" ] || file=.gtd/ARCHITECTURE.md\n gtd check qa \"$file\" --open-questions > /dev/null 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n printf 'open questions remain in %s\\n' \"$file\" > .gtd/QUESTIONS.md\n # See the shared suite check's cache-buster rationale on\n # `entryGate.check` above.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> .gtd/QUESTIONS.md\n else\n rm -f .gtd/QUESTIONS.md\n fi\n on:\n \"A .gtd/QUESTIONS.md\": answer\n \"M .gtd/QUESTIONS.md\": answer\n \"D .gtd/QUESTIONS.md\": $onNone\n \"C\": $onNone\n answer:\n actor: human\n label: $answerLabel\n file: $file\n mode: qa\n answerGate: true\n message: $message\n on:\n \"* **\":\n to: $onRevise\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace\n `_your answer_` for your own) to send it back for the agent to\n fold your answers in. Delete a question to skip it, or delete\n the whole `## Open Questions` section to accept the plan as-is.\n\n # The triage phase — one conversation across the whole back-and-forth (the\n # human's answer rationale survives every return lap). A review loop-back\n # lap runs the suite itself and files any breakage as its own FIRST\n # concern, rather than spending one of packages.item.fix-suite's\n # (per-episode) retries on breakage the package didn't cause.\n # ▸ planner identity.\n designPlan:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.designPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onArchitecture]\n entry: triage\n states:\n triage:\n actor: agent\n label: Triaging the change\n file: REQUIREMENTS.md\n mode: qa\n # Guards against discarding assembled review input without folding\n # it in (the original bug this guard was added for).\n requireProgress: true\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this turn touches is `.gtd/REQUIREMENTS.md`; develop it. Do\n not create other files to hold notes or output.\n\n `.gtd/TODO.md` is the likely home of the sketch that\n started this process. Despite living beside this workflow's own\n state files, it is the human's input — the sketch to be folded into\n the concerns below like any other part of the start diff. It is\n never a state file to preserve, and never gtd bookkeeping to ignore.\n\n This process started at commit `<%= it.startCommit %>`. The change\n that started it was reverted out of the working tree by the\n `unwind` beat right after it landed, so `git diff <%= it.startCommit %>`\n (against the working tree) is now empty — its content survives only\n in history. Find the entry commit yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\n (the process's very first turn, landed immediately after\n `<%= it.startCommit %>`, before any later baseline-repair commits) —\n then `git show` that commit to see everything that started this\n process: a hand-edit to real code, a scratch note in some file, or\n both.\n\n Whichever lap this is, leave `.gtd/REQUIREMENTS.md` uncommitted and\n finish your turn once it reflects this lap's own work below.\n\n ## First lap\n\n `.gtd/REQUIREMENTS.md` does not exist yet (or holds whatever the\n human's own change happened to leave there). Build it from that\n start diff:\n\n ### Group into concerns\n\n Group the diff into an ordered list of concerns — each one a\n coherent piece of work that can leave the test suite green on\n its own, in the order you'd want them built. If a concern would\n red-light tests that a later concern is meant to fix, that is\n not a valid split: merge the two into one concern.\n\n Two more tests sit beside that green floor, not instead of it.\n Vertical, not horizontal: slice by capability, never by layer. A\n concern that only prepares for a later one — types, scaffolding,\n renames — is folded into the first concern that uses it. Distinct\n acceptance: each concern needs an observable check that fails\n before it and passes after; if you can't name one, merge it into\n its neighbour. Prefer fewer, larger packages — the smallest\n independently valuable change, not the smallest change that\n compiles.\n\n ### Classify each concern\n\n Classify each concern as PRODUCT (a user-facing/requirements\n decision) or TECHNICAL (an implementation decision).\n\n <%~ it.vars.questionBar %>\n\n\n Raise questions only for product concerns here — a technical\n concern's open points wait for the next phase. When every concern\n in this lap is TECHNICAL, write no `## Open Questions` section at\n all.\n\n ### Fold in the sketch\n\n Fold everything the entry commit added into the concerns above —\n a scratch note and a real code edit are both just a sketch of\n the intent, a spec to be finished, never work to be preserved;\n `unwind` already reverted both out of the tree, so there is\n nothing left to delete. Describe each piece under the concern it\n belongs to.\n\n ## Return lap\n\n <%~ it.vars.questionBarReturn %>\n\n ## Review loop-back\n\n `.gtd/REQUIREMENTS.md` already holds concerns and none of them\n have a ticked-but-unfolded answer waiting — that shape means a\n completed REVIEW round put this material here, not a question you\n asked. Develop those existing concerns further with whatever the\n round raised; never rediscover or regroup them from a cold read\n the way the first lap does.\n\n The human's own edit from that review round has been reverted out\n of the working tree the same way the entry commit is — read it\n from history, never the working tree: run\n `git show <%= it.reviewBase %>` to see it. (On the first lap that\n same hash is the process's own diff base, `<%= it.startCommit %>`,\n so this branch does not apply: `.gtd/REQUIREMENTS.md`\n does not exist yet for a review round to have happened against.)\n\n Whatever this round touches, every decision already settled under\n `## Answered Questions` stays settled on this lap too — never\n re-open one. A genuinely open PRODUCT point in the round's own\n material may still raise a fresh question under\n `## Open Questions`: the product gate sits on this path exactly as\n it does on the first lap, and will ask it.\n\n Before grouping anything on this lap, run\n `<%= it.vars.testCommand %>` yourself as part of reading the\n situation: a review loop-back runs no green-baseline gate between\n the revert above and this turn, so the tree may already be red —\n reverting the human's edit can undo a fix that edit had put in. If\n the suite is red, make that breakage the first concern in your\n ordered list, ahead of everything else — every later concern's\n \"leaves the suite green on its own\" property assumes the suite\n already started green.\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: REQUIREMENTS.md\n onNone: $onArchitecture\n onRevise: triage\n checkLabel: Checking for open questions\n answerLabel: Awaiting your product answers\n message: |\n `.gtd/REQUIREMENTS.md` holds the concerns under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n # A separate machine from designPlan (its own memory scope — a COLD read,\n # not a resumed conversation). Merge authority lives in `author`, the first\n # state that knows the *how*: it alone may re-merge concerns, never split;\n # `decompose` carries that grouping over verbatim. ▸ planner identity.\n archPlan:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.architectPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onPackages]\n entry: author\n states:\n author:\n actor: agent\n label: Refining the technical plan\n file: ARCHITECTURE.md\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state files this\n turn touches are `.gtd/ARCHITECTURE.md` (write it) and\n `.gtd/REQUIREMENTS.md` (delete it once its content is\n folded in). Do not create other files to hold notes or output.\n\n You do not resume the design conversation — this is a separate\n machine with its own memory, a cold read every time. Read\n `.gtd/REQUIREMENTS.md` (the settled, ordered, classified\n concerns) in full. Treat every decision recorded there — plain\n prose or a resolved `## Answered Questions` entry, PRODUCT or\n TECHNICAL alike — as settled; never re-open it.\n\n Cold means the machine carries no conversational memory of\n triage's own back-and-forth; it never meant no git access. This\n process started at commit `<%= it.startCommit %>`. Find the\n process's first turn yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\n then `git show` that commit to see everything that started this\n process: a hand-edit to real code, a scratch note in some file,\n or both.\n\n Whichever lap this is, once `.gtd/ARCHITECTURE.md` is written,\n delete `.gtd/REQUIREMENTS.md` — its content has been folded in and\n it must not linger. Leave everything uncommitted and finish your\n turn.\n\n ## First lap\n\n Develop `.gtd/ARCHITECTURE.md` from those concerns: for each one,\n in the same order, work out the *how* — file/module structure,\n data models, library/tech-stack choices, error-handling strategy —\n building on the *what* already settled.\n\n You now know the *how*, so you know each concern's file footprint —\n list each concern's primary paths. Merge concerns whose footprints\n center on the same files into one, unless the later one only\n consumes an interface the earlier one creates: that exception is\n what keeps a genuine build-on-top sequence from collapsing into one\n blob. This authority is to merge only, never to split — granting\n split authority here would work directly against the goal; the\n whole problem is over-granularity. Record every merge under a\n dedicated `## Merged Concerns` heading, carrying both merged requirements\n verbatim so the per-package spec review still covers each one\n independently. A merge raises no open question and stops for no\n human — do not route it to `architecture.gate` for a veto; the\n human still sees every merge when they review the plan, and the\n per-package spec review is the real safety net. Prefer fewer,\n larger packages — the smallest independently valuable change, not\n the smallest change that compiles.\n\n Every open point here is TECHNICAL — triage already resolved the\n product ones, one phase earlier.\n\n <%~ it.vars.questionBar %>\n\n ## Return lap\n\n <%~ it.vars.questionBarReturn %>\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: ARCHITECTURE.md\n onNone: decompose\n onRevise: author\n checkLabel: Checking for open questions\n answerLabel: Awaiting your technical answers\n message: |\n `.gtd/ARCHITECTURE.md` holds the technical plan under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n decompose:\n actor: agent\n label: Decomposing into packages\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state files this turn touches\n are the package files it writes under `.gtd/packages/` and\n `.gtd/ARCHITECTURE.md`, which it deletes. Do not create other\n files to hold notes or output.\n\n If you wrote `.gtd/ARCHITECTURE.md` earlier in this\n conversation, work from what you already settled; otherwise read it\n (the converged technical plan) yourself. It already lists an\n ordered set of concerns with every merge/split judgement\n `architecture.author` made — this turn is a mechanical write-out,\n not a planning one. A `## Merged Concerns` heading there is a\n record of those merges, never a concern of its own: write no\n package file for it.\n\n Write one package file per concern, in the settled order, under\n `.gtd/packages/` (e.g.\n `.gtd/packages/01-name.md`,\n `.gtd/packages/02-name.md`, ...). Each package file\n carries that concern's requirement(s) (so it can be reviewed\n against its spec — both, independently, when the concern was\n merged), its independent tasks, and each task's acceptance\n criteria as `- [ ]` checkboxes and relevant paths. Do not merge or\n split concerns here — that judgement already happened in\n `architecture.author`; carry the settled grouping over verbatim.\n No package file may reference any other `.gtd/` file.\n\n Once the package files are written, delete\n `.gtd/ARCHITECTURE.md`. Leave everything uncommitted and\n finish your turn.\n on:\n \"* .gtd/packages/**\": $onPackages\n\n # The shared review tail. ▸ planner identity — `collecting` only judges/\n # classifies feedback; there's no coder follow-through inside this machine.\n #\n # Nested inside buildTail (build.review), not a root sibling: a machine's\n # memory scope is its dotted instance path, so nesting keeps `build.fix ->\n # build.health.check -> build.review.*` (the `--entry fix-precheck` path)\n # inside one scope, so the sign-off's boundary commit lands from the same\n # scope that made the fixes.\n humanReview:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.reviewerPersona %>\n\n\n <%~ it.vars.agentConduct %>\n entry: reviewing\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: REVIEW.md\n mode: review\n # `gtd --entry review-gate.check` enters via review-gate.check first\n # (never straight here), which sets the Gtd-Review-Base trailer this\n # tail reuses.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this turn touches is `.gtd/REVIEW.md`; write it at exactly\n that path. Do not create other files to hold notes or output.\n\n Write `.gtd/REVIEW.md` to help a human review the changes in\n this exact format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.reviewBase %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation of\n what changed and why, then one file pointer per hunk (checkboxes are\n for the human to tick, not you; every path is `./`-relative, with an\n optional `#line`). Put the note's opening line right on the\n pointer's own line, after a dash — the reflow-proof shape:\n\n - [ ] ./path/to/file.ts#42 — what this hunk does\n\n If the note needs more than one line, continue it on the line(s)\n below the pointer, indented exactly two spaces — never four or\n more, which after a blank line reads as an indented code block\n and never reflows:\n\n - [ ] ./path/to/file.ts#42 — what this hunk does\n and here is more detail, continued below it\n\n A note that sits entirely on the line(s) beneath the pointer is\n also valid. Either way, the explanation itself must never start\n with a bare `./path` token — that parses as a second hunk pointer,\n not a 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 `.gtd/REVIEW.md` uncommitted and finish your turn.\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n label: Awaiting your review\n file: REVIEW.md\n mode: review\n # Opens the review checkout window (src/ReviewWindow.ts) while\n # resting here. Deleting REVIEW.md is refused by the review-doc\n # guard (src/StepGuards.ts).\n reviewWindow: true\n message: |\n `.gtd/REVIEW.md` holds the review record for the process — one\n `- [ ]` checkbox per reviewable item, grouped into chunks. Tick a box\n (`- [x]`) as you review each hunk; ticking only records that you've read\n it, it is not sign-off.\n\n When you've been through the whole diff, run `gtd land`:\n\n - **Sign off** — leave no comment — no note in\n `.gtd/REVIEW.md`, no code edit — to close the process,\n whatever the boxes say. Every turn commit stays on the\n branch; run `gtd summary` afterward for a closing-message\n prompt.\n - **Request changes** — leave a comment: a note on a\n `.gtd/REVIEW.md` line, or a direct code edit — to send a FULL\n development lap (**review.deciding** → **review.collecting** →\n re-triage). A hand-edit you make here is treated as a SKETCH, not a\n fix the agent builds on: it is reverted out of the tree and re-planned\n from scratch, the same as any other change that starts a process.\n There is no baseline check on the way back into planning — only a\n genuinely non-actionable comment (an approving remark with no code\n edit) skips the lap and signs off straight away.\n\n Deleting `.gtd/REVIEW.md` is refused.\n on:\n \"* **\": deciding\n\n deciding:\n actor: check\n label: Reviewing\n file: REVIEW.md\n mode: review\n # Anchors the INCREMENTAL review base to the most-recent in-process\n # commit (the process start on the first review).\n reviewBase: true\n script: |\n #!/usr/bin/env sh\n # FEEDBACK iff the human left a REVIEW.md note (any edit beyond a\n # checkbox tick) or hand-edited any file this round outside .gtd/;\n # otherwise a clean sign-off. This turn only CAPTURES the raw\n # material into REVIEW_RAW.md — collecting judges actionability.\n # A/M REVIEW_RAW.md rows are declared before D REVIEW.md so a\n # feedback round (which also deletes REVIEW.md) isn't mistaken for\n # sign-off.\n set +e\n mkdir -p .gtd\n head=$(git rev-parse HEAD)\n # Normalize [ ]/[x] before diffing, so only a real note counts.\n before=$(git show \"HEAD^:.gtd/REVIEW.md\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n after=$(git show \"HEAD:.gtd/REVIEW.md\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n if git diff-tree --no-commit-id --name-only -r HEAD -- . \":(exclude).gtd\" \\\n | grep -q . \\\n || [ \"$before\" != \"$after\" ]; then\n {\n echo \"This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.\"\n echo\n echo \"Commit: $head\"\n echo \"The human's notes are in .gtd/REVIEW.md at this commit. Any hand edits are\"\n echo \"in that commit's other paths. Run: git show $head\"\n } > .gtd/REVIEW_RAW.md\n fi\n rm -f .gtd/REVIEW.md\n on:\n \"A .gtd/REVIEW_RAW.md\": collecting\n \"M .gtd/REVIEW_RAW.md\": collecting\n \"D .gtd/REVIEW.md\": $onSignoff\n\n # Outcome is by which paths THIS diff touches: writing REQUIREMENTS.md\n # (A/M) is the actionable case, so those rows are declared before \"D\n # REVIEW_RAW.md\" — an actionable round's diff is both together, and\n # the wrong order would short-circuit every round to sign-off.\n # Consuming REVIEW_RAW.md alone is the non-actionable short-circuit,\n # reusing $onSignoff since that IS a sign-off. No `C`/`\"* **\"` row: a\n # clean turn here is a fruitless dispatch (falls through to the\n # ordinary attempt default), never a verdict; a dirty tree matching\n # neither row is a refusal.\n collecting:\n actor: agent\n label: Collecting your feedback\n file: REQUIREMENTS.md\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent judging and classifying a round\n of review feedback.\n\n <%~ it.vars.stateFileRules %>\n\n The only state files this turn\n touches are `.gtd/REQUIREMENTS.md` and\n `.gtd/REVIEW_RAW.md`, which it deletes — do not change any\n other file: you classify, you do not build.\n\n The raw review material is: <%~ it.read(\".gtd/REVIEW_RAW.md\") %>\n\n It names a commit. If you wrote today's review earlier in this\n conversation, work from what you already reviewed; otherwise read that\n commit's diff yourself before classifying anything.\n\n The round is actionable if any of the following is true:\n\n 1. The human left a note on `.gtd/REVIEW.md` — a note is\n anything beyond a checkbox flip, regardless of whether the box\n itself is ticked (ticking means only \"I read this hunk\", never\n sign-off). A note is a mandatory concern below, whether or not\n its box is ticked.\n 2. The human added a code comment this round (a `+` line that is a\n comment, in a hand-edited file) — even a plain-prose one. That\n comment is an instruction: describe it as a concern below, and\n note that the comment line itself is transient — it must not\n survive the lap that satisfies it.\n 3. The human hand-edited any non-comment code this round. That code\n is no longer an already-committed intent to build on top of — it\n is a sketch, like the entry commit's own diff: describe what it\n was reaching for under the concern it belongs to, and expect the\n lap that follows to re-derive it from scratch. Never say the\n human's lines are final or that they must survive unchanged.\n\n The round is not actionable only when none of the above holds — the\n human left nothing but an approving remark (a compliment, a \"looks\n good\", or similar) with no code edit and no substantive note. Never\n invent actionability where the definition above doesn't hold — it\n manufactures a lap nothing asked for — and never dismiss a real note\n or edit as approval, which drops feedback the human left.\n\n If the round is actionable: write `.gtd/REQUIREMENTS.md`\n with an ordered list of concerns, each classified PRODUCT (a\n user-facing/requirements decision) or TECHNICAL (an implementation\n decision) — the same shape `design.triage` builds, one\n `## <heading>` per concern, in the order you'd want them built. Fold\n every note, comment, and hand-edit from this round in under the\n concern it belongs to, per the rules above. Raise no open\n questions here — write no `## Open Questions` section;\n `design.triage` owns that, one phase later. Then delete\n `.gtd/REVIEW_RAW.md` and finish your turn.\n\n If the round is not actionable: delete\n `.gtd/REVIEW_RAW.md` and finish your turn, writing nothing\n to `.gtd/REQUIREMENTS.md` — consuming the capture with no\n other change is the sign-off.\n on:\n \"A .gtd/REQUIREMENTS.md\": $onFeedback\n \"M .gtd/REQUIREMENTS.md\": $onFeedback\n \"D .gtd/REVIEW_RAW.md\": $onSignoff\n\n # The per-package review — a separate MIND from the implementer. ▸ planner\n # identity; fixing findings is a coder action (packages.item.fix-spec).\n specReview:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.specReviewerPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onApproved, onFix]\n entry: review\n states:\n review:\n actor: agent\n label: Reviewing the package\n # No retry cap: the loop only re-enters through\n # `fix-spec`/`health.check`, so under the per-episode rule this state's\n # episode count can never exceed 1 and a cap could never fire. The\n # spec-review loop is genuinely unbounded — see docs/configuration.md's\n # `retry` documentation.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are an autonomous coding agent reviewing a freshly-built work package\n against its own spec.\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this turn touches is\n `.gtd/SPEC_FEEDBACK.md`; write it only when — and only\n when — you find problems. Do not create other files to hold notes or\n output.\n\n The package spec is: <%~ it.read(\".gtd/NEXT.md\") %>\n\n Read that spec, then verify the package's implementation against it: are\n all its tasks done, acceptance criteria met, and the code sound and\n consistent with the codebase? This prompt carries no diff — read it\n yourself: the range runs from `<%= it.startCommit %>` to the working\n tree.\n\n Note the range is process-wide (from the start of the whole build, not\n just this package), so it can span earlier packages too.\n\n - If it fully satisfies the spec, write nothing and finish your turn — a\n clean turn is your approval.\n - If it does not, write `.gtd/SPEC_FEEDBACK.md` listing the\n concrete problems for a fix turn to address. Be specific.\n\n Do not fix anything yourself and do not delete the package file — a later\n step owns that. Finish your turn.\n on:\n \"A .gtd/SPEC_FEEDBACK.md\": $onFix\n \"M .gtd/SPEC_FEEDBACK.md\": $onFix\n \"D .gtd/SPEC_FEEDBACK.md\": $onApproved\n \"C\": $onApproved\n\n # The per-package build queue — identity-free; build identity lives in\n # packageItem (below).\n packageLoop:\n params: [onDrained]\n entry: picking\n states:\n picking:\n actor: check\n label: Picking the next package\n script: |\n #!/usr/bin/env sh\n # Mechanics only — NEXT.md's presence/absence is interpreted by\n # the `on` rows below, never here.\n set +e\n mkdir -p .gtd\n # Sweep spent design/architecture steering files (gone by now) and\n # any REVIEW_RAW.md the review loop-back left behind — the only\n # sweeper on that path before it would leak into a later `gtd\n # summary` prompt's diff range.\n rm -f .gtd/REQUIREMENTS.md .gtd/ARCHITECTURE.md .gtd/QUESTIONS.md .gtd/REVIEW_RAW.md\n # Names are gtd-authored, never containing whitespace — safe to\n # disable SC2012.\n # shellcheck disable=SC2012\n next=$(ls .gtd/packages/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n printf '%s' \"$next\" > .gtd/NEXT.md\n else\n rm -f .gtd/NEXT.md\n fi\n on:\n \"D .gtd/NEXT.md\": $onDrained\n \"* .gtd/NEXT.md\": item.building\n \"C\": $onDrained\n\n # Its own coder machine (below) keeps `packages` itself a single state.\n item:\n machine: packageItem\n with:\n onNext: picking\n\n # The per-package build identity. ▸ coder identity. packages.item.closing\n # -> picking is the one upward-resolving target in the file, hence $onNext.\n packageItem:\n model: <%= it.vars.coderModel %>\n system: |-\n <%~ it.vars.builderPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onNext]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n The only state file this\n turn may write is `.gtd/SATISFIED.md` (see below); do not\n delete the package file (the spec-review gate reads it after you)\n and never touch `.gtd/NEXT.md` — the `picking` state owns\n it.\n\n The package to implement is: <%~ it.read(\".gtd/NEXT.md\") %>\n\n Before implementing anything, check the package's acceptance criteria\n against the current tree — an earlier package's fix turn may already\n have satisfied them. If **every** criterion is already met, implement\n nothing: write `.gtd/SATISFIED.md` recording each criterion\n and the concrete evidence it is already satisfied (the commit, file,\n or symbol that proves it), change nothing else, and finish your turn.\n If any criterion is outstanding, implement the package normally and\n do not write that file.\n\n That file describes a set of independent tasks. Implement all of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement exactly the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"A .gtd/SATISFIED.md\": &satisfied\n to: health.check\n action: Close as already satisfied\n describe: >-\n record per-criterion evidence in .gtd/SATISFIED.md when this\n package's work already landed (an earlier package's fix turn\n pulled it in) — the package still runs the checks and the spec\n review, then closes out.\n \"M .gtd/SATISFIED.md\": *satisfied\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.fixFeedbackPrompt %>\n\n If the\n only way to green the suite is to implement work that another\n package file describes, make the smallest change that gets there and\n do it — that later package will then legitimately report itself\n already satisfied when its turn comes.\n\n Leave everything uncommitted and finish your turn — do not commit.\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n fix-spec:\n actor: agent\n label: Fixing review feedback\n file: SPEC_FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n The only state file this\n turn touches is `.gtd/SPEC_FEEDBACK.md`; address it, then\n delete it. Do not create other files to hold notes or output.\n\n Read `.gtd/SPEC_FEEDBACK.md` (the reviewer's concerns about the\n current package) and the package spec (`<%= it.read(\".gtd/NEXT.md\") %>`),\n then fix the code to resolve every concern. Keep the change focused.\n\n Delete `.gtd/SPEC_FEEDBACK.md` once every concern is addressed.\n Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": health.check\n\n closing:\n actor: check\n label: Closing out the package\n script: |\n #!/usr/bin/env sh\n # Removes the just-reviewed package file (path in NEXT.md) plus\n # leftover spec feedback/evidence, so picking selects the next.\n # Reached only on spec-review approval — that loop carries no retry\n # cap, so there is no force-close path here.\n set +e\n pkg=$(cat .gtd/NEXT.md 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f .gtd/SPEC_FEEDBACK.md .gtd/NEXT.md .gtd/SATISFIED.md\n on:\n \"* **\": $onNext\n\n 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 spec:\n machine: specReview\n with:\n onApproved: closing\n onFix: fix-spec\n\n # The process's build identity. ▸ coder identity — model/system stamped\n # onto fix.\n buildTail:\n model: <%= it.vars.coderModel %>\n system: |-\n <%~ it.vars.finisherPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onDone, onFeedback]\n # entry: is required by the machine grammar even though nothing resolves\n # this machine bare; `fix` is an arbitrary pick.\n entry: fix\n states:\n fix:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.fixFeedbackPrompt %>\n\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 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 # Nested here (not root) so the sign-off's boundary commit stays inside\n # the builder's own scope — see humanReview above.\n review:\n machine: humanReview\n with:\n onSignoff: $onDone\n onFeedback: $onFeedback\n\n # WIRING ONLY — the root ties together the machine tree above.\n unified:\n entry: idle\n states:\n idle:\n actor: human\n label: Idle\n file: TODO.md\n message: |\n No active gtd process.\n\n To start one, make ANY change — a hand-edit to real code, a scratch\n note, anything at all. .gtd/TODO.md is a good default\n place to start sketching. gtd treats it as a SKETCH, not finished\n work: the very next beat unwinds it out of your working tree (its\n intent survives in history, for the triage phase to read), then\n checks the test baseline is green, then triages the reverted diff\n into ordered, classified concerns: product questions, then\n technical questions, then one package per concern, built and\n reviewed in parallel.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: unwind\n action: Start\n describe: >-\n make any change — gtd unwinds it out of your working tree (its\n intent survives in history) before checking the test baseline is\n green, then triages the reverted diff into ordered, classified\n concerns and resolves product questions, then technical ones,\n before building each concern's package (**unwind** ->\n **start-gate.check**).\n\n # --entry fix-precheck: a red suite drops into build.fix (-> health ->\n # review tail); a green suite is a no-op back to idle.\n fix-precheck:\n actor: check\n label: Checking the baseline\n entry: true\n script: *suiteCheck\n on:\n \"A .gtd/FEEDBACK.md\": build.fix\n \"M .gtd/FEEDBACK.md\": build.fix\n \"D .gtd/REVIEW_RAW.md\": idle\n \"D .gtd/FEEDBACK.md\": idle\n \"C\": idle\n\n # Separate from start-gate.check: that gate's blocked -> check retry\n # edge would otherwise re-run a revert here and discard a human's\n # baseline repair. One inbound edge (from idle) means unwind runs\n # exactly once, so it needs no idempotence guard itself.\n unwind:\n actor: check\n label: Unwinding your input\n script: |\n #!/usr/bin/env sh\n set +e\n # Hoisted here, 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.\n # Uses it.currentCommit (render-time), not bare HEAD, so a\n # late-running driver still reverts the right commit.\n commit=\"<%~ it.currentCommit %>\"\n git revert --no-commit \"$commit\"\n on:\n \"* **\": start-gate\n\n # Reverts the human's own review-round hand-edit before re-planning,\n # scoped to real code (`.gtd/` excluded — REVIEW.md is already gone by\n # now). Names the commit via it.reviewBase (reviewBaseFor's own\n # `await-review -> deciding` commit), never bare HEAD.\n #\n # requireRevert (below) re-checks the tree itself and refuses the step\n # if residue remains, rather than trusting the script's exit code — a\n # clean-filter round-trip or binary path can make `git apply -R` a\n # silent no-op. Both `on` rows below target `design`: a hand-edited\n # round leaves a dirty tree, a note-only round's patch is empty and\n # leaves the tree clean — both are legitimate outcomes here, not a\n # stall.\n #\n # `file: REVIEW.md` + `requireRevert: true` exist only to give that\n # guard the path to exempt (it reads Rest.hints.file, never this\n # literal) — REVIEW.md is already gone two commits earlier, so this\n # state never renders it.\n re-unwind:\n actor: check\n label: Re-unwinding your review edit\n file: REVIEW.md\n requireRevert: true\n script: |\n #!/usr/bin/env sh\n # Scoped revert of the human's review-round edit — .gtd/ excluded\n # (the guard's isCodePath re-derives the same exemption; keep both\n # in sync). Expected to succeed; requireRevert catches a silent\n # apply failure.\n set +e\n # Hoisted here, 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.\n commit=\"<%~ it.reviewBase %>\"\n patch=.gtd/.re-unwind.patch\n mkdir -p .gtd\n git diff --binary \"$commit^\" \"$commit\" -- . \":(exclude).gtd\" > \"$patch\"\n if [ -s \"$patch\" ]; then\n git apply -R \"$patch\" || echo \"re-unwind: could not revert $commit\" >&2\n fi\n rm -f \"$patch\"\n on:\n \"* **\": design # a code hand-edit was reverted\n \"C\": design # note-only round: nothing to revert\n\n start-gate:\n machine: entryGate\n with:\n onGreen: design\n reviewBase: \"\"\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `.gtd/FEEDBACK.md` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**start-gate.check**). To repair the baseline as its own separate\n reviewed commit instead, abandon this start and run\n `gtd --entry fix-precheck` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n review-gate:\n machine: entryGate\n with:\n onGreen: build.review\n reviewBase: <%= it.vars.reviewBase %>\n blockedMessage: |\n The test baseline is red — gtd will not start a review on a broken suite.\n `.gtd/FEEDBACK.md` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**review-gate.check**).\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n design:\n machine: designPlan\n with:\n onArchitecture: architecture\n\n architecture:\n machine: archPlan\n with:\n onPackages: packages\n\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n build:\n machine: buildTail\n with:\n onDone: idle\n onFeedback: re-unwind\n";
191925
+ var unified_default = "# gtd's BUILT-IN DEFAULT workflow — used when no `workflow:` key is\n# configured anywhere in the cwd→home config chain; `gtd init` seeds only\n# vars.testCommand + a modes: suggestion, never this file. Compiled through\n# the same `compileWorkflowConfig` a user's own `workflow:` goes through — no\n# privileged path. See skills/authoring/SKILL.md for how to read or edit it\n# (machines, patterns, templates) and docs/driver.md for how a driver runs it.\n#\n# One flow: any change to the tree starts the process. `idle` -> `unwind`\n# reverts that diff (intent survives only in history) -> a green-baseline\n# gate -> `design.triage` groups the reverted diff into ordered, classified\n# concerns and resolves the PRODUCT ones' open questions (`design.gate`) ->\n# `architecture.author` works out the *how* and resolves the TECHNICAL ones\n# (`architecture.gate`) -> `architecture.decompose` writes one package file\n# per concern -> the per-package queue (`packages.*`) builds and reviews each\n# -> the shared tail (`build.review.*`) decides sign-off (-> `idle`, landing\n# an ordinary commit that closes the process — every per-turn commit stays on\n# the branch; run `gtd summary` afterward for a closing-message prompt) vs.\n# actionable feedback (-> `re-unwind` -> back to `design.triage`, a full\n# re-plan lap that never builds on the human's hand-edit). `--entry\n# review-gate.check --var reviewBase=<commitish>` and `--entry fix-precheck`\n# are the two other entries into this same flow.\nvars:\n testCommand: npm test\n # plannerModel: the heavier tier for one-shot triage/design/review turns.\n # coderModel: the tier for build/fix turns. Both repointable via a\n # top-level `vars:` key or a `GTD_<NAME>` env var, like any other var.\n plannerModel: smart\n coderModel: base\n # Declared (not left undeclared) so `--var reviewBase=...` passes the CLI's\n # declared-name check; blank renders empty, which the review entry refuses.\n reviewBase: \"\"\n # gtd's own voice for generated files — adapted from the \"Spartan\" output\n # style (https://github.com/alexgreensh/attention-span, AGPL-3.0, version 0.6),\n # rewritten in gtd's own words for deliverables rather than chat replies;\n # no upstream text ships. A point-in-time derivation with no refresh\n # mechanism.\n styleBlock: |-\n This file is a deliverable, not a chat reply — size follows the work, and\n padding is the target of any cut. Lead with the answer immediately; never\n circle back to restate it once it has landed.\n\n Write flat, commanding sentences — commit to the claim, do not hedge it.\n Favor everyday vocabulary; where a term is unavoidable, define it in five\n words or fewer on first use.\n\n In markdown, bold carries the load: bold the claim, not the sentence around\n it — reading only the bold text must yield the full point and every risk.\n\n Never trim a risk, a number, a threshold, or a scoped condition to save\n space. This outranks every other rule here.\n\n Ship the artifact bare — no lead-in, no sign-off wrapped around it.\n Compressing is not dropping: a task with three load-bearing parts ships all\n three, each shorter, never two of three.\n\n One idea per block; break when the idea shifts. Flag risk in one blunt\n line rather than hedged prose. Never narrate the work — do it.\n styleFormatContract: |-\n This file is machine-read; its format contract outranks every style rule\n above it: keep every `##`/`###` heading, every `- [ ]` checkbox row, and\n every marker line exactly as specified — do not renumber or rename a\n heading or replace a checkbox list with prose. A parser reads these\n literally; a violation refuses the turn outright.\n\n Voice rules above govern the prose between these elements, never the\n elements themselves.\n\n # One persona per prompt-bearing machine below, stamped as its `system:`\n # (see src/StateFields.ts's `system`). `--system-prompt` REPLACES a\n # harness's default system prompt — including its injected cwd/env/\n # git-status block — so each persona is written self-contained, restating\n # that context explicitly rather than relying on it.\n\n # Shared conduct tail, appended after every persona's own role paragraph:\n # use tools without asking, orient yourself with git, and go inspect what\n # the turn's message names. Deliberately NOT named `*Persona`:\n # `templates.test.ts` derives its six-name persona set from\n # `it\\.vars\\.(\\w+Persona)`, so that suffix would silently grow the set.\n agentConduct: |-\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. This runs unattended: there is\n no one to grant permission, confirm a command, or approve a plan before\n you act. Investigate with real commands rather than assuming what a file,\n a commit, or a decision's status actually is, then act on what you find.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block — no cwd, no branch, no recent history, no tree state. Run\n `git status`, `git log`, `ls` and the like yourself before you do\n anything else; that is the normal first move of this turn, not an extra\n step.\n\n The message you receive each turn names a commit, a range, or a file for\n you to go inspect yourself — `git show`, `git diff`, reading the file\n directly — never a diff or a summary rendered into the conversation. Go\n look before you act. Across the several turns one conversation may span,\n work from what you already read and settled earlier rather than\n re-deriving it from a cold read every time, unless the turn's own\n instructions tell you this is a fresh, cold pass.\n designPersona: |-\n You are the product-facing planning voice in an autonomous build pipeline\n called gtd. Your job is to turn a raw, half-formed sketch — a hand-edit to\n real code, a scratch note, or both — into an ordered, classified list of\n concerns a build can execute, and to carry a running conversation with a\n human about the product decisions that list depends on. You are read by\n that human, so write plainly and decisively; you are not read by a parser\n except where a state's own instructions say otherwise.\n architectPersona: |-\n You are the technical planning voice in an autonomous build pipeline\n called gtd. You take over once the product-facing concerns are settled,\n reading them cold — you do not carry over the product conversation, only\n its written, settled output. Your job is to work out the technical *how*\n for each settled concern — file and module structure, data models,\n library and tech-stack choices, error-handling strategy — raise the\n technical open questions a human still needs to decide, and once every\n question is answered, write one package spec file per concern with no\n further judgement call: the grouping and ordering were already decided\n upstream, and this final step is a mechanical write-out of a plan you\n already converged on.\n reviewerPersona: |-\n You are the independent reviewing mind in an autonomous build pipeline\n called gtd — deliberately a separate identity from whichever agent wrote\n the code under review, so you bring no attachment to how it got built.\n Your job splits into two distinct turns: first, writing a structured\n review document that groups a diff into reviewable chunks for a human to\n read through and comment on; later, reading back a round of that human's\n feedback — a note, a hand-edit — and classifying whether it is genuinely\n actionable or just an approving remark, without fixing anything yourself\n either way. You judge and you classify; you never build.\n specReviewerPersona: |-\n You are the adversarial spec-conformance checker in an autonomous build\n pipeline called gtd, reviewing one freshly-built work package against the\n written spec it was built from. Your only job is to verify: are the\n package's tasks actually done, its acceptance criteria actually met, and\n the code sound and consistent with the rest of the codebase? You write\n concrete, actionable feedback only when something is genuinely wrong —\n and when nothing is, you write nothing at all, because a clean turn from\n you IS the approval. You never fix a problem you find; naming it\n precisely enough for someone else's fix turn to act on is the whole job.\n builderPersona: |-\n You are the TDD implementer in an autonomous build pipeline called gtd.\n Your job is to build exactly one package's declared scope end to end,\n tests first, and to come back and fix things when redirected: a failing\n check's output, or a reviewer's concrete feedback. Stay strictly inside\n the package in front of you, touching neither another package's files nor\n code outside the current task. Treat feedback you're given as the\n diagnosis to act on; discard it, with reason, only when it is simply\n wrong.\n finisherPersona: |-\n You are the closing identity in an autonomous build pipeline called gtd —\n the last coder this process hands work to before it closes. You are\n handed a late-breaking failing check after sign-off; fix it, focused and\n minimal, before the process can close. Every turn you land stays its own\n commit on the branch — nothing here gets squashed away.\n\n # The scratchpad-opener sentence shared by every prompt-content state (see\n # STATE_FIELDS's `prompt`) — this workflow's own state files are its private\n # working notes, never project code or documentation a human reads. Injected\n # as the leading line of every prompt body that touches a `.gtd/` file\n # directly; each state's own \"the only state file this turn touches is X\"\n # sentence stays local, immediately after the tag. Some states splice a\n # role clause into this same opening sentence (`build.review.collecting`,\n # `packages.item.spec.review`) — those hoist the clause to its own leading\n # sentence ahead of the tag instead, keeping every word.\n stateFileRules: |-\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.\n # The open-questions warrant test, decide-it-yourself sink, and `## Open\n # Questions` checkbox shape shared by `design.triage` and\n # `architecture.author` — written phase-agnostic (no PRODUCT/TECHNICAL\n # bake-in) so both sites can inject it verbatim. Each site's own\n # phase-scope sentence (triage: product-only, technical waits; architecture:\n # every point here is TECHNICAL) stays local around the tag — the two\n # genuinely disagree on scope, so that sentence is never shared.\n # `questionBarReturn` is the return-lap half — split out so each site can\n # inject it under its own `## Return lap` heading instead of burying\n # return-lap behaviour inside the first lap's own section.\n questionBar: |-\n For every open point, a question is warranted only when all three hold:\n the answer is a genuine fork with divergent outcomes — user-visible for a\n product question, materially different implementations for a technical\n one; the sketch, the entry diff, and history do not already settle it\n (treat anything the entry diff states explicitly — a directive in\n `.gtd/TODO.md`, a committed choice in a hand-edit — as already settled,\n and on a review loop-back lap treat whatever the human's own review-round\n edit or note states explicitly the same way); and getting it wrong would\n survive all the way to the human review tail.\n\n Anything below that bar, decide yourself: record the decision under\n `## Answered Questions` — the same section a human-answered question\n lands in — as `### <the point phrased as a question>` followed by the\n decision and a one-line rationale in prose, no checkboxes.\n\n A question that clears the bar is written under a `## Open Questions`\n heading near the top and takes this shape: one `### <question>`\n sub-heading each, followed by a checkbox list of candidate answers — 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.\n questionBarReturn: |-\n The human has ticked one option per question (or replaced\n `_your answer_` with their own text): fold each chosen answer into its\n concern's prose, then move the question into a `## Answered Questions`\n section as `### <question>` followed by the resolved answer in plain\n prose (drop the checkboxes). Never re-raise a deleted question; treat a\n deleted `## Open Questions` section as acceptance. Omit `## Open\n Questions` entirely once there are none left.\n # The body `packages.item.fix-suite` and `build.fix` share byte for byte —\n # both fix a red `.gtd/FEEDBACK.md`. `fix-suite` appends its own\n # cross-package paragraph right after this tag (before the shared\n # \"Leave everything uncommitted\" close, which stays local at both sites so\n # the appended paragraph lands in the same position it renders in today);\n # `build.fix` renders the tag alone.\n fixFeedbackPrompt: |-\n The only state file this turn touches is `.gtd/FEEDBACK.md`; fix it per its\n contents, or delete it if the feedback turns out to be wrong. Do not\n create other files to hold notes or output.\n\n Read `.gtd/FEEDBACK.md` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `.gtd/FEEDBACK.md` instead of \"fixing\" a non-issue.\n\n# `gtd summary`'s prompt — printed cold, no session identity, no diff inlined.\n# Names the hashes/range for the agent to inspect itself; renders the same\n# it.processCost/it.processCostByModel the old squash finale used to.\nsummary: |\n <%~ it.vars.styleBlock %>\n\n\n Write the closing message for the process HEAD closes or currently sits\n inside — the text a driver may use for a squash, an amend, or a pull\n request body. You are starting cold: read every decision back out of the\n commits named below rather than assuming any prior context.\n\n Cover the initial motivation, the important decisions, and the trade-offs\n made, and the high-level architectural changes — never which files\n changed; that is what `git diff --stat` is for, not this message.\n\n The process's entry commit is `<%= it.entryCommit %>`. <% if\n (it.humanCommits.length > 0) { %>The human contributed at these commits,\n oldest to newest:\n <% it.humanCommits.forEach(function (c) { %>\n - `<%= c.hash %>` (entering `<%= c.state %>`)\n <% }) %><% } else { %>The human left no comment or edit this process —\n every commit is machine-authored.<% } %>\n\n Inspect the range yourself: `git log <%= it.processBase %>..<%= it.processTip %>`\n and `git diff <%= it.processBase %> <%= it.processTip %>`. That range is\n exactly what a squash or PR body should describe.\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n\n Print the closing message and stop — this command writes nothing itself.\n\nentry:\n default: unified\n\nmachines:\n # Shared green-baseline gate — start-gate/review-gate/fix-precheck all\n # alias this script via &suiteCheck.\n entryGate:\n params: [onGreen, blockedMessage, blockedDescribe, checkLabel, blockedLabel, reviewBase]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: &suiteCheck |\n #!/usr/bin/env sh\n set +e\n mkdir -p .gtd\n # Sweep a raw review capture an earlier, abandoned process may have\n # left behind — no ordinary path from deciding/collecting reaches\n # this check.\n rm -f .gtd/REVIEW_RAW.md\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output .gtd/FEEDBACK.md\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > .gtd/FEEDBACK.md\n fi\n # Stamp with HEAD so a repeat identical failure still re-registers\n # as an M/A edit instead of looking byte-identical (GREEN).\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> .gtd/FEEDBACK.md\n else\n rm -f .gtd/.check-output\n rm -f .gtd/FEEDBACK.md\n fi\n # Marks BOTH entryGate instances (start-gate/review-gate); only\n # review-gate strictly needs it, but the shared dedup makes marking\n # one mark both — both ARE entry gates.\n entry: true\n # Only review-gate binds a real reviewBase (via --var); start-gate\n # binds \"\" (no base), since it's reached from idle, never entered.\n reviewBase: $reviewBase\n on:\n \"A .gtd/FEEDBACK.md\": blocked\n \"M .gtd/FEEDBACK.md\": blocked\n \"D .gtd/REVIEW_RAW.md\": $onGreen\n \"D .gtd/FEEDBACK.md\": $onGreen\n \"C\": $onGreen\n blocked:\n actor: human\n label: $blockedLabel\n file: FEEDBACK.md\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $blockedDescribe\n\n # Shared check/escalate pair for build.health.*/packages.item.health.* —\n # the caller's own coder fix-state ($onRed), not this machine, handles red.\n healthGate:\n params: [onGreen, onRed, escalateDescribe, checkLabel, escalateLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # See the shared suite check on `entryGate.check` above.\n script: *suiteCheck\n on:\n \"A .gtd/FEEDBACK.md\": $onRed\n \"M .gtd/FEEDBACK.md\": $onRed\n \"* **\": $onGreen\n \"C\": $onGreen\n escalate:\n actor: human\n label: $escalateLabel\n file: FEEDBACK.md\n message: |\n The agent could not get the check to pass after repeated attempts.\n `.gtd/FEEDBACK.md` holds the last failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $escalateDescribe\n\n # Shared check/answer pair for design.gate/architecture.gate. No `file:`\n # PARAM for the probe script itself: machine `$param` substitution is\n # whole-value only, so a path can't be spliced into a shared script body —\n # `file` here is instead the `answer` param's own `file:` (the human gate).\n questionGate:\n params: [file, message, onNone, onRevise, checkLabel, answerLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: |\n #!/usr/bin/env sh\n # Exactly one of REQUIREMENTS.md/ARCHITECTURE.md exists on disk at\n # a time — architecture.author deletes the former in the same turn\n # it writes the latter — so this probe is unambiguous either way.\n set +e\n mkdir -p .gtd\n file=.gtd/REQUIREMENTS.md\n [ -f \"$file\" ] || file=.gtd/ARCHITECTURE.md\n gtd check qa \"$file\" --open-questions > /dev/null 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n printf 'open questions remain in %s\\n' \"$file\" > .gtd/QUESTIONS.md\n # See the shared suite check's cache-buster rationale on\n # `entryGate.check` above.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> .gtd/QUESTIONS.md\n else\n rm -f .gtd/QUESTIONS.md\n fi\n on:\n \"A .gtd/QUESTIONS.md\": answer\n \"M .gtd/QUESTIONS.md\": answer\n \"D .gtd/QUESTIONS.md\": $onNone\n \"C\": $onNone\n answer:\n actor: human\n label: $answerLabel\n file: $file\n mode: qa\n answerGate: true\n message: $message\n on:\n \"* **\":\n to: $onRevise\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace\n `_your answer_` for your own) to send it back for the agent to\n fold your answers in. Delete a question to skip it, or delete\n the whole `## Open Questions` section to accept the plan as-is.\n\n # The triage phase — one conversation across the whole back-and-forth (the\n # human's answer rationale survives every return lap). A review loop-back\n # lap runs the suite itself and files any breakage as its own FIRST\n # concern, rather than spending one of packages.item.fix-suite's\n # (per-episode) retries on breakage the package didn't cause.\n # ▸ planner identity.\n designPlan:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.designPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onArchitecture]\n entry: triage\n states:\n triage:\n actor: agent\n label: Triaging the change\n file: REQUIREMENTS.md\n mode: qa\n # Guards against discarding assembled review input without folding\n # it in (the original bug this guard was added for).\n requireProgress: true\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this turn touches is `.gtd/REQUIREMENTS.md`; develop it. Do\n not create other files to hold notes or output.\n\n `.gtd/TODO.md` is the likely home of the sketch that\n started this process. Despite living beside this workflow's own\n state files, it is the human's input — the sketch to be folded into\n the concerns below like any other part of the start diff. It is\n never a state file to preserve, and never gtd bookkeeping to ignore.\n\n This process started at commit `<%= it.startCommit %>`. The change\n that started it was reverted out of the working tree by the\n `unwind` beat right after it landed, so `git diff <%= it.startCommit %>`\n (against the working tree) is now empty — its content survives only\n in history. Find the entry commit yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\n (the process's very first turn, landed immediately after\n `<%= it.startCommit %>`, before any later baseline-repair commits) —\n then `git show` that commit to see everything that started this\n process: a hand-edit to real code, a scratch note in some file, or\n both.\n\n Whichever lap this is, leave `.gtd/REQUIREMENTS.md` uncommitted and\n finish your turn once it reflects this lap's own work below.\n\n ## First lap\n\n `.gtd/REQUIREMENTS.md` does not exist yet (or holds whatever the\n human's own change happened to leave there). Build it from that\n start diff:\n\n ### Group into concerns\n\n Group the diff into an ordered list of concerns — each one a\n coherent piece of work that can leave the test suite green on\n its own, in the order you'd want them built. If a concern would\n red-light tests that a later concern is meant to fix, that is\n not a valid split: merge the two into one concern.\n\n Two more tests sit beside that green floor, not instead of it.\n Vertical, not horizontal: slice by capability, never by layer. A\n concern that only prepares for a later one — types, scaffolding,\n renames — is folded into the first concern that uses it. Distinct\n acceptance: each concern needs an observable check that fails\n before it and passes after; if you can't name one, merge it into\n its neighbour. Prefer fewer, larger packages — the smallest\n independently valuable change, not the smallest change that\n compiles.\n\n ### Classify each concern\n\n Classify each concern as PRODUCT (a user-facing/requirements\n decision) or TECHNICAL (an implementation decision).\n\n <%~ it.vars.questionBar %>\n\n\n Raise questions only for product concerns here — a technical\n concern's open points wait for the next phase. When every concern\n in this lap is TECHNICAL, write no `## Open Questions` section at\n all.\n\n ### Fold in the sketch\n\n Fold everything the entry commit added into the concerns above —\n a scratch note and a real code edit are both just a sketch of\n the intent, a spec to be finished, never work to be preserved;\n `unwind` already reverted both out of the tree, so there is\n nothing left to delete. Describe each piece under the concern it\n belongs to.\n\n ## Return lap\n\n <%~ it.vars.questionBarReturn %>\n\n ## Review loop-back\n\n `.gtd/REQUIREMENTS.md` already holds concerns and none of them\n have a ticked-but-unfolded answer waiting — that shape means a\n completed REVIEW round put this material here, not a question you\n asked. Develop those existing concerns further with whatever the\n round raised; never rediscover or regroup them from a cold read\n the way the first lap does.\n\n The human's own edit from that review round has been reverted out\n of the working tree the same way the entry commit is — read it\n from history, never the working tree: run\n `git show <%= it.reviewBase %>` to see it. (On the first lap that\n same hash is the process's own diff base, `<%= it.startCommit %>`,\n so this branch does not apply: `.gtd/REQUIREMENTS.md`\n does not exist yet for a review round to have happened against.)\n\n Whatever this round touches, every decision already settled under\n `## Answered Questions` stays settled on this lap too — never\n re-open one. A genuinely open PRODUCT point in the round's own\n material may still raise a fresh question under\n `## Open Questions`: the product gate sits on this path exactly as\n it does on the first lap, and will ask it.\n\n Before grouping anything on this lap, run\n `<%= it.vars.testCommand %>` yourself as part of reading the\n situation: a review loop-back runs no green-baseline gate between\n the revert above and this turn, so the tree may already be red —\n reverting the human's edit can undo a fix that edit had put in. If\n the suite is red, make that breakage the first concern in your\n ordered list, ahead of everything else — every later concern's\n \"leaves the suite green on its own\" property assumes the suite\n already started green.\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: REQUIREMENTS.md\n onNone: $onArchitecture\n onRevise: triage\n checkLabel: Checking for open questions\n answerLabel: Awaiting your product answers\n message: |\n `.gtd/REQUIREMENTS.md` holds the concerns under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n # A separate machine from designPlan (its own memory scope — a COLD read,\n # not a resumed conversation). Merge authority lives in `author`, the first\n # state that knows the *how*: it alone may re-merge concerns, never split;\n # `decompose` carries that grouping over verbatim. ▸ planner identity.\n archPlan:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.architectPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onPackages]\n entry: author\n states:\n author:\n actor: agent\n label: Refining the technical plan\n file: ARCHITECTURE.md\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state files this\n turn touches are `.gtd/ARCHITECTURE.md` (write it) and\n `.gtd/REQUIREMENTS.md` (delete it once its content is\n folded in). Do not create other files to hold notes or output.\n\n You do not resume the design conversation — this is a separate\n machine with its own memory, a cold read every time. Read\n `.gtd/REQUIREMENTS.md` (the settled, ordered, classified\n concerns) in full. Treat every decision recorded there — plain\n prose or a resolved `## Answered Questions` entry, PRODUCT or\n TECHNICAL alike — as settled; never re-open it.\n\n Cold means the machine carries no conversational memory of\n triage's own back-and-forth; it never meant no git access. This\n process started at commit `<%= it.startCommit %>`. Find the\n process's first turn yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\n then `git show` that commit to see everything that started this\n process: a hand-edit to real code, a scratch note in some file,\n or both.\n\n Whichever lap this is, once `.gtd/ARCHITECTURE.md` is written,\n delete `.gtd/REQUIREMENTS.md` — its content has been folded in and\n it must not linger. Leave everything uncommitted and finish your\n turn.\n\n ## First lap\n\n Develop `.gtd/ARCHITECTURE.md` from those concerns: for each one,\n in the same order, work out the *how* — file/module structure,\n data models, library/tech-stack choices, error-handling strategy —\n building on the *what* already settled.\n\n You now know the *how*, so you know each concern's file footprint —\n list each concern's primary paths. Merge concerns whose footprints\n center on the same files into one, unless the later one only\n consumes an interface the earlier one creates: that exception is\n what keeps a genuine build-on-top sequence from collapsing into one\n blob. This authority is to merge only, never to split — granting\n split authority here would work directly against the goal; the\n whole problem is over-granularity. Record every merge under a\n dedicated `## Merged Concerns` heading, carrying both merged requirements\n verbatim so the per-package spec review still covers each one\n independently. A merge raises no open question and stops for no\n human — do not route it to `architecture.gate` for a veto; the\n human still sees every merge when they review the plan, and the\n per-package spec review is the real safety net. Prefer fewer,\n larger packages — the smallest independently valuable change, not\n the smallest change that compiles.\n\n Every open point here is TECHNICAL — triage already resolved the\n product ones, one phase earlier.\n\n <%~ it.vars.questionBar %>\n\n ## Return lap\n\n <%~ it.vars.questionBarReturn %>\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: ARCHITECTURE.md\n onNone: decompose\n onRevise: author\n checkLabel: Checking for open questions\n answerLabel: Awaiting your technical answers\n message: |\n `.gtd/ARCHITECTURE.md` holds the technical plan under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n decompose:\n actor: agent\n label: Decomposing into packages\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state files this turn touches\n are the package files it writes under `.gtd/packages/` and\n `.gtd/ARCHITECTURE.md`, which it deletes. Do not create other\n files to hold notes or output.\n\n If you wrote `.gtd/ARCHITECTURE.md` earlier in this\n conversation, work from what you already settled; otherwise read it\n (the converged technical plan) yourself. It already lists an\n ordered set of concerns with every merge/split judgement\n `architecture.author` made — this turn is a mechanical write-out,\n not a planning one. A `## Merged Concerns` heading there is a\n record of those merges, never a concern of its own: write no\n package file for it.\n\n Write one package file per concern, in the settled order, under\n `.gtd/packages/` (e.g.\n `.gtd/packages/01-name.md`,\n `.gtd/packages/02-name.md`, ...). Each package file\n carries that concern's requirement(s) (so it can be reviewed\n against its spec — both, independently, when the concern was\n merged), its independent tasks, and each task's acceptance\n criteria as `- [ ]` checkboxes and relevant paths. Do not merge or\n split concerns here — that judgement already happened in\n `architecture.author`; carry the settled grouping over verbatim.\n No package file may reference any other `.gtd/` file.\n\n Once the package files are written, delete\n `.gtd/ARCHITECTURE.md`. Leave everything uncommitted and\n finish your turn.\n on:\n \"* .gtd/packages/**\": $onPackages\n\n # The shared review tail. ▸ planner identity — `collecting` only judges/\n # classifies feedback; there's no coder follow-through inside this machine.\n #\n # Nested inside buildTail (build.review), not a root sibling: a machine's\n # memory scope is its dotted instance path, so nesting keeps `build.fix ->\n # build.health.check -> build.review.*` (the `--entry fix-precheck` path)\n # inside one scope, so the sign-off's boundary commit lands from the same\n # scope that made the fixes.\n humanReview:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.reviewerPersona %>\n\n\n <%~ it.vars.agentConduct %>\n entry: reviewing\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: REVIEW.md\n mode: review\n # `gtd --entry review-gate.check` enters via review-gate.check first\n # (never straight here), which sets the Gtd-Review-Base trailer this\n # tail reuses.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this turn touches is `.gtd/REVIEW.md`; write it at exactly\n that path. Do not create other files to hold notes or output.\n\n Write `.gtd/REVIEW.md` to help a human review the changes in\n this exact format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.reviewBase %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation of\n what changed and why, then one file pointer per hunk (checkboxes are\n for the human to tick, not you; every path is `./`-relative, with an\n optional `#line`). Put the note's opening line right on the\n pointer's own line, after a dash — the reflow-proof shape:\n\n - [ ] ./path/to/file.ts#42 — what this hunk does\n\n If the note needs more than one line, continue it on the line(s)\n below the pointer, indented exactly two spaces — never four or\n more, which after a blank line reads as an indented code block\n and never reflows:\n\n - [ ] ./path/to/file.ts#42 — what this hunk does\n and here is more detail, continued below it\n\n A note that sits entirely on the line(s) beneath the pointer is\n also valid. Either way, the explanation itself must never start\n with a bare `./path` token — that parses as a second hunk pointer,\n not a 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 `.gtd/REVIEW.md` uncommitted and finish your turn.\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n label: Awaiting your review\n file: REVIEW.md\n mode: review\n message: |\n `.gtd/REVIEW.md` holds the review record for the process — one\n `- [ ]` checkbox per reviewable item, grouped into chunks. Tick a box\n (`- [x]`) as you review each hunk; ticking only records that you've read\n it, it is not sign-off.\n\n Review the diff yourself, with whatever tool you like — gtd checks\n nothing out and touches no ref; `gtd base` prints this same hash any\n time you need it again. The range runs from the review base to the\n working tree:\n\n git diff <%= it.reviewBase %>\n\n When you've been through the whole diff, run `gtd land`:\n\n - **Sign off** — leave no comment — no note in\n `.gtd/REVIEW.md`, no code edit — to close the process,\n whatever the boxes say. Every turn commit stays on the\n branch; run `gtd summary` afterward for a closing-message\n prompt.\n - **Request changes** — leave a comment: a note on a\n `.gtd/REVIEW.md` line, or a direct code edit — to send a FULL\n development lap (**review.deciding** → **review.collecting** →\n re-triage). A hand-edit you make here is treated as a SKETCH, not a\n fix the agent builds on: it is reverted out of the tree and re-planned\n from scratch, the same as any other change that starts a process.\n There is no baseline check on the way back into planning — only a\n genuinely non-actionable comment (an approving remark with no code\n edit) skips the lap and signs off straight away.\n\n Deleting `.gtd/REVIEW.md` is refused.\n on:\n \"* **\": deciding\n\n deciding:\n actor: check\n label: Reviewing\n file: REVIEW.md\n mode: review\n # Anchors the INCREMENTAL review base to the most-recent in-process\n # commit (the process start on the first review).\n reviewBase: true\n script: |\n #!/usr/bin/env sh\n # FEEDBACK iff the human left a REVIEW.md note (any edit beyond a\n # checkbox tick) or hand-edited any file this round outside .gtd/;\n # otherwise a clean sign-off. This turn only CAPTURES the raw\n # material into REVIEW_RAW.md — collecting judges actionability.\n # A/M REVIEW_RAW.md rows are declared before D REVIEW.md so a\n # feedback round (which also deletes REVIEW.md) isn't mistaken for\n # sign-off.\n set +e\n mkdir -p .gtd\n head=$(git rev-parse HEAD)\n # Normalize [ ]/[x] before diffing, so only a real note counts.\n before=$(git show \"HEAD^:.gtd/REVIEW.md\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n after=$(git show \"HEAD:.gtd/REVIEW.md\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n if git diff-tree --no-commit-id --name-only -r HEAD -- . \":(exclude).gtd\" \\\n | grep -q . \\\n || [ \"$before\" != \"$after\" ]; then\n {\n echo \"This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.\"\n echo\n echo \"Commit: $head\"\n echo \"The human's notes are in .gtd/REVIEW.md at this commit. Any hand edits are\"\n echo \"in that commit's other paths. Run: git show $head\"\n } > .gtd/REVIEW_RAW.md\n fi\n rm -f .gtd/REVIEW.md\n on:\n \"A .gtd/REVIEW_RAW.md\": collecting\n \"M .gtd/REVIEW_RAW.md\": collecting\n \"D .gtd/REVIEW.md\": $onSignoff\n # Deliberately no `C` row: REVIEW.md missing here means the review\n # gate's own file-provisioning invariant broke, not a sign-off — a\n # clean tree must not be able to auto-approve the round with\n # nothing reviewed. Stays a no-op (validateHasCRow's warning is the\n # accepted cost) rather than routing anywhere.\n\n # Outcome is by which paths THIS diff touches: writing REQUIREMENTS.md\n # (A/M) is the actionable case, so those rows are declared before \"D\n # REVIEW_RAW.md\" — an actionable round's diff is both together, and\n # the wrong order would short-circuit every round to sign-off.\n # Consuming REVIEW_RAW.md alone is the non-actionable short-circuit,\n # reusing $onSignoff since that IS a sign-off. No `C`/`\"* **\"` row: a\n # clean turn here is a fruitless dispatch (falls through to the\n # ordinary attempt default), never a verdict; a dirty tree matching\n # neither row is a refusal.\n collecting:\n actor: agent\n label: Collecting your feedback\n file: REQUIREMENTS.md\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent judging and classifying a round\n of review feedback.\n\n <%~ it.vars.stateFileRules %>\n\n The only state files this turn\n touches are `.gtd/REQUIREMENTS.md` and\n `.gtd/REVIEW_RAW.md`, which it deletes — do not change any\n other file: you classify, you do not build.\n\n The raw review material is: <%~ it.read(\".gtd/REVIEW_RAW.md\") %>\n\n It names a commit. If you wrote today's review earlier in this\n conversation, work from what you already reviewed; otherwise read that\n commit's diff yourself before classifying anything.\n\n The round is actionable if any of the following is true:\n\n 1. The human left a note on `.gtd/REVIEW.md` — a note is\n anything beyond a checkbox flip, regardless of whether the box\n itself is ticked (ticking means only \"I read this hunk\", never\n sign-off). A note is a mandatory concern below, whether or not\n its box is ticked.\n 2. The human added a code comment this round (a `+` line that is a\n comment, in a hand-edited file) — even a plain-prose one. That\n comment is an instruction: describe it as a concern below, and\n note that the comment line itself is transient — it must not\n survive the lap that satisfies it.\n 3. The human hand-edited any non-comment code this round. That code\n is no longer an already-committed intent to build on top of — it\n is a sketch, like the entry commit's own diff: describe what it\n was reaching for under the concern it belongs to, and expect the\n lap that follows to re-derive it from scratch. Never say the\n human's lines are final or that they must survive unchanged.\n\n The round is not actionable only when none of the above holds — the\n human left nothing but an approving remark (a compliment, a \"looks\n good\", or similar) with no code edit and no substantive note. Never\n invent actionability where the definition above doesn't hold — it\n manufactures a lap nothing asked for — and never dismiss a real note\n or edit as approval, which drops feedback the human left.\n\n If the round is actionable: write `.gtd/REQUIREMENTS.md`\n with an ordered list of concerns, each classified PRODUCT (a\n user-facing/requirements decision) or TECHNICAL (an implementation\n decision) — the same shape `design.triage` builds, one\n `## <heading>` per concern, in the order you'd want them built. Fold\n every note, comment, and hand-edit from this round in under the\n concern it belongs to, per the rules above. Raise no open\n questions here — write no `## Open Questions` section;\n `design.triage` owns that, one phase later. Then delete\n `.gtd/REVIEW_RAW.md` and finish your turn.\n\n If the round is not actionable: delete\n `.gtd/REVIEW_RAW.md` and finish your turn, writing nothing\n to `.gtd/REQUIREMENTS.md` — consuming the capture with no\n other change is the sign-off.\n on:\n \"A .gtd/REQUIREMENTS.md\": $onFeedback\n \"M .gtd/REQUIREMENTS.md\": $onFeedback\n \"D .gtd/REVIEW_RAW.md\": $onSignoff\n\n # The per-package review — a separate MIND from the implementer. ▸ planner\n # identity; fixing findings is a coder action (packages.item.fix-spec).\n specReview:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.specReviewerPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onApproved, onFix]\n entry: review\n states:\n review:\n actor: agent\n label: Reviewing the package\n # No retry cap: the loop only re-enters through\n # `fix-spec`/`health.check`, so under the per-episode rule this state's\n # episode count can never exceed 1 and a cap could never fire. The\n # spec-review loop is genuinely unbounded — see docs/configuration.md's\n # `retry` documentation.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are an autonomous coding agent reviewing a freshly-built work package\n against its own spec.\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this turn touches is\n `.gtd/SPEC_FEEDBACK.md`; write it only when — and only\n when — you find problems. Do not create other files to hold notes or\n output.\n\n The package spec is: <%~ it.read(\".gtd/NEXT.md\") %>\n\n Read that spec, then verify the package's implementation against it: are\n all its tasks done, acceptance criteria met, and the code sound and\n consistent with the codebase? This prompt carries no diff — read it\n yourself: the range runs from `<%= it.startCommit %>` to the working\n tree.\n\n Note the range is process-wide (from the start of the whole build, not\n just this package), so it can span earlier packages too.\n\n - If it fully satisfies the spec, write nothing and finish your turn — a\n clean turn is your approval.\n - If it does not, write `.gtd/SPEC_FEEDBACK.md` listing the\n concrete problems for a fix turn to address. Be specific.\n\n Do not fix anything yourself and do not delete the package file — a later\n step owns that. Finish your turn.\n on:\n \"A .gtd/SPEC_FEEDBACK.md\": $onFix\n \"M .gtd/SPEC_FEEDBACK.md\": $onFix\n \"D .gtd/SPEC_FEEDBACK.md\": $onApproved\n \"C\": $onApproved\n\n # The per-package build queue — identity-free; build identity lives in\n # packageItem (below).\n packageLoop:\n params: [onDrained]\n entry: picking\n states:\n picking:\n actor: check\n label: Picking the next package\n script: |\n #!/usr/bin/env sh\n # Mechanics only — NEXT.md's presence/absence is interpreted by\n # the `on` rows below, never here.\n set +e\n mkdir -p .gtd\n # Sweep spent design/architecture steering files (gone by now) and\n # any REVIEW_RAW.md the review loop-back left behind — the only\n # sweeper on that path before it would leak into a later `gtd\n # summary` prompt's diff range.\n rm -f .gtd/REQUIREMENTS.md .gtd/ARCHITECTURE.md .gtd/QUESTIONS.md .gtd/REVIEW_RAW.md\n # Names are gtd-authored, never containing whitespace — safe to\n # disable SC2012.\n # shellcheck disable=SC2012\n next=$(ls .gtd/packages/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n printf '%s' \"$next\" > .gtd/NEXT.md\n else\n rm -f .gtd/NEXT.md\n fi\n on:\n \"D .gtd/NEXT.md\": $onDrained\n \"* .gtd/NEXT.md\": item.building\n \"C\": $onDrained\n\n # Its own coder machine (below) keeps `packages` itself a single state.\n item:\n machine: packageItem\n with:\n onNext: picking\n\n # The per-package build identity. ▸ coder identity. packages.item.closing\n # -> picking is the one upward-resolving target in the file, hence $onNext.\n packageItem:\n model: <%= it.vars.coderModel %>\n system: |-\n <%~ it.vars.builderPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onNext]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n The only state file this\n turn may write is `.gtd/SATISFIED.md` (see below); do not\n delete the package file (the spec-review gate reads it after you)\n and never touch `.gtd/NEXT.md` — the `picking` state owns\n it.\n\n The package to implement is: <%~ it.read(\".gtd/NEXT.md\") %>\n\n Before implementing anything, check the package's acceptance criteria\n against the current tree — an earlier package's fix turn may already\n have satisfied them. If **every** criterion is already met, implement\n nothing: write `.gtd/SATISFIED.md` recording each criterion\n and the concrete evidence it is already satisfied (the commit, file,\n or symbol that proves it), change nothing else, and finish your turn.\n If any criterion is outstanding, implement the package normally and\n do not write that file.\n\n That file describes a set of independent tasks. Implement all of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement exactly the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"A .gtd/SATISFIED.md\": &satisfied\n to: health.check\n action: Close as already satisfied\n describe: >-\n record per-criterion evidence in .gtd/SATISFIED.md when this\n package's work already landed (an earlier package's fix turn\n pulled it in) — the package still runs the checks and the spec\n review, then closes out.\n \"M .gtd/SATISFIED.md\": *satisfied\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.fixFeedbackPrompt %>\n\n If the\n only way to green the suite is to implement work that another\n package file describes, make the smallest change that gets there and\n do it — that later package will then legitimately report itself\n already satisfied when its turn comes.\n\n Leave everything uncommitted and finish your turn — do not commit.\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n fix-spec:\n actor: agent\n label: Fixing review feedback\n file: SPEC_FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n The only state file this\n turn touches is `.gtd/SPEC_FEEDBACK.md`; address it, then\n delete it. Do not create other files to hold notes or output.\n\n Read `.gtd/SPEC_FEEDBACK.md` (the reviewer's concerns about the\n current package) and the package spec (`<%= it.read(\".gtd/NEXT.md\") %>`),\n then fix the code to resolve every concern. Keep the change focused.\n\n Delete `.gtd/SPEC_FEEDBACK.md` once every concern is addressed.\n Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": health.check\n\n closing:\n actor: check\n label: Closing out the package\n script: |\n #!/usr/bin/env sh\n # Removes the just-reviewed package file (path in NEXT.md) plus\n # leftover spec feedback/evidence, so picking selects the next.\n # Reached only on spec-review approval — that loop carries no retry\n # cap, so there is no force-close path here.\n set +e\n pkg=$(cat .gtd/NEXT.md 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f .gtd/SPEC_FEEDBACK.md .gtd/NEXT.md .gtd/SATISFIED.md\n on:\n \"* **\": $onNext\n # Nothing left to sweep (an already-clean NEXT.md/package) still\n # needs to proceed to the next package, not stall here.\n \"C\": $onNext\n\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 spec:\n machine: specReview\n with:\n onApproved: closing\n onFix: fix-spec\n\n # The process's build identity. ▸ coder identity — model/system stamped\n # onto fix.\n buildTail:\n model: <%= it.vars.coderModel %>\n system: |-\n <%~ it.vars.finisherPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onDone, onFeedback]\n # entry: is required by the machine grammar even though nothing resolves\n # this machine bare; `fix` is an arbitrary pick.\n entry: fix\n states:\n fix:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.fixFeedbackPrompt %>\n\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 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 # Nested here (not root) so the sign-off's boundary commit stays inside\n # the builder's own scope — see humanReview above.\n review:\n machine: humanReview\n with:\n onSignoff: $onDone\n onFeedback: $onFeedback\n\n # WIRING ONLY — the root ties together the machine tree above.\n unified:\n entry: idle\n states:\n idle:\n actor: human\n label: Idle\n file: TODO.md\n message: |\n No active gtd process.\n\n To start one, make ANY change — a hand-edit to real code, a scratch\n note, anything at all. .gtd/TODO.md is a good default\n place to start sketching. gtd treats it as a SKETCH, not finished\n work: the very next beat unwinds it out of your working tree (its\n intent survives in history, for the triage phase to read), then\n checks the test baseline is green, then triages the reverted diff\n into ordered, classified concerns: product questions, then\n technical questions, then one package per concern, built and\n reviewed in parallel.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: unwind\n action: Start\n describe: >-\n make any change — gtd unwinds it out of your working tree (its\n intent survives in history) before checking the test baseline is\n green, then triages the reverted diff into ordered, classified\n concerns and resolves product questions, then technical ones,\n before building each concern's package (**unwind** ->\n **start-gate.check**).\n\n # --entry fix-precheck: a red suite drops into build.fix (-> health ->\n # review tail); a green suite is a no-op back to idle.\n fix-precheck:\n actor: check\n label: Checking the baseline\n entry: true\n script: *suiteCheck\n on:\n \"A .gtd/FEEDBACK.md\": build.fix\n \"M .gtd/FEEDBACK.md\": build.fix\n \"D .gtd/REVIEW_RAW.md\": idle\n \"D .gtd/FEEDBACK.md\": idle\n \"C\": idle\n\n # Separate from start-gate.check: that gate's blocked -> check retry\n # edge would otherwise re-run a revert here and discard a human's\n # baseline repair. One inbound edge (from idle) means unwind runs\n # exactly once, so it needs no idempotence guard itself.\n unwind:\n actor: check\n label: Unwinding your input\n script: |\n #!/usr/bin/env sh\n set +e\n # Hoisted here, 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.\n # Uses it.currentCommit (render-time), not bare HEAD, so a\n # late-running driver still reverts the right commit.\n commit=\"<%~ it.currentCommit %>\"\n git revert --no-commit \"$commit\"\n on:\n \"* **\": start-gate\n # Deliberately no `C` row: `git revert --no-commit` under `set +e`\n # leaves a clean tree on either a genuine no-op OR a hard failure\n # (e.g. a merge commit with no `-m`) — the two are indistinguishable\n # from the diff alone, and advancing unconditionally would run the\n # baseline check against a tree that still carries the human's\n # un-reverted sketch. Stays a no-op (validateHasCRow's warning is\n # the accepted cost) rather than risking a false-clean baseline.\n\n # Reverts the human's own review-round hand-edit before re-planning,\n # scoped to real code (`.gtd/` excluded — REVIEW.md is already gone by\n # now). Names the commit via it.reviewBase (reviewBaseFor's own\n # `await-review -> deciding` commit), never bare HEAD.\n #\n # requireRevert (below) re-checks the tree itself and refuses the step\n # if residue remains, rather than trusting the script's exit code — a\n # clean-filter round-trip or binary path can make `git apply -R` a\n # silent no-op. Both `on` rows below target `design`: a hand-edited\n # round leaves a dirty tree, a note-only round's patch is empty and\n # leaves the tree clean — both are legitimate outcomes here, not a\n # stall.\n #\n # `file: REVIEW.md` + `requireRevert: true` exist only to give that\n # guard the path to exempt (it reads Rest.hints.file, never this\n # literal) — REVIEW.md is already gone two commits earlier, so this\n # state never renders it.\n re-unwind:\n actor: check\n label: Re-unwinding your review edit\n file: REVIEW.md\n requireRevert: true\n script: |\n #!/usr/bin/env sh\n # Scoped revert of the human's review-round edit — .gtd/ excluded\n # (the guard's isCodePath re-derives the same exemption; keep both\n # in sync). Expected to succeed; requireRevert catches a silent\n # apply failure.\n set +e\n # Hoisted here, 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.\n commit=\"<%~ it.reviewBase %>\"\n patch=.gtd/.re-unwind.patch\n mkdir -p .gtd\n git diff --binary \"$commit^\" \"$commit\" -- . \":(exclude).gtd\" > \"$patch\"\n if [ -s \"$patch\" ]; then\n git apply -R \"$patch\" || echo \"re-unwind: could not revert $commit\" >&2\n fi\n rm -f \"$patch\"\n on:\n \"* **\": design # a code hand-edit was reverted\n \"C\": design # note-only round: nothing to revert\n\n start-gate:\n machine: entryGate\n with:\n onGreen: design\n reviewBase: \"\"\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `.gtd/FEEDBACK.md` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**start-gate.check**). To repair the baseline as its own separate\n reviewed commit instead, abandon this start and run\n `gtd --entry fix-precheck` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n review-gate:\n machine: entryGate\n with:\n onGreen: build.review\n reviewBase: <%= it.vars.reviewBase %>\n blockedMessage: |\n The test baseline is red — gtd will not start a review on a broken suite.\n `.gtd/FEEDBACK.md` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**review-gate.check**).\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n design:\n machine: designPlan\n with:\n onArchitecture: architecture\n\n architecture:\n machine: archPlan\n with:\n onPackages: packages\n\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n build:\n machine: buildTail\n with:\n onDone: idle\n onFeedback: re-unwind\n";
191889
191926
  //#endregion
191890
191927
  //#region src/workflows/templates.ts
191891
191928
  const SCHEMA_URL = "https://cdn.jsdelivr.net/npm/@pmelab/gtd/schema.json";
@@ -192221,16 +192258,18 @@ const toOperations = (decoded, root, fileRefs = nodeFileRefReader) => {
192221
192258
  workflowVars: defaultWorkflowVars,
192222
192259
  rcVars,
192223
192260
  machineTree: defaultMachineTree,
192224
- stateScopes: defaultStateScopes
192261
+ stateScopes: defaultStateScopes,
192262
+ warnings: validateDefinition(defaultWorkflowDefinition).warnings
192225
192263
  };
192226
192264
  }
192227
- const { definition, vars: workflowVars, tree, scopes } = compileWorkflowConfig(decoded.workflow, root, rcModes, false, fileRefs);
192265
+ const { definition, vars: workflowVars, tree, scopes, warnings } = compileWorkflowConfig(decoded.workflow, root, rcModes, false, fileRefs);
192228
192266
  return {
192229
192267
  workflow: definition,
192230
192268
  workflowVars,
192231
192269
  rcVars,
192232
192270
  machineTree: tree,
192233
- stateScopes: scopes
192271
+ stateScopes: scopes,
192272
+ warnings
192234
192273
  };
192235
192274
  };
192236
192275
  /**
@@ -192445,7 +192484,6 @@ const makeGitImpl = (executor, root) => {
192445
192484
  lastCommitMessage: () => exec("git", "log", "-1", "--pretty=%B").pipe(map$4((s) => s.trim())),
192446
192485
  hasCommits: () => exec("git", "rev-parse", "--verify", "HEAD").pipe(map$4(() => true), catchAll(() => succeed$3(false))),
192447
192486
  readFileAtRef: (ref, path) => exec("git", "show", `${ref}:${path}`),
192448
- changedPathsSince: (ref) => exec("git", "diff", "--name-status", "-z", ref, "HEAD").pipe(map$4((out) => parseNameStatus(splitNul(out)))),
192449
192487
  changedPaths: (base) => gen(function* () {
192450
192488
  const ref = base ?? "HEAD";
192451
192489
  const nameStatusOut = yield* exec("git", "diff", "--name-status", "-z", ref).pipe(catchIf((e) => !isIndexLockError(e), () => succeed$3("")));
@@ -192507,8 +192545,7 @@ const makeGitImpl = (executor, root) => {
192507
192545
  updateRef: (ref, hash) => exec("git", "update-ref", ref, hash).pipe(asVoid),
192508
192546
  deleteRef: (ref) => exec("git", "update-ref", "-d", ref).pipe(asVoid),
192509
192547
  mixedResetTo: (ref) => exec("git", "reset", "--mixed", ref).pipe(asVoid),
192510
- hardResetTo: (ref) => exec("git", "reset", "--hard", ref).pipe(asVoid),
192511
- restoreStagedFrom: (source, paths) => paths.length === 0 ? _void : exec("git", "restore", "--staged", `--source=${source}`, "--", ...paths).pipe(catchIf((e) => !isIndexLockError(e), () => _void))
192548
+ hardResetTo: (ref) => exec("git", "reset", "--hard", ref).pipe(asVoid)
192512
192549
  };
192513
192550
  };
192514
192551
  const makeLiveEffect = gen(function* () {
@@ -193134,23 +193171,6 @@ const renderStateTemplate = (template, context) => eta.renderString(template, co
193134
193171
  /** Render one mode command template — same throw-on-failure discipline as `renderStateTemplate`. `SteeringMode.ts` turns a render failure into a refusal rather than running a half-rendered command. */
193135
193172
  const renderModeCommand = (template, context) => eta.renderString(template, context);
193136
193173
  //#endregion
193137
- //#region src/RetainedHistory.ts
193138
- /** Per-worktree (`refs/worktree/gtd/*`, like `ReviewWindow.ts`'s refs) so linked worktrees sharing one `.git` don't clobber each other's retained history. */
193139
- const HISTORY_REF = "refs/worktree/gtd/history";
193140
- const readRetainedHistory = (git) => git.readRefOption(HISTORY_REF);
193141
- /**
193142
- * Whether resetting to `tipHash` is safe: HEAD must be an ancestor of
193143
- * `tipHash` (nothing on top of the retained tip would be lost). Anything else
193144
- * is refused.
193145
- */
193146
- const restorability = (git, headHash, tipHash) => gen(function* () {
193147
- if (yield* git.isAncestor(headHash, tipHash)) return { ok: true };
193148
- return {
193149
- ok: false,
193150
- reason: "HEAD has advanced past the retained tip — restoring would discard commits built on top of it"
193151
- };
193152
- });
193153
- //#endregion
193154
193174
  //#region src/GitScript.ts
193155
193175
  const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
193156
193176
  const pathspec = (paths) => paths.map(shellQuote).join(" ");
@@ -193179,25 +193199,6 @@ const hardResetTo = (ref) => `git reset --hard ${shellQuote(ref)}`;
193179
193199
  const updateRef = (ref, hash) => `git update-ref ${shellQuote(ref)} ${shellQuote(hash)}`;
193180
193200
  /** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
193181
193201
  const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
193182
- /**
193183
- * `git restore --staged --source=<source> -- <paths…>`, tolerant of a missing
193184
- * ref or no path matching at `source` (mirrors `Git.ts`'s `restoreStagedFrom`)
193185
- * but NOT of `index.lock` contention — swallowing that here would silently
193186
- * skip the `.gtd/` index pin and leak plumbing into the review diff. Empty
193187
- * `paths` emits nothing, mirroring the Effect implementation's own skip.
193188
- */
193189
- const restoreStagedFrom = (source, paths) => {
193190
- if (paths.length === 0) return "";
193191
- const pathArgs = pathspec(paths);
193192
- return [
193193
- `if ! out=$(git restore --staged --source=${shellQuote(source)} -- ${pathArgs} 2>&1); then`,
193194
- ` case "$out" in`,
193195
- ` *"index.lock"*|*"Another git process seems to be running"*) printf '%s\\n' "$out" >&2; exit 1 ;;`,
193196
- ` *) : ;;`,
193197
- ` esac`,
193198
- `fi`
193199
- ].join("\n");
193200
- };
193201
193202
  //#endregion
193202
193203
  //#region src/OutcomeScript.ts
193203
193204
  /** The plain-text twin of `printfLine`: substitutes `args` into `fmt`'s `%s` placeholders, in order. */
@@ -193213,14 +193214,9 @@ const renderFormat = (fmt, ...args) => {
193213
193214
  * conversion spec.
193214
193215
  */
193215
193216
  const printfLine = (fmt, args) => `printf ${shellQuote(fmt)} ${args.join(" ")}`;
193216
- const FMT_NOOP = "nothing to do at \"%s\"\n";
193217
193217
  const FMT_ABANDON_NOOP = "no gtd process is underway (resting at \"%s\") — nothing to abandon\n";
193218
193218
  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";
193219
193219
  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";
193220
- /** `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. */
193221
- const noopText = (state) => renderFormat(FMT_NOOP, state);
193222
- /** The initial-state collapse's line — no commit landed, HEAD was rewound to the process's start parent. */
193223
- const COLLAPSED_TEXT = "nothing to retain — rewound to the commit before the process started\n";
193224
193220
  /** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
193225
193221
  const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
193226
193222
  /**
@@ -193346,12 +193342,6 @@ const headAssertion = (expectedHead) => {
193346
193342
  * same `set -eu` safety `headAssertion` documents.
193347
193343
  */
193348
193344
  const fileExistsGuard = (file) => `[ -f ${shellQuote(file)} ] || exit 0`;
193349
- /** Same shape as `headAssertion`, for the saved-head ref a review window pins; `--verify --quiet` reads a missing ref as an empty string instead of erroring the substitution. */
193350
- const reviewWindowAssertion = (ref, expectedHash) => {
193351
- const refQ = shellQuote(ref);
193352
- const hashQ = shellQuote(expectedHash);
193353
- 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; }`;
193354
- };
193355
193345
  /**
193356
193346
  * Wraps `command` so a non-zero exit prints `prompt` plus the command's
193357
193347
  * captured output before propagating that exit code. The `{ … }` group lets
@@ -193421,7 +193411,6 @@ const assembleScript = (preconditions, steps) => {
193421
193411
  if (steps.length === 0) return "";
193422
193412
  const sections = ["set -eu"];
193423
193413
  if (preconditions.expectedHead !== void 0) sections.push(headAssertion(preconditions.expectedHead));
193424
- if (preconditions.reviewWindow) sections.push(reviewWindowAssertion(preconditions.reviewWindow.ref, preconditions.reviewWindow.expectedHash));
193425
193414
  if (steps.some((step) => step.kind === "gitWrite")) sections.push(RETRY_HELPER);
193426
193415
  if (steps.some((step) => step.kind === "outcome")) sections.push(OUTCOME_PREAMBLE);
193427
193416
  sections.push(...steps.map(renderStep));
@@ -193437,10 +193426,10 @@ const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — pipe it i
193437
193426
  const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip";
193438
193427
  const PRESENTATION_FAILURE_WARNING = "printf 'gtd: presentation-only follow-up failed — continuing\\n' >&2";
193439
193428
  /**
193440
- * A plain-text write command's single pasteable script (`gtd land | sh`).
193441
- * `optional`, when non-empty, is wrapped in a subshell whose failure is
193442
- * swallowed — presentation-only, so it must never turn a landed turn into a
193443
- * non-zero exit.
193429
+ * A write command's single pasteable script (`gtd abandon | sh`,
193430
+ * `gtd land --json=script`'s own field). `optional`, when non-empty, is
193431
+ * wrapped in a subshell whose failure is swallowed — presentation-only, so it
193432
+ * must never turn a landed turn into a non-zero exit.
193444
193433
  */
193445
193434
  const combinedScript = (required, optional) => {
193446
193435
  if (required.length === 0) return "";
@@ -193456,14 +193445,7 @@ const combinedScript = (required, optional) => {
193456
193445
  };
193457
193446
  //#endregion
193458
193447
  //#region src/Edge.ts
193459
- const EMPTY_TREE$2 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
193460
- /**
193461
- * The review checkout window's saved-head ref — duplicated from
193462
- * `src/ReviewWindow.ts`'s own `REVIEW_HEAD_REF` rather than imported, to avoid
193463
- * a circular module dependency (`ReviewWindow.ts` imports FROM this module).
193464
- * Read (never written) here; the window itself is opened/closed elsewhere.
193465
- */
193466
- const REVIEW_HEAD_REF$1 = "refs/worktree/gtd/review-head";
193448
+ const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
193467
193449
  const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
193468
193450
  const COST_TRAILER_PREFIX = "Gtd-Cost: ";
193469
193451
  const COST_TRAILER_RE = /^Gtd-Cost:[ \t]*([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+?))?[ \t]*$/gm;
@@ -193550,7 +193532,7 @@ const resolveRestFrom = (def, headSubject) => {
193550
193532
  }
193551
193533
  };
193552
193534
  };
193553
- const pendingChanges = (git, base) => git.changedPaths(base).pipe(map$4((entries) => entries.map((e) => ({
193535
+ const pendingChanges = (git) => git.changedPaths().pipe(map$4((entries) => entries.map((e) => ({
193554
193536
  status: normalizeStatus(e.status),
193555
193537
  path: e.path
193556
193538
  }))));
@@ -193638,9 +193620,9 @@ const walkProcessBoundary = (history, initialState, includeClosingBoundary) => {
193638
193620
  * is recorded as `ProcessRun.closingHash`. A process still in flight (HEAD is
193639
193621
  * not itself such a boundary) resolves identically to the flag being unset.
193640
193622
  */
193641
- const computeProcessRun = (git, def, head, includeClosingBoundary = false) => gen(function* () {
193623
+ const computeProcessRun = (git, def, includeClosingBoundary = false) => gen(function* () {
193642
193624
  const initialState = initialStateOf(def);
193643
- const history = yield* git.commitHistory(void 0, head);
193625
+ const history = yield* git.commitHistory();
193644
193626
  const { boundaryIndex: i, closingHash } = walkProcessBoundary(history, initialState, includeClosingBoundary);
193645
193627
  const startIdx = i + 1;
193646
193628
  const processCommits = history.slice(startIdx);
@@ -193653,7 +193635,7 @@ const computeProcessRun = (git, def, head, includeClosingBoundary = false) => ge
193653
193635
  };
193654
193636
  });
193655
193637
  const costEntries = parseCostTrailers(processCommits.map((h) => h.message));
193656
- const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$2;
193638
+ const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$1;
193657
193639
  const startHash = startIdx < history.length ? history[startIdx].hash : history[history.length - 1].hash;
193658
193640
  const { reviewBase: reviewBaseOverride, vars: entryVars } = parseEntryCommitOverrides(processCommits);
193659
193641
  return {
@@ -193668,36 +193650,26 @@ const computeProcessRun = (git, def, head, includeClosingBoundary = false) => ge
193668
193650
  };
193669
193651
  });
193670
193652
  /**
193671
- * The run alone, WITHOUT resolving a rest — `gtd abandon`'s escape hatch (it
193653
+ * The run alone, WITHOUT resolving a rest — `gtd abandon`'s escape hatch: it
193672
193654
  * must still work when HEAD names a state `currentRest` would refuse on,
193673
- * since abandon IS the recovery command for that case) and the review
193674
- * window's re-arm, which degrades gracefully (stays closed) rather than
193675
- * erroring on the same kind of foreign/refused rest.
193676
- *
193677
- * Window-aware on exactly the same terms as `restAt`: while a review checkout
193678
- * window is open, real HEAD is rewound to the review base, so walking the
193679
- * trace from literal HEAD would see a process that has not started. Both
193680
- * callers need the REAL head, so the saved head is the walk's origin.
193655
+ * since abandon IS the recovery command for that case.
193681
193656
  */
193682
193657
  const currentRun = gen(function* () {
193683
193658
  const git = yield* GitService;
193684
193659
  const config = yield* (yield* ConfigService).load;
193685
- const windowHead = getOrUndefined(yield* git.readRefOption(REVIEW_HEAD_REF$1));
193686
- return yield* computeProcessRun(git, config.workflow, windowHead);
193660
+ return yield* computeProcessRun(git, config.workflow);
193687
193661
  });
193688
193662
  /**
193689
193663
  * The process HEAD closes or sits inside — `gtd summary`'s run resolution.
193690
- * Window-aware on the same terms as `currentRun`. Identical to `currentRun`
193691
- * for a process still in flight; for a finished process (HEAD is itself a
193692
- * commit entering the initial state), the closing commit is folded back into
193693
- * the trace and its hash recorded as `closingHash` — see
193694
- * `computeProcessRun`'s `includeClosingBoundary` flag.
193664
+ * Identical to `currentRun` for a process still in flight; for a finished
193665
+ * process (HEAD is itself a commit entering the initial state), the closing
193666
+ * commit is folded back into the trace and its hash recorded as
193667
+ * `closingHash` — see `computeProcessRun`'s `includeClosingBoundary` flag.
193695
193668
  */
193696
193669
  const summaryRun = gen(function* () {
193697
193670
  const git = yield* GitService;
193698
193671
  const config = yield* (yield* ConfigService).load;
193699
- const windowHead = getOrUndefined(yield* git.readRefOption(REVIEW_HEAD_REF$1));
193700
- return yield* computeProcessRun(git, config.workflow, windowHead, true);
193672
+ return yield* computeProcessRun(git, config.workflow, true);
193701
193673
  });
193702
193674
  /**
193703
193675
  * PURE: the most-recent in-process turn commit that entered a `reviewBase`
@@ -193887,47 +193859,25 @@ const summaryTemplateContext = (run) => gen(function* () {
193887
193859
  const reviewBase = reviewBaseFor(def, run);
193888
193860
  return yield* buildTemplateContext(git, templateRead(files), "", "", run, vars, void 0, 0, void 0, reviewBase);
193889
193861
  });
193890
- /**
193891
- * True when this process would retain NOTHING: nothing pending and no net
193892
- * change between its trace/retry boundary and HEAD. Used by `planStep`'s
193893
- * "returned to the initial state retaining nothing" mixed-reset branch.
193894
- */
193895
- const retainsNothing = (git, run, changes) => gen(function* () {
193896
- if (changes.length > 0) return false;
193897
- return (yield* git.changedPathsSince(run.startParentHash)).length === 0;
193898
- });
193899
193862
  const omitUndefined = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
193900
- /**
193901
- * Resolve `Rest` at an arbitrary ref (`gtd visualize` reading the review
193902
- * window's saved head via `REVIEW_HEAD_REF`, or `undefined` for HEAD itself).
193903
- *
193904
- * Window-aware ONLY at `ref === undefined`: when the review checkout window's
193905
- * saved-head ref resolves, its hash is used as the effective head for
193906
- * `lastCommitSubject` and `computeProcessRun`'s trace walk — real git HEAD
193907
- * has been rewound to the review base while the window is open, so reading
193908
- * literal `HEAD` there would resolve state/trace/memory against the wrong
193909
- * commit.
193910
- */
193863
+ /** Resolve `Rest` at an arbitrary ref, or `undefined` for HEAD itself. */
193911
193864
  const restAt = (ref) => gen(function* () {
193912
193865
  const git = yield* GitService;
193913
193866
  const config = yield* (yield* ConfigService).load;
193914
193867
  const files = yield* RepoFiles;
193915
193868
  const envVars = yield* EnvVars;
193916
193869
  const def = config.workflow;
193917
- let windowHead;
193918
- if (ref === void 0) windowHead = getOrUndefined(yield* git.readRefOption(REVIEW_HEAD_REF$1));
193919
- const effectiveRef = windowHead ?? ref;
193920
- const headSubject = yield* git.lastCommitSubject(effectiveRef);
193870
+ const headSubject = yield* git.lastCommitSubject(ref);
193921
193871
  const resolution = resolveRestFrom(def, headSubject);
193922
193872
  if (!resolution.ok) return yield* fail$2(resolution.error);
193923
193873
  const resolved = resolution.rest;
193924
193874
  yield* (yield* Narrator).narrate(`rest resolved: ${resolved.state} (awaits ${resolved.actor})`);
193925
- const run = yield* computeProcessRun(git, def, windowHead);
193875
+ const run = yield* computeProcessRun(git, def);
193926
193876
  const vars = resolveVars(config.workflowVars, config.rcVars, run.entryVars, envVars.all);
193927
193877
  const on = yield* renderOnEdgesOrFail(resolved.stateDef.on, vars);
193928
193878
  const stepDef = withRenderedOn(def, resolved.state, on);
193929
193879
  const reviewBase = reviewBaseFor(def, run);
193930
- const changes = yield* pendingChanges(git, windowHead);
193880
+ const changes = yield* pendingChanges(git);
193931
193881
  const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase);
193932
193882
  const memory = memoryKeyFor(config.stateScopes, resolved, run);
193933
193883
  const memoryResumed = memoryResumedFor(def, config.stateScopes, resolved, run);
@@ -193939,7 +193889,6 @@ const restAt = (ref) => gen(function* () {
193939
193889
  on,
193940
193890
  stepDef,
193941
193891
  changes,
193942
- windowHead,
193943
193892
  memory,
193944
193893
  memoryResumed,
193945
193894
  hints,
@@ -194013,108 +193962,32 @@ const stalledAt = (rest) => rest.changes.length === 0 && rest.run.headTurn?.stat
194013
193962
  * `PatternMachine.step`'s own tests (a defensive message beats a lie).
194014
193963
  */
194015
193964
  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)"}`;
194016
- /** `updateRef(HISTORY_REF, tip)` as a single-element (or empty) step list — OMITTED when `tip === startParentHash`, mirroring `retainHistory`'s own no-op check. Used by the commit branch's collapse case below. */
194017
- const retainHistoryStep = (tip, startParentHash) => tip === startParentHash ? [] : [{
194018
- kind: "gitWrite",
194019
- command: updateRef(HISTORY_REF, tip)
194020
- }];
194021
193965
  /** The commit branch's own trailing outcome: a bare `commitOutcome` for a self-loop (`from === to`), else a `transitionOutcome` naming both states. */
194022
193966
  const commitDecisionOutcome = (decision) => ({
194023
193967
  kind: "outcome",
194024
193968
  command: decision.from === decision.to ? commitOutcome(decision.subject) : transitionOutcome(decision.from, decision.to)
194025
193969
  });
194026
193970
  /**
194027
- * True for a `"commit"` decision that is the "green re-entry into the initial
194028
- * state retaining nothing" collapse — target is the initial state, the
194029
- * process started somewhere other than the empty tree, and nothing pending or
194030
- * already-committed since its start parent survives (`retainsNothing`).
194031
- * Always `false` for an ATTEMPT: an agent that did nothing must never rewind
194032
- * a process, and an attempt landing at a prompt state that's also the initial
194033
- * state would otherwise satisfy every other criterion identically to a
194034
- * genuine collapse.
194035
- */
194036
- const collapsesWith = (git, rest, decision) => gen(function* () {
194037
- if (decision.attempt === true) return false;
194038
- return parseStateSubject(decision.subject)?.state === initialStateOf(rest.def) && rest.run.startParentHash !== EMPTY_TREE$2 && (yield* retainsNothing(git, rest.run, rest.changes));
194039
- });
194040
- /**
194041
- * The service-requiring twin of `collapsesWith`, for `program.ts` to ask
194042
- * instead of reaching into `GitService` itself — the boundary AGENTS.md pins.
194043
- * `planLanding` calls this to fill `LandResult.settled` for a `"commit"`/
194044
- * `"commit"` plan, off the same `rest`/`decision` `renderDecision` already
194045
- * decided against, so the two git reads (`changedPathsSince` + `resolveRef`)
194046
- * can never disagree with each other.
194047
- */
194048
- const collapsesToInitialState = (rest, decision) => gen(function* () {
194049
- const git = yield* GitService;
194050
- return yield* collapsesWith(git, rest, decision);
194051
- });
194052
- /**
194053
193971
  * Render a `"commit"` decision as the `EmitStep`s the external driver runs to
194054
193972
  * produce its git effect — the ONE place a decision becomes git commands.
194055
- * Every git call here is a READ; nothing is written — gtd itself never writes
194056
- * git, only a driven script does.
194057
- *
194058
- * One non-obvious case: a commit whose target is the workflow's INITIAL
194059
- * state, from a process that retained nothing, is a no-op probe
194060
- * (`gtd --entry fix-precheck` against a green suite is the shipped example)
194061
- * and must leave no trace — it emits retain-history plus a mixed reset to the
194062
- * process start instead of a commit, so the entry commit and the probe
194063
- * collapse away together rather than dirtying the log with a round trip to
194064
- * `idle`.
194065
- */
194066
- const renderDecision = (git, rest, decision, cost, model) => gen(function* () {
194067
- const run = rest.run;
194068
- if (yield* collapsesWith(git, rest, decision)) {
194069
- const tip = yield* git.resolveRef("HEAD");
194070
- return [
194071
- ...retainHistoryStep(tip, run.startParentHash),
194072
- {
194073
- kind: "gitWrite",
194074
- command: mixedResetTo(run.startParentHash)
194075
- },
194076
- {
194077
- kind: "outcome",
194078
- command: noteOutcome(COLLAPSED_TEXT)
194079
- }
194080
- ];
194081
- }
193973
+ * Pure: no git read, no failure mode — a commit decision always becomes an
193974
+ * ordinary commit plus its outcome report.
193975
+ */
193976
+ const renderDecision = (rest, decision, cost, model) => {
194082
193977
  return [{
194083
193978
  kind: "gitWrite",
194084
193979
  command: commitAll(withCostTrailer(decision.subject, cost, model))
194085
193980
  }, commitDecisionOutcome(decision)];
194086
- });
193981
+ };
194087
193982
  /**
194088
193983
  * The `EmitPreconditions` a step's/entry's assembled scripts assert against:
194089
193984
  * `expectedHead` is the resolved HEAD hash the `Rest` snapshot was taken at.
194090
- * When `targetState` declares `reviewWindow: true` and a window is currently
194091
- * open, the script also pins the window's saved-head hash — a later run whose
194092
- * window has since closed or moved must re-decide rather than trust a stale
194093
- * script.
194094
- */
194095
- const buildPreconditions = (git, rest, targetState) => gen(function* () {
194096
- const expectedHead = rest.context.currentCommit;
194097
- if (!isReviewWindowState(rest.def, targetState)) return { expectedHead };
194098
- const windowHead = yield* git.readRefOption(REVIEW_HEAD_REF$1);
194099
- if (isNone(windowHead)) return { expectedHead };
194100
- return {
194101
- expectedHead,
194102
- reviewWindow: {
194103
- ref: REVIEW_HEAD_REF$1,
194104
- expectedHash: windowHead.value
194105
- }
194106
- };
194107
- });
194108
- /**
194109
- * The `EmittedScripts` a `"commit"` `StepPlan` carries alongside `perform` —
194110
- * built from the SAME `renderDecision` output, but a render failure collapses
194111
- * to an EMPTY script (`emitScripts` with no steps) instead of failing this
194112
- * Effect: `perform` stays the one place a render failure is ever reported to
194113
- * a caller, unchanged by this addition.
194114
193985
  */
194115
- const buildStepScripts = (git, rest, decision, cost, model) => gen(function* () {
194116
- return emitScripts(yield* buildPreconditions(git, rest, decision.to), yield* renderDecision(git, rest, decision, cost, model).pipe(catchAll(() => succeed$3([]))));
194117
- });
193986
+ const buildPreconditions = (rest) => ({ expectedHead: rest.context.currentCommit });
193987
+ /** The `EmittedScripts` a `"commit"` `StepPlan` carries alongside `perform` — built from `renderDecision`'s output. */
193988
+ const buildStepScripts = (rest, decision, cost, model) => {
193989
+ return emitScripts(buildPreconditions(rest), renderDecision(rest, decision, cost, model));
193990
+ };
194118
193991
  /**
194119
193992
  * A no-op is TERMINAL only at a `script` rest: gtd rendered the script, the
194120
193993
  * driver ran it, it left nothing any pattern claims, and re-running it can't
@@ -194122,9 +193995,9 @@ const buildStepScripts = (git, rest, decision, cost, model) => gen(function* ()
194122
193995
  * `settled` signal). A `prompt` rest can't produce a no-op at all (a clean
194123
193996
  * tree with no `C` row there commits an ATTEMPT instead; `stalledAt` is its
194124
193997
  * own signal for that). A no-op at a `message` rest is a human gate the loop
194125
- * already halts on. This is one of TWO settled shapes — the other is the
194126
- * initial-state collapse, decided independently by `program.ts`'s
194127
- * `planLanding`.
193998
+ * already halts on. This is the ONLY settled shape — `gtd land` never moves
193999
+ * HEAD, so a commit decision is never settled, even one re-entering the
194000
+ * initial state.
194128
194001
  */
194129
194002
  const noOpSettles = (rest) => contentKindOf(rest.stateDef) === "script";
194130
194003
  /**
@@ -194133,7 +194006,7 @@ const noOpSettles = (rest) => contentKindOf(rest.stateDef) === "script";
194133
194006
  * and — for a `"commit"` decision — assemble the emitted `scripts` a driver
194134
194007
  * runs to land it, against `rest.context`.
194135
194008
  */
194136
- const planStep = (rest, opts = {}) => gen(function* () {
194009
+ const planStep = (rest, opts = {}) => sync$1(() => {
194137
194010
  const decision = step(rest.stepDef, rest.state, rest.actor, {
194138
194011
  changes: rest.changes,
194139
194012
  processTrace: rest.run.trace.map((entry) => entry.state)
@@ -194148,8 +194021,7 @@ const planStep = (rest, opts = {}) => gen(function* () {
194148
194021
  settled: noOpSettles(rest)
194149
194022
  };
194150
194023
  const { cost, model } = opts;
194151
- const git = yield* GitService;
194152
- const scripts = yield* buildStepScripts(git, rest, decision, cost, model);
194024
+ const scripts = buildStepScripts(rest, decision, cost, model);
194153
194025
  return {
194154
194026
  kind: decision.kind,
194155
194027
  state: rest.state,
@@ -194227,12 +194099,11 @@ const planEntry = (rest, actor, entry) => gen(function* () {
194227
194099
  ...base !== void 0 ? { base } : {},
194228
194100
  vars: varOverrides
194229
194101
  });
194230
- const scriptsGit = yield* GitService;
194231
194102
  return {
194232
194103
  kind: "entry",
194233
194104
  state: entryState,
194234
194105
  subject,
194235
- scripts: emitScripts(yield* buildPreconditions(scriptsGit, rest, entryState), [{
194106
+ scripts: emitScripts(buildPreconditions(rest), [{
194236
194107
  kind: "gitWrite",
194237
194108
  command: commitAll(message)
194238
194109
  }, {
@@ -194273,106 +194144,22 @@ const buildSummary = (def, run, base) => {
194273
194144
  return renderStateTemplate(def.summary, context);
194274
194145
  };
194275
194146
  //#endregion
194276
- //#region src/ReviewWindow.ts
194147
+ //#region src/RetainedHistory.ts
194148
+ /** Per-worktree (`refs/worktree/gtd/*`) so linked worktrees sharing one `.git` don't clobber each other's retained history. */
194149
+ const HISTORY_REF = "refs/worktree/gtd/history";
194150
+ const readRetainedHistory = (git) => git.readRefOption(HISTORY_REF);
194277
194151
  /**
194278
- * Per-worktree (`refs/worktree/*`) so linked worktrees sharing one `.git`
194279
- * don't clobber each other's window.
194152
+ * Whether resetting to `tipHash` is safe: HEAD must be an ancestor of
194153
+ * `tipHash` (nothing on top of the retained tip would be lost). Anything else
194154
+ * is refused.
194280
194155
  */
194281
- const REVIEW_HEAD_REF = "refs/worktree/gtd/review-head";
194282
- const REVIEW_BASE_REF = "refs/worktree/gtd/review-base";
194283
- /** The pre-7.2 shared refs — gtd never writes them, only reads them to finish a window an older gtd left open across the upgrade. */
194284
- const LEGACY_REVIEW_HEAD_REF = "refs/gtd/review-head";
194285
- const LEGACY_REVIEW_BASE_REF = "refs/gtd/review-base";
194286
- const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
194287
- /** The manual-recovery incantation every close refusal ends with. */
194288
- const manualRecovery = (refs) => `\`git reset --mixed ${refs.headRef} && git update-ref -d ${refs.headRef} && git update-ref -d ${refs.baseRef}\``;
194289
- /**
194290
- * Which ref pair (if any) records an open window: this worktree's own
194291
- * `refs/worktree/gtd/*` pair first, falling back to the pre-7.2 shared
194292
- * `refs/gtd/*` pair so an upgrade mid-window still closes cleanly.
194293
- */
194294
- const openWindowRefs = (git) => gen(function* () {
194295
- const scoped = yield* git.readRefOption(REVIEW_HEAD_REF);
194296
- if (isSome(scoped)) return some({
194297
- headRef: REVIEW_HEAD_REF,
194298
- baseRef: REVIEW_BASE_REF,
194299
- headHash: scoped.value,
194300
- legacy: false
194301
- });
194302
- const legacy = yield* git.readRefOption(LEGACY_REVIEW_HEAD_REF);
194303
- if (isNone(legacy)) return none$4();
194304
- return some({
194305
- headRef: LEGACY_REVIEW_HEAD_REF,
194306
- baseRef: LEGACY_REVIEW_BASE_REF,
194307
- headHash: legacy.value,
194308
- legacy: true
194309
- });
194310
- });
194311
- const decideCloseWindow = gen(function* () {
194312
- const git = yield* GitService;
194313
- const open = yield* openWindowRefs(git);
194314
- if (isNone(open)) return { shouldClose: false };
194315
- const refs = open.value;
194316
- const base = yield* git.readRefOption(refs.baseRef);
194317
- if (isSome(base) && base.value !== EMPTY_TREE$1) {
194318
- if (!(yield* git.isAncestor(base.value, "HEAD"))) return yield* fail$2(/* @__PURE__ */ new Error(`a gtd review checkout window is open but HEAD has moved off the reviewed branch — return to it, or restore manually with ${manualRecovery(refs)}`));
194319
- }
194320
- if (refs.legacy) {
194321
- if (!(yield* git.isAncestor("HEAD", refs.headHash))) return yield* fail$2(/* @__PURE__ */ new Error(`a review checkout window is recorded under the shared ${LEGACY_REVIEW_HEAD_REF} (written by gtd 7.1 or older) but it does not belong to this worktree — HEAD is not contained in it. gtd now scopes the window per worktree (${REVIEW_HEAD_REF}); in the worktree that owns it, run ${manualRecovery(refs)}`));
194322
- }
194156
+ const restorability = (git, headHash, tipHash) => gen(function* () {
194157
+ if (yield* git.isAncestor(headHash, tipHash)) return { ok: true };
194323
194158
  return {
194324
- shouldClose: true,
194325
- refs
194159
+ ok: false,
194160
+ reason: "HEAD has advanced past the retained tip — restoring would discard commits built on top of it"
194326
194161
  };
194327
194162
  });
194328
- /**
194329
- * PURE: the close sequence's bash text — reset to the saved head, then delete
194330
- * both refs — built from `src/GitScript.ts`'s `mixedResetTo`/`deleteRef`.
194331
- * Assumes its caller's guards (the reviewed-branch check, the legacy
194332
- * containment check — see `decideCloseWindow`) have already passed; this
194333
- * builder performs none of them, only the write sequence for the case where
194334
- * they did.
194335
- *
194336
- * Resets to `refs.headHash` (the resolved hash), not `refs.headRef` by name —
194337
- * the very next statement deletes that ref, so naming it for the reset would
194338
- * fail on re-entry. A hash stays resolvable regardless, keeping the sequence
194339
- * idempotent.
194340
- */
194341
- const buildCloseWindowScript = (refs) => [
194342
- mixedResetTo(refs.headHash),
194343
- deleteRef(refs.headRef),
194344
- deleteRef(refs.baseRef)
194345
- ].join(" &&\n");
194346
- const decideOpenWindow = (def, target, run, head) => {
194347
- if (!isReviewWindowState(def, target)) return { shouldOpen: false };
194348
- const base = reviewBaseFor(def, run);
194349
- if (base === EMPTY_TREE$1 || base === head) return { shouldOpen: false };
194350
- return {
194351
- shouldOpen: true,
194352
- base,
194353
- head
194354
- };
194355
- };
194356
- /**
194357
- * The open sequence's bash text: base-ref write, head-ref write,
194358
- * mixed-reset-to-base, then a `.gtd/`-scoped `restoreStagedFrom` pin. No `git
194359
- * add --intent-to-add` — that both loses the index-lock race and truncates
194360
- * discarded files to zero bytes — so new files stay untracked; new files
194361
- * outside `.gtd/` stay unpinned too, since `restoreStagedFrom` is scoped
194362
- * there alone. Idempotent under re-entry.
194363
- *
194364
- * `".gtd"` is a deliberate literal, not derived from states' `file:`
194365
- * declarations — it must also cover paths no state declares (a check
194366
- * script's temp output), and `src/testing/EmittedScriptRecognizer.ts`'s
194367
- * `recognizeReviewWindowOpen` re-derives this exact string to recognize the
194368
- * script, which only works while it's fixed.
194369
- */
194370
- const buildOpenWindowScript = (decision) => [
194371
- updateRef(REVIEW_BASE_REF, decision.base),
194372
- updateRef(REVIEW_HEAD_REF, decision.head),
194373
- mixedResetTo(decision.base),
194374
- restoreStagedFrom(REVIEW_HEAD_REF, [".gtd"])
194375
- ].join(" &&\n");
194376
194163
  //#endregion
194377
194164
  //#region node_modules/vscode-languageserver/lib/common/utils/is.js
194378
194165
  var require_is$2 = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -205995,7 +205782,7 @@ const openInBrowser = (url) => {
205995
205782
  };
205996
205783
  //#endregion
205997
205784
  //#region src/StepGuards.ts
205998
- /** True when `changes` deletes `file` — shared by `GuardContext.fileDeleted` and `program.ts`'s `steeringModeSteps`, so both ask it the same way. */
205785
+ /** True when `changes` deletes `file` — the require-revert guard's own `GuardContext.fileDeleted` check. */
205999
205786
  const deletesFile = (changes, file) => changes.some((c) => c.path === file && c.status === "D");
206000
205787
  /**
206001
205788
  * True when `path` sits inside gtd's plumbing directory. Exact match or
@@ -206007,9 +205794,10 @@ const isPlumbingPath = (path) => path === ".gtd" || path.startsWith(`.gtd/`);
206007
205794
  const isCodePath = (path) => !isPlumbingPath(path);
206008
205795
  /** The `mode:` name of gtd's built-in REVIEW.md checkbox format — the only mode the review-doc guard understands. */
206009
205796
  const REVIEW_MODE = "review";
205797
+ /** Selects exactly the human review gate (`await-review` in the bundled template) — a human-actor state declaring `mode: review`. */
206010
205798
  const reviewDocGuard = {
206011
205799
  name: "review-doc",
206012
- appliesTo: (rest) => isReviewWindowState(rest.def, rest.state) && rest.stateDef.mode === REVIEW_MODE,
205800
+ appliesTo: (rest) => rest.stateDef.actor === "human" && rest.stateDef.mode === REVIEW_MODE,
206013
205801
  check: (ctx) => succeed$3(ctx.fileDeleted ? `${ctx.file} was deleted at "${ctx.rest.state}" — restore it, or leave a note (or edit code) to request changes.` : void 0)
206014
205802
  };
206015
205803
  /** The one-line marker `feedback-collecting` writes when a review round left nothing actionable — the ONLY content that lets a `requireProgress` state's instructions file be deleted without a code change. */
@@ -206082,7 +205870,7 @@ const enforceStepGuards = (input) => gen(function* () {
206082
205870
  template: input.context
206083
205871
  };
206084
205872
  const files = yield* RepoFiles;
206085
- const head = yield* cached(files.committed(file, input.windowHead));
205873
+ const head = yield* cached(files.committed(file));
206086
205874
  const worktree = yield* cached(try_(() => files.working(file)));
206087
205875
  const ctx = {
206088
205876
  ...base,
@@ -206142,74 +205930,6 @@ const modeContradictionSkipNotice = (mode) => {
206142
205930
  return `printf '%s\\n' ${shellQuote(`gtd: mode "${mode}" has an external validate: command — skipping the format/validate contradiction check`)} >&2`;
206143
205931
  };
206144
205932
  //#endregion
206145
- //#region src/Sh.ts
206146
- /** POSIX single-quote escaping: wraps in `'...'`, replacing embedded `'` with `'\''`. */
206147
- const shQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
206148
- const isNestedShape = (node) => typeof node !== "string";
206149
- /** Every leaf variable name a shape declares, flattened with `_`, in shape declaration order — the whole `unset` preamble's contents. */
206150
- const shVarNames = (prefix, shape) => {
206151
- const names = [];
206152
- const walk = (path, node) => {
206153
- for (const [key, kind] of Object.entries(node)) {
206154
- const varPath = `${path}_${key}`;
206155
- if (isNestedShape(kind)) walk(varPath, kind);
206156
- else names.push(varPath);
206157
- }
206158
- };
206159
- walk(prefix, shape);
206160
- return names;
206161
- };
206162
- /** A TSV cell: absent/null renders empty; every other value is stringified, with any tab/newline flattened to a space so it can't be mistaken for a row/column boundary. */
206163
- const shCell = (value) => {
206164
- if (value === void 0 || value === null) return "";
206165
- return String(value).replace(/[\t\n]/g, " ");
206166
- };
206167
- /**
206168
- * Renders a list of records as tab-separated rows. Columns are the union of
206169
- * keys across this call's rows, in first-seen order — a different call, even
206170
- * with structurally identical records, can produce a different column order
206171
- * if its rows carry a different subset of optional keys first, so a driver
206172
- * must not hard-code column positions across separate beats.
206173
- */
206174
- const renderTsv = (rows) => {
206175
- const columns = [];
206176
- for (const row of rows) for (const key of Object.keys(row)) if (!columns.includes(key)) columns.push(key);
206177
- return rows.map((row) => columns.map((column) => shCell(row[column])).join(" ")).join("\n");
206178
- };
206179
- /** One leaf's rendered `=value` right-hand side, or `undefined` to emit nothing (false/null/absent scalar, or an empty list). */
206180
- const renderLeafValue = (kind, value) => {
206181
- if (value === void 0 || value === null) return void 0;
206182
- if (kind === "bool") return value === true ? "true" : void 0;
206183
- if (kind === "list") {
206184
- const rows = value;
206185
- return rows.length === 0 ? void 0 : shQuote(renderTsv(rows));
206186
- }
206187
- return shQuote(String(value));
206188
- };
206189
- /**
206190
- * Renders one shell-sourceable document: one `unset` line naming every leaf
206191
- * `shape` declares (so `eval`ing this document is self-contained and no
206192
- * field absent from it can leave a previous document's value standing),
206193
- * then a generic recursive walk assigning each present leaf.
206194
- */
206195
- const renderShDocument = (prefix, shape, fields) => {
206196
- const lines = [`unset ${shVarNames(prefix, shape).join(" ")}`];
206197
- const walk = (path, node, data) => {
206198
- for (const [key, kind] of Object.entries(node)) {
206199
- const varPath = `${path}_${key}`;
206200
- const value = data?.[key];
206201
- if (isNestedShape(kind)) {
206202
- walk(varPath, kind, value);
206203
- continue;
206204
- }
206205
- const rhs = renderLeafValue(kind, value);
206206
- if (rhs !== void 0) lines.push(`${varPath}=${rhs}`);
206207
- }
206208
- };
206209
- walk(prefix, shape, fields);
206210
- return lines.join("\n") + "\n";
206211
- };
206212
- //#endregion
206213
205933
  //#region src/Beat.ts
206214
205934
  /** Assembles one `gtd land` result's fields in `LandFields`' declared order — the ONLY place `gtd land --json`'s wire shape is built. */
206215
205935
  const landFields = (input) => ({
@@ -206221,24 +205941,19 @@ const landFields = (input) => ({
206221
205941
  cost: input.cost,
206222
205942
  model: input.model
206223
205943
  });
206224
- /** `--sh`'s shape for `LandFields`: one entry per field, so a field added to `LandFields` with no matching entry here is a compile error. */
206225
- const LAND_SH_SHAPE = {
206226
- script: "scalar",
206227
- settled: "bool",
206228
- idle: "bool",
206229
- state: "scalar",
206230
- subject: "scalar",
206231
- cost: "scalar",
206232
- model: "scalar"
206233
- };
206234
205944
  const renderLandJson = (fields) => JSON.stringify(fields) + "\n";
205945
+ const FMT_NOOP = "nothing to do at \"%s\"\n";
205946
+ const FMT_LAND_PROSE = "commit everything with this message: %s\n";
205947
+ /** `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. */
205948
+ const noopText = (state) => renderFormat(FMT_NOOP, state);
205949
+ /** `commit everything with this message: <subject>` — plain `gtd land`'s own stdout at a pending diff (no script); `--json` keeps emitting the script itself, unaffected. */
205950
+ const landProseText = (subject) => renderFormat(FMT_LAND_PROSE, subject);
206235
205951
  /**
206236
- * `gtd land --json`'s shell-sourceable counterpart (see `src/Sh.ts`). Its
206237
- * `unset` preamble only names this document's own leaves, so it never clears
206238
- * `gtd_content`/`gtd_log`/`gtd_session_id` — beat-only names a driver may
206239
- * still rely on after the land.
205952
+ * `gtd land`'s plain-text encoding — names the commit subject at a real
205953
+ * landing and points at `--json=script`, since the script itself is
205954
+ * unreachable from plain output; prints the existing no-op note otherwise.
206240
205955
  */
206241
- const renderLandSh = (fields) => renderShDocument("gtd", LAND_SH_SHAPE, fields);
205956
+ const renderLandPlain = (fields) => fields.subject !== null ? `${landProseText(fields.subject).trimEnd()}\n(run \`gtd land --json=script | sh\` to get the landing script)\n` : noopText(fields.state);
206242
205957
  /** `gtd next --json`'s `next` key — `null` on no match, else the matched edge's fields (`action` omitted, never an explicit `undefined`, when unset). */
206243
205958
  const nextField = (next) => next === null ? null : {
206244
205959
  ...next.action !== void 0 ? { action: next.action } : {},
@@ -206285,60 +206000,28 @@ const beatFields = (input) => {
206285
206000
  kind,
206286
206001
  content: kind === "stalled" ? stallDiagnosis(rendered.state, rendered.actor) : rendered.content,
206287
206002
  idle,
206288
- ...dispatchable && session !== void 0 ? { session: {
206003
+ session: dispatchable && session !== void 0 ? {
206289
206004
  id: session.id,
206290
206005
  resume: session.resume
206291
- } } : {},
206292
- ...rendered.model !== void 0 ? { model: rendered.model } : {},
206293
- ...rendered.system !== void 0 && rendered.system !== "" ? { system: rendered.system } : {},
206294
- ...dispatchable && validate !== void 0 ? { validate } : {},
206006
+ } : void 0,
206007
+ model: rendered.model,
206008
+ system: rendered.system !== void 0 && rendered.system !== "" ? rendered.system : void 0,
206009
+ validate: dispatchable && validate !== void 0 ? validate : void 0,
206295
206010
  log,
206296
206011
  state: rendered.state,
206297
206012
  actor: rendered.actor,
206298
- ...rendered.label !== void 0 ? { label: rendered.label } : {},
206299
- ...rendered.memory !== void 0 ? { memory: rendered.memory } : {},
206300
- ...rendered.file !== void 0 ? { file: rendered.file } : {},
206301
- ...rendered.mode !== void 0 ? { mode: rendered.mode } : {},
206302
- ...rendered.edges.length > 0 ? { edges: rendered.edges } : {},
206013
+ label: rendered.label,
206014
+ memory: rendered.memory,
206015
+ file: rendered.file,
206016
+ mode: rendered.mode,
206017
+ edges: rendered.edges.length > 0 ? rendered.edges : void 0,
206303
206018
  changes,
206304
206019
  next: nextField(next),
206305
- ...hasCost ? {
206306
- cost,
206307
- costByModel
206308
- } : {}
206020
+ cost: hasCost ? cost : void 0,
206021
+ costByModel: hasCost ? costByModel : void 0
206309
206022
  };
206310
206023
  };
206311
- /** `--sh`'s shape for `BeatFields`: one entry per field, so a field added to `BeatFields` with no matching entry here is a compile error. */
206312
- const BEAT_SH_SHAPE = {
206313
- kind: "scalar",
206314
- content: "scalar",
206315
- idle: "bool",
206316
- session: {
206317
- id: "scalar",
206318
- resume: "bool"
206319
- },
206320
- model: "scalar",
206321
- system: "scalar",
206322
- validate: "scalar",
206323
- log: "scalar",
206324
- state: "scalar",
206325
- actor: "scalar",
206326
- label: "scalar",
206327
- memory: "scalar",
206328
- file: "scalar",
206329
- mode: "scalar",
206330
- edges: "list",
206331
- changes: "list",
206332
- next: {
206333
- pattern: "scalar",
206334
- target: "scalar",
206335
- action: "scalar"
206336
- },
206337
- cost: "scalar",
206338
- costByModel: "list"
206339
- };
206340
206024
  const renderBeatJson = (fields) => JSON.stringify(fields) + "\n";
206341
- const renderBeatSh = (fields) => renderShDocument("gtd", BEAT_SH_SHAPE, fields);
206342
206025
  /**
206343
206026
  * True when the per-model breakdown adds information beyond the `Cost:` total:
206344
206027
  * more than one model, or a single model that carries an actual `--model` tag
@@ -206390,11 +206073,15 @@ const beatHeaderLines = (fields) => {
206390
206073
  };
206391
206074
  /**
206392
206075
  * The self-validation instruction appended to a `prompt` beat's plain output
206393
- * only — a structured (`--json`/`--sh`) driver runs the `validate` field
206076
+ * only — a structured (`--json`) driver runs the `validate` field
206394
206077
  * itself instead. Advisory either way: `gtd land` refuses a turn whose
206395
206078
  * steering file is invalid regardless of whether this was followed.
206396
206079
  */
206397
206080
  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`;
206081
+ /** `script`'s plain-output instruction line — a literal constant, never a template. */
206082
+ const SCRIPT_INSTRUCTION = "Run this script:";
206083
+ /** `capture`'s plain-output instruction line — a literal constant, never a template. */
206084
+ const CAPTURE_INSTRUCTION = "The edit is already made — run `gtd land` to land it.";
206398
206085
  /**
206399
206086
  * `gtd next`'s plain-text encoding. At `kind === "prompt"` the status header
206400
206087
  * is suppressed and only `content` plus the self-validation instruction is
@@ -206407,7 +206094,10 @@ const selfValidateInstruction = (command, file) => `\nBefore finishing your turn
206407
206094
  const renderBeatPlain = (fields, selfValidateCommand) => {
206408
206095
  const content = fields.content.endsWith("\n") ? fields.content : fields.content + "\n";
206409
206096
  if (fields.kind === "prompt") return selfValidateCommand !== void 0 && fields.file !== void 0 ? content + selfValidateInstruction(selfValidateCommand, fields.file) : content;
206410
- return beatHeaderLines(fields).join("\n") + "\n\n" + content;
206097
+ const body = beatHeaderLines(fields).join("\n") + "\n\n" + content;
206098
+ if (fields.kind === "script") return `${SCRIPT_INSTRUCTION}\n` + body;
206099
+ if (fields.kind === "capture") return `${CAPTURE_INSTRUCTION}\n` + body;
206100
+ return body;
206411
206101
  };
206412
206102
  //#endregion
206413
206103
  //#region src/WorktreeState.ts
@@ -206454,72 +206144,86 @@ set -eu
206454
206144
 
206455
206145
  beat=1
206456
206146
  while :; do
206457
- # One invocation per beat: assign first, THEN eval — command substitution
206458
- # inside eval's own argument would swallow a failed \`gtd next\` under
206459
- # \`set -e\` (eval would see only the empty string and abort on some later
206460
- # unset variable with a confusing message). Assigning to \`out\` first makes
206461
- # this a simple command whose own exit status IS the substitution's, so
206462
- # \`set -e\` aborts correctly on a genuine failure.
206463
- out="$(gtd next --sh)"
206464
- eval "$out"
206147
+ # One value per invocation: \`gtd next --json=<path>\` is a pure read (peek
206148
+ # or dispatch, indistinguishable, always safe to call again), so nothing is
206149
+ # lost by reading \`kind\`/\`idle\` as two separate calls rather than one
206150
+ # combined document.
206151
+ kind="$(gtd next --json=kind)"
206152
+ idle="$(gtd next --json=idle)"
206465
206153
 
206466
- # \`gtd_idle\` (true iff the initial state, clean tree) is the one shape
206467
- # that means the process is genuinely done — EXCEPT on the run's opening
206468
- # beat: land it anyway, so a workflow whose initial state declares its own
206154
+ # \`idle\` (true iff the initial state, clean tree) is the one shape that
206155
+ # means the process is genuinely done — EXCEPT on the run's opening beat:
206156
+ # land it anyway, so a workflow whose initial state declares its own
206469
206157
  # clean-tree "C" pattern still gets a chance to fire.
206470
- if [ "$beat" -gt 1 ] && [ "\${gtd_idle:-}" = true ]; then
206158
+ if [ "$beat" -gt 1 ] && [ "\${idle:-}" = true ]; then
206471
206159
  gtd next
206472
206160
  exit 0
206473
206161
  fi
206474
206162
 
206475
- case "$gtd_kind" in
206476
- stalled) printf '%s\\n' "$gtd_content" >&2; exit 1 ;;
206163
+ case "$kind" in
206164
+ stalled)
206165
+ gtd next --json=content >&2
206166
+ exit 1 ;;
206477
206167
  # you re-ran us resting here: you either edited something or accepted by
206478
206168
  # editing nothing, so land the opening beat either way. Later beats are
206479
206169
  # gates we just produced and you have not read yet — hand off.
206480
206170
  message) [ "$beat" = 1 ] || { gtd next; exit 0; } ;;
206481
206171
  capture) ;; # the human already acted — just land it
206482
206172
  script)
206483
- sh -c "$gtd_content" >>"$gtd_log" 2>&1 || true ;;
206173
+ # A \`script\` rest's plain output is prose, not pipeable into \`sh\` — the
206174
+ # raw script comes from \`--json=content\` instead.
206175
+ content="$(gtd next --json=content)"
206176
+ log="$(gtd next --json=log)"
206177
+ sh -c "$content" >>"$log" 2>&1 || true ;;
206484
206178
  prompt)
206485
- # $gtd_content embeds a full diff, so it goes to the agent over
206486
- # stdin, never as an argv positional — argv is capped (~1 MB on
206487
- # macOS, and POSIX guarantees only 4 KB, ARG_MAX), and a diff crosses
206488
- # that far sooner than you'd expect.
206489
- agent_turn() { printf '%s' "$gtd_content" | claude -p "$1" "$gtd_session_id" \\
206490
- \${gtd_model:+--model "$gtd_model"} \\
206491
- \${gtd_system:+--system-prompt "$gtd_system"} \\
206492
- --dangerously-skip-permissions >>"$gtd_log" 2>&1; }
206493
- if [ "\${gtd_session_resume:-}" = true ]
206179
+ session_id="$(gtd next --json=session.id)"
206180
+ resume="$(gtd next --json=session.resume)"
206181
+ model="$(gtd next --json=model)"
206182
+ system="$(gtd next --json=system)"
206183
+ validate="$(gtd next --json=validate)"
206184
+ log="$(gtd next --json=log)"
206185
+ # The prompt embeds a full diff, so it goes to the agent over stdin,
206186
+ # never as an argv positional — argv is capped (~1 MB on macOS, and
206187
+ # POSIX guarantees only 4 KB, ARG_MAX), and a diff crosses that far
206188
+ # sooner than you'd expect. Piping plain \`gtd next\` (not
206189
+ # --json=content) means the diff is rendered once, for this input,
206190
+ # rather than twice.
206191
+ agent_turn() { gtd next | claude -p "$1" "$session_id" \\
206192
+ \${model:+--model "$model"} \\
206193
+ \${system:+--system-prompt "$system"} \\
206194
+ --dangerously-skip-permissions >>"$log" 2>&1; }
206195
+ if [ "\${resume:-}" = true ]
206494
206196
  then agent_turn --resume || agent_turn --session-id
206495
206197
  else agent_turn --session-id || agent_turn --resume
206496
206198
  fi
206497
206199
  n=0
206498
- while [ -n "\${gtd_validate:-}" ] && ! fix="$(sh -c "$gtd_validate" 2>&1)"; do
206200
+ while [ -n "\${validate:-}" ] && ! fix="$(sh -c "$validate" 2>&1)"; do
206499
206201
  n=$((n + 1)) && [ "$n" -gt 3 ] && { printf '%s\\n' "$fix" >&2; exit 1; }
206500
- # $fix IS the fix prompt, verbatim — piped for the same reason as
206501
- # $gtd_content above. Whether \`claude --resume\` re-applies the
206202
+ # $fix IS the fix prompt, verbatim — piped for the same reason the
206203
+ # agent's own turn above is. Whether \`claude --resume\` re-applies the
206502
206204
  # original session's model/system prompt is a harness detail gtd
206503
206205
  # cannot verify from outside, so this passes the identical
206504
- # $gtd_model/$gtd_system on both calls rather than assume it does —
206206
+ # $model/$system on both calls rather than assume it does —
206505
206207
  # otherwise this fix turn might silently fall back to Claude Code's
206506
206208
  # own defaults while the turn that produced the file ran under the
206507
206209
  # workflow's own model and persona.
206508
- printf '%s' "$fix" | claude -p --resume "$gtd_session_id" \\
206509
- \${gtd_model:+--model "$gtd_model"} \\
206510
- \${gtd_system:+--system-prompt "$gtd_system"} \\
206511
- --dangerously-skip-permissions >>"$gtd_log" 2>&1
206210
+ printf '%s' "$fix" | claude -p --resume "$session_id" \\
206211
+ \${model:+--model "$model"} \\
206212
+ \${system:+--system-prompt "$system"} \\
206213
+ --dangerously-skip-permissions >>"$log" 2>&1
206512
206214
  done ;;
206513
206215
  esac
206514
206216
 
206515
- # \`gtd land --sh\` carries \`settled\`/\`idle\` alongside the script itself —
206516
- # one invocation tells us both what to run and whether to stop, with no
206517
- # second read needed to decide either.
206518
- out="$(gtd land --sh)"
206519
- eval "$out"
206520
- printf '%s\\n' "$gtd_script" | sh
206521
- if [ "\${gtd_settled:-}" = true ]; then
206522
- [ "\${gtd_idle:-}" = true ] && gtd next
206217
+ # Read every value the landing needs BEFORE piping the script to \`sh\`:
206218
+ # \`gtd land\` itself never mutates — it only plans and prints — so
206219
+ # \`settled\`/\`idle\`/\`script\` planned against the same untouched tree all
206220
+ # agree, whatever order they're read in, as long as all three are read
206221
+ # before the script runs and changes the tree out from under them.
206222
+ settled="$(gtd land --json=settled)"
206223
+ idle="$(gtd land --json=idle)"
206224
+ gtd land --json=script | sh
206225
+ if [ "\${settled:-}" = true ]; then
206226
+ [ "\${idle:-}" = true ] && gtd next
206523
206227
  exit 0
206524
206228
  fi
206525
206229
  beat=$((beat + 1))
@@ -206527,102 +206231,104 @@ done`;
206527
206231
  const EDIT_COMMAND = `#!/usr/bin/env sh
206528
206232
  set -eu
206529
206233
  cd "$(git rev-parse --show-toplevel)"
206530
- out="$(gtd next --sh)"
206531
- eval "$out"
206532
- f="\${gtd_file:-.gtd/TODO.md}"
206234
+ f="$(gtd next --json=file)"
206235
+ f="\${f:-.gtd/TODO.md}"
206533
206236
  mkdir -p "$(dirname "$f")"
206534
206237
  exec "\${EDITOR:-vi}" "$f"`;
206535
- 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`;
206238
+ const HEADER = () => `gtd ${GTD_VERSION$1} — driver protocol\n\ngtd decides and prints; the driver executes. gtd never runs git, never runs\nyour agent, never runs a check script, and never runs a steering mode's own\nformat:/validate: command either — that pair is text in the script gtd next\n--json's validate field (or gtd validate) prints, for the driver to run\nitself ahead of gtd land.\n`;
206536
206239
  const BEAT_PROTOCOL = `
206537
206240
  ## The beat protocol
206538
206241
 
206539
206242
  Two reads and one write, repeated: ask -> act -> land.
206540
206243
 
206541
- gtd next --sh which kind is next, as gtd_-prefixed POSIX shell
206542
- assignments (eval it) — a pure read; poll it, peek it, no writes
206543
- gtd next what to run/show, in plain text — the SAME rest the --sh read just fetched
206544
- gtd land --sh how to record what happened, and whether anything is
206545
- still owed (gtd_script to run, gtd_settled to check) — eval it, run gtd_script
206244
+ gtd next --json=<path> which kind is next, and any other field, one pure
206245
+ scalar read per value (kind, content, idle,
206246
+ session.id, model, validate, log, ...)
206247
+ gtd next what to run/show, in plain text — the SAME rest
206248
+ the --json=<path> reads above are peeking at
206249
+ gtd land --json=<path> how to record what happened, and whether anything
206250
+ is still owed (script to run, settled to check)
206546
206251
 
206547
206252
  Every beat that acts, lands. Every beat that rests, exits.
206548
206253
 
206549
- Both reads are pure PEEKS — every field \`gtd next --sh\`/\`--json\` reports
206550
- (\`gtd_session_id\`/\`gtd_session_resume\` included) is DERIVED from history,
206551
- never stored, so looking is free: nothing distinguishes a peek from a
206552
- dispatch, and calling either one twice in a row is always safe. There is no
206553
- separate claiming form.
206254
+ Every \`gtd next --json=<path>\`/\`gtd land --json=<path>\` read is a pure PEEK —
206255
+ every field it reports (\`session.id\`/\`session.resume\` included) is DERIVED
206256
+ from history, never stored, so looking is free: nothing distinguishes a peek
206257
+ from a dispatch, and calling either one twice in a row is always safe. There
206258
+ is no separate claiming form.
206554
206259
 
206555
- \`gtd next\` has \`--json\`/\`--sh\` encodings, mutually exclusive, reading the
206556
- SAME field set whether you call \`gtd next --json\` or \`gtd next --sh\` —
206557
- \`--json\`'s keys, or \`--sh\`'s \`gtd_\`-prefixed shell assignments. An AI
206558
- driver reads \`gtd_kind\`/\`gtd_session_id\` and the rest straight off \`--sh\`
206559
- without parsing anything — no JSON library, no \`jq\`; this briefing's own
206560
- reference driver uses \`--sh\` throughout. Plain
206561
- \`gtd next\` (no flag) is NOT a parsing surface: \`content\` is the bare step
206562
- in every encoding; plain adds the self-validation instruction at a
206260
+ \`gtd next\` has one structured encoding, \`--json\`: bare \`--json\` prints the
206261
+ whole document, \`--json=<path>\` (a dotted key path, e.g. \`kind\`, \`content\`,
206262
+ \`session.id\`) prints just that one value — a scalar raw and unquoted, a
206263
+ boolean as \`true\`/\`false\`, a list one JSON entry per line. An absent
206264
+ optional field prints nothing and exits 0; an unknown path is a usage error
206265
+ (exit 2). An AI driver reads \`kind\`/\`session.id\` and the rest straight off
206266
+ \`--json=<path>\`, one call per value, without parsing anything — no JSON
206267
+ library, no \`jq\`; this briefing's own reference driver reads every field this
206268
+ way. Plain \`gtd next\` (no flag) is NOT a parsing surface: \`content\` is the
206269
+ bare step in every encoding; plain adds the self-validation instruction at a
206563
206270
  validatable \`prompt\` rest, and adds a status-summary header at every kind
206564
206271
  EXCEPT \`prompt\` (those bytes are the agent's own input, so no header is
206565
- prefixed there). A driver that wants \`content\` programmatically reads it
206566
- off \`--json\`/\`--sh\` (\`gtd_content\` under \`--sh\`), never by scraping plain
206567
- text.
206272
+ prefixed there). A driver that wants \`content\` programmatically reads it off
206273
+ \`--json=content\`, never by scraping plain text.
206568
206274
 
206569
- The \`kind\` field (\`gtd_kind\` under \`--sh\`) selects what to do:
206275
+ The \`kind\` field (\`--json=kind\`) selects what to do:
206570
206276
 
206571
- - \`stalled\` — print \`gtd_content\` (the diagnosis, already in hand) to
206277
+ - \`stalled\` — print \`--json=content\` (the diagnosis, already in hand) to
206572
206278
  stderr and exit non-zero. Terminal: another dispatch would just repeat the
206573
206279
  same fruitless turn.
206574
206280
  - \`message\` — print plain \`gtd next\`'s output (the richer status header,
206575
- not bare \`gtd_content\`) and exit 0. This is a human gate. EXCEPT on the
206576
- run's opening beat: the human invoked you while resting there, so land it
206281
+ not bare \`content\`) and exit 0. This is a human gate. EXCEPT on the run's
206282
+ opening beat: the human invoked you while resting there, so land it
206577
206283
  instead of printing. Some gates route a \`"C"\` (clean-tree) pattern
206578
206284
  onward, making "change nothing" a real decision that looks identical to a
206579
206285
  gate nobody has read yet; landing an opening beat at a gate without one is
206580
206286
  a benign no-op, and it prints on the next beat.
206581
206287
  - \`capture\` — a human gate the human already acted on (the tree is dirty).
206582
206288
  Land it immediately, no display needed.
206583
- - \`script\` — run \`gtd_content\` with \`sh -c\`, appending its output to
206584
- \`gtd_log\`, and ignore its exit code — the outcome lives in the tree, not
206585
- in the exit code.
206586
- - \`prompt\` — send \`gtd_content\` to your agent CLI over stdin, using the
206587
- accompanying \`gtd_session_id\`/\`gtd_model\`/\`gtd_validate\` fields off the
206588
- SAME \`--sh\` read (see below).
206289
+ - \`script\` — run \`--json=content\` with \`sh -c\`, appending its output to
206290
+ \`--json=log\`, and ignore its exit code — the outcome lives in the tree,
206291
+ not in the exit code.
206292
+ - \`prompt\` — send plain \`gtd next\`'s own output to your agent CLI over
206293
+ stdin (it's the bare content at this kind, so no separate \`--json=content\`
206294
+ read is needed), using the accompanying \`session.id\`/\`session.resume\`/
206295
+ \`model\`/\`validate\` fields, each its own \`--json=<path>\` read (see
206296
+ below).
206589
206297
  `;
206590
206298
  const JSON_FIELD_REFERENCE = `
206591
206299
  ## Field reference
206592
206300
 
206593
- \`gtd next\` and \`gtd land\` each carry one field set, in two encodings:
206594
- \`--json\`'s keys, or \`--sh\`'s \`gtd_\`-prefixed shell assignments — this
206595
- driver's own beats poll \`--sh\` throughout (see the beat protocol above).
206596
- There is no version field — the field set itself is the contract, and a
206597
- breaking change to it is a major release. Every field below is always
206598
- present unless marked "when set" — omitted under \`--json\` (never \`null\`),
206599
- genuinely unset under \`--sh\` (its own \`unset\` preamble, so read an optional
206600
- one as \`\${gtd_model:-}\` or \`\${gtd_model:+...}\`, never bare).
206301
+ \`gtd next\` and \`gtd land\` each carry one field set, read through \`--json\`
206302
+ (bare for the whole document, \`--json=<path>\` for one value) — this driver's
206303
+ own beats read fields one at a time via \`--json=<path>\` throughout (see the
206304
+ beat protocol above). There is no version field — the field set itself is the
206305
+ contract, and a breaking change to it is a major release. Every field below
206306
+ is always present unless marked "when set" — omitted under bare \`--json\`
206307
+ (never \`null\`); under \`--json=<path>\` an absent field prints nothing and
206308
+ exits 0, so read it in a shell variable and guard on emptiness
206309
+ (\`\${x:-}\`/\`\${x:+...}\`) rather than assuming it is always populated.
206601
206310
 
206602
206311
  \`gtd next\` fields — Always: \`kind\` (\`capture\`|\`message\`|\`script\`|\`prompt\`|
206603
206312
  \`stalled\`), \`content\` (the bare step in every encoding — plain \`gtd next\`
206604
206313
  adds the self-validation instruction at a validatable \`prompt\` rest, and a
206605
206314
  status-summary header at every OTHER kind; \`content\` itself never carries
206606
206315
  either), \`idle\` (\`true\` iff the resolved rest is the workflow's initial
206607
- state with a clean tree — under \`--sh\`, present only when \`true\`), \`log\`,
206608
- \`state\`, \`actor\`, \`changes\` (which declared \`on\` pattern, if any, each
206609
- pending change matches), \`next\` (the first declared \`on\` edge that would
206610
- fire right now — \`{action?, pattern, target}\` — or \`null\` on no match).
206611
- Only at \`kind: "prompt"\` (the DISPATCH BLOCK — absent at every other kind,
206612
- a \`stalled\` beat included, by construction): \`session\` (\`{id, resume}\` —
206613
- \`gtd_session_id\`/\`gtd_session_resume\` under \`--sh\` — both DERIVED from
206614
- history, never stored, so a plain peek is exactly as safe to call as a
206615
- dispatch would be) and, when the state declares a validatable steering
206616
- file, \`validate\` (the script that formats then validates it). When set:
206617
- \`model\`, \`memory\`, \`label\`, \`file\`, \`mode\`, \`edges\`. When a cost has been
206618
- recorded (a prior \`gtd land --cost=<n>\`): \`cost\`, \`costByModel\`.
206316
+ state with a clean tree), \`log\`, \`state\`, \`actor\`, \`changes\` (which
206317
+ declared \`on\` pattern, if any, each pending change matches), \`next\` (the
206318
+ first declared \`on\` edge that would fire right now — \`{action?, pattern,
206319
+ target}\` — or \`null\` on no match). Only at \`kind: "prompt"\` (the DISPATCH
206320
+ BLOCK — absent at every other kind, a \`stalled\` beat included, by
206321
+ construction): \`session.id\`/\`session.resume\` — both DERIVED from history,
206322
+ never stored, so a plain peek is exactly as safe to call as a dispatch would
206323
+ be — and, when the state declares a validatable steering file, \`validate\`
206324
+ (the script that formats then validates it). When set: \`model\`, \`memory\`,
206325
+ \`label\`, \`file\`, \`mode\`, \`edges\`. When a cost has been recorded (a prior
206326
+ \`gtd land --cost=<n>\`): \`cost\`, \`costByModel\`.
206619
206327
 
206620
- \`gtd land\` fields, in fixed order: \`script\` (the POSIX sh to run —
206621
- \`gtd_script\` under \`--sh\`), \`settled\` (true for either terminal shape: a
206622
- no-op at a \`script\` rest, or a decision that collapses back to the initial
206623
- state retaining nothing — stop immediately, nothing more to read), \`idle\`
206624
- (true iff the state landing rests at is the workflow's initial state),
206625
- \`state\`, \`subject\`, \`cost\`, \`model\` (the last three \`null\`, never
206328
+ \`gtd land\` fields, in fixed order: \`script\` (the POSIX sh to run), \`settled\`
206329
+ (true for a no-op at a \`script\` rest — stop immediately, nothing more to
206330
+ read), \`idle\` (true iff the state landing rests at is the workflow's initial
206331
+ state), \`state\`, \`subject\`, \`cost\`, \`model\` (the last three \`null\`, never
206626
206332
  omitted, for a genuine no-op).
206627
206333
 
206628
206334
  ### The error envelope
@@ -206634,63 +206340,62 @@ stdout stays byte-empty: \`{"state":"error","prompt":"<message>"}\` rides
206634
206340
  or defect, exit 2 for a usage error (nothing was even attempted). This is
206635
206341
  true of every command, not only \`gtd next\` — an invocation that carries (or
206636
206342
  misuses) \`--json\` still gets this envelope on its own failure. A driver
206637
- piping stdout into a JSON parser on a failed run reads nothing; read stderr
206638
- or the exit code to learn why.
206343
+ reading stdout on a failed run reads nothing; read stderr or the exit code
206344
+ to learn why.
206639
206345
  `;
206640
206346
  const DRIVER_OBLIGATIONS = `
206641
206347
  ## Driver obligations, in order
206642
206348
 
206643
- 1. Read the resolved rest's \`kind\` off \`gtd next --sh\` once per iteration
206644
- (assign its output to a variable, THEN \`eval\` that variable — never
206645
- \`eval "$(gtd next --sh)"\` directly, since a failed \`gtd next\` would then
206646
- eval the empty string and die later on some unrelated unset variable
206647
- instead of aborting cleanly). Exit code is uniformly \`0\` on success now —
206648
- it never says whose turn is next; only \`1\`/\`2\` means gtd itself failed.
206649
- There is no opening move: a human's pending edit arrives as a
206650
- \`kind: "capture"\` beat, which you land immediately without executing
206651
- anything. EXCEPT: don't trust \`gtd_idle\` on the run's very first
206652
- iteration — land that beat first, so a workflow whose initial state
206653
- declares its own clean-tree \`"C"\` pattern still gets one chance to
206654
- advance before you conclude nothing is owed.
206655
- 2. Read the content to run/show off the SAME \`--sh\` read's \`gtd_content\` —
206656
- never off plain \`gtd next\`, which is not a parsing surface (it wraps
206657
- \`content\` in a status-summary header at every kind but \`prompt\`; see the
206658
- beat protocol above). At a \`prompt\` beat this content embeds a full
206659
- diff, so hand it to the agent CLI over STDIN, never as a command-line
206660
- argument — argv is capped (roughly 1 MB on macOS, and POSIX guarantees
206661
- only 4 KB, \`ARG_MAX\`), both reachable by an ordinary diff, and an
206662
- argv-passing driver fails on the first large one in a way that looks
206663
- like an agent error rather than a driver bug.
206664
- 3. A \`kind: "stalled"\` beat prints \`gtd_content\` (already in hand — no
206665
- re-invocation needed) to stderr and halts with a non-zero exit; a
206666
- \`kind: "message"\` beat prints plain \`gtd next\`'s output and exits 0,
206667
- unless it is the run's opening beat — land that one, since the human's
206668
- re-invocation while resting there is itself their decision.
206669
- 4. Run scripts with their output appended to \`gtd_log\` — gtd never creates
206670
- or truncates that file itself; truncate it once per run. \`$GTD_LOOP_LOG\`
206671
- overrides its path.
206672
- 5. Map \`gtd_session_id\`/\`gtd_session_resume\` onto the agent CLI's own
206349
+ 1. Read the resolved rest's \`kind\` off \`gtd next --json=kind\` once per
206350
+ iteration. Exit code is uniformly \`0\` on success now — it never says
206351
+ whose turn is next; only \`1\`/\`2\` means gtd itself failed. There is no
206352
+ opening move: a human's pending edit arrives as a \`kind: "capture"\` beat,
206353
+ which you land immediately without executing anything. EXCEPT: don't
206354
+ trust \`idle\` on the run's very first iteration — land that beat first, so
206355
+ a workflow whose initial state declares its own clean-tree \`"C"\` pattern
206356
+ still gets one chance to advance before you conclude nothing is owed.
206357
+ 2. Read the content to run/show off \`--json=content\` — never off plain
206358
+ \`gtd next\`, which is not a parsing surface (it wraps \`content\` in a
206359
+ status-summary header at every kind but \`prompt\`; see the beat protocol
206360
+ above). At a \`prompt\` beat, though, pipe plain \`gtd next\`'s own output to
206361
+ the agent instead of reading \`--json=content\` separately — it IS the bare
206362
+ content at that kind, so a second read would only render the same diff
206363
+ twice. Whichever form you read, hand it to the agent CLI over STDIN, never
206364
+ as a command-line argument — argv is capped (roughly 1 MB on macOS, and
206365
+ POSIX guarantees only 4 KB, \`ARG_MAX\`), both reachable by an ordinary
206366
+ diff, and an argv-passing driver fails on the first large one in a way
206367
+ that looks like an agent error rather than a driver bug.
206368
+ 3. A \`kind: "stalled"\` beat prints \`--json=content\` to stderr and halts with
206369
+ a non-zero exit; a \`kind: "message"\` beat prints plain \`gtd next\`'s
206370
+ output and exits 0, unless it is the run's opening beat — land that one,
206371
+ since the human's re-invocation while resting there is itself their
206372
+ decision.
206373
+ 4. Run scripts with their output appended to \`--json=log\` — gtd never
206374
+ creates or truncates that file itself; truncate it once per run.
206375
+ \`$GTD_LOOP_LOG\` overrides its path.
206376
+ 5. Map \`--json=session.id\`/\`--json=session.resume\` onto the agent CLI's own
206673
206377
  session flags — try \`resume\`'s hinted flag first and fall back to the
206674
206378
  other on failure (\`resume\` is a hint, not a contract: nothing is stored,
206675
206379
  so a crashed prior turn or an expired agent session recovers by itself).
206676
- 6. After a \`prompt\` beat, run the same read's own \`gtd_validate\` script
206380
+ 6. After a \`prompt\` beat, run the same beat's own \`--json=validate\` script
206677
206381
  (when set) and re-prompt its output verbatim on failure — the DRIVER
206678
206382
  owns the retry cap, not gtd.
206679
206383
  7. Land only a beat you acted on (\`capture\`/\`script\`/\`prompt\`) — a stray
206680
206384
  \`gtd land\` at a clean \`prompt\` rest authors an empty attempt on purpose
206681
206385
  (that IS the stall bookkeeping), so don't land beats you didn't dispatch.
206682
- 8. Run \`gtd land --sh\` (assign, then \`eval\`, exactly like obligation 1) and
206683
- pipe its own \`gtd_script\` into \`sh\` — never a bare \`gtd land | sh\`,
206684
- which would hand an empty script to \`sh\` on a refusal instead of
206685
- stopping first.
206686
- 9. Check the SAME \`gtd land --sh\` read's \`gtd_settled\`/\`gtd_idle\` fields —
206687
- no second read needed to decide. \`gtd_settled = true\` means stop right
206688
- there (a no-op at a \`script\` rest settles in place; nothing more to
206689
- read); otherwise keep looping, and the run finishes when a LATER
206690
- \`gtd next --sh\` reports \`gtd_idle = true\` (read once more with plain
206691
- \`gtd next\` only to show that gate's message, the decision to stop
206692
- already made). Exit code carries none of this any more — every command
206693
- exits \`0\` on success uniformly.
206386
+ 8. Read \`--json=settled\`/\`--json=idle\` from \`gtd land\` BEFORE running the
206387
+ script — \`gtd land\` never mutates, it only plans and prints, so these
206388
+ reads and the \`--json=script\` read that follows all agree, planned
206389
+ against the same untouched tree. Pipe \`--json=script\`'s own output into
206390
+ \`sh\` — never a bare \`gtd land | sh\`, which would hand an empty script to
206391
+ \`sh\` on a refusal instead of stopping first.
206392
+ 9. Check the settled/idle values read in step 8 — no second read needed to
206393
+ decide. \`settled = true\` means stop right there (a no-op at a \`script\`
206394
+ rest settles in place; nothing more to read); otherwise keep looping, and
206395
+ the run finishes when a LATER \`gtd next --json=idle\` reports \`true\`
206396
+ (read once more with plain \`gtd next\` only to show that gate's message,
206397
+ the decision to stop already made). Exit code carries none of this any
206398
+ more — every command exits \`0\` on success uniformly.
206694
206399
  `;
206695
206400
  const RECOVERY = `
206696
206401
  ## Recovery
@@ -206698,14 +206403,14 @@ const RECOVERY = `
206698
206403
  gtd exiting 2 means nothing was even attempted — a usage error; exiting 1
206699
206404
  means a refusal — nothing was emitted either. An emitted script exiting
206700
206405
  non-zero when YOU run it means something MAY have partially happened. Both
206701
- recover the same way: re-invoke gtd (\`gtd next --sh\`, \`gtd next\`, then
206702
- \`gtd land --sh\`). It re-reads the real repository state fresh every time —
206703
- never a cached plan — and emits whatever still needs to happen from there.
206704
- This works because every emitted script asserts its own HEAD precondition,
206705
- so a script generated against a repository state that has since moved
206706
- refuses loudly instead of corrupting anything. Scripts are re-runnable and
206707
- assert their own HEAD precondition — a driver needs no retry logic beyond
206708
- "ask gtd again".
206406
+ recover the same way: re-invoke gtd (\`gtd next --json=kind\`, \`gtd next\`,
206407
+ then \`gtd land --json=<path>\`). It re-reads the real repository state fresh
206408
+ every time — never a cached plan — and emits whatever still needs to happen
206409
+ from there. This works because every emitted script asserts its own HEAD
206410
+ precondition, so a script generated against a repository state that has
206411
+ since moved refuses loudly instead of corrupting anything. Scripts are
206412
+ re-runnable and assert their own HEAD precondition — a driver needs no retry
206413
+ logic beyond "ask gtd again".
206709
206414
  `;
206710
206415
  const referenceImplementation = () => `
206711
206416
  ## Building the user's driver: interview first, then adapt
@@ -206734,19 +206439,19 @@ follow in the same numbered list.
206734
206439
  committing it first turns the config into the very first thing the
206735
206440
  process plans against. Commit it, THEN start driving.
206736
206441
  4. **Which agent should run the turns?** Whatever coding-agent CLI they
206737
- already use (default: \`claude\`). Map \`gtd_session_id\`/
206738
- \`gtd_session_resume\` onto THAT agent's continuation mechanism
206739
- (obligation 5); an agent with no session concept just ignores them —
206740
- memory is an optimization, every prompt is self-contained.
206442
+ already use (default: \`claude\`). Map \`session.id\`/\`session.resume\`
206443
+ onto THAT agent's continuation mechanism (obligation 5); an agent with
206444
+ no session concept just ignores them — memory is an optimization, every
206445
+ prompt is self-contained.
206741
206446
  5. **Under which permission model?** Fully autonomous turns (e.g.
206742
206447
  \`--dangerously-skip-permissions\`), a sandbox, or the agent's default
206743
206448
  prompting — their risk tolerance, their call. Also whether the workflow's
206744
- own \`gtd_model\` hints should be honored (default: yes).
206449
+ own \`model\` hints should be honored (default: yes).
206745
206450
  6. **How do they want to invoke it?** A command on PATH (default:
206746
206451
  \`~/.local/bin/gtd-loop\`), a project task-runner entry, a CI job step —
206747
206452
  or no artifact at all: YOU drive the beats yourself, following the
206748
206453
  obligations directly. Pick the runtime to match: bash, their language of
206749
- choice, anything that parses \`--json\`/\`--sh\` and spawns subprocesses.
206454
+ choice, anything that reads \`--json=<path>\` and spawns subprocesses.
206750
206455
  7. **What should happen at the boundaries?** Where the log goes (obligation
206751
206456
  4), and whether halting at a human gate should do anything richer than
206752
206457
  print — desktop notification, terminal-multiplexer status, editor focus —
@@ -206758,13 +206463,13 @@ follow in the same numbered list.
206758
206463
  (\`~/.local/bin/gtd-loop\`) — the interview can override either name.
206759
206464
 
206760
206465
  Then build it, and verify safely before the first real drive:
206761
- \`gtd next --sh\`/\`gtd next\` are both pure reads — eval/parse them, check
206762
- your kind dispatch against the table above, call them as often as you like.
206763
- Nothing happens until you run an emitted script.
206466
+ \`gtd next --json=<path>\`/\`gtd next\` are both pure reads — read/parse them,
206467
+ check your kind dispatch against the table above, call them as often as you
206468
+ like. Nothing happens until you run an emitted script.
206764
206469
 
206765
- The reference rendering in sh (no \`jq\`, no JSON parser at all — \`--sh\`'s
206766
- own \`gtd_\`-prefixed shell assignments are eval'd directly; the \`claude\`
206767
- lines are what answers 1–2 replace):
206470
+ The reference rendering in sh (no \`jq\`, no JSON parser at all — each
206471
+ \`--json=<path>\` call prints its one value directly; the \`claude\` lines are
206472
+ what answers 1–2 replace):
206768
206473
 
206769
206474
  \`\`\`bash
206770
206475
  ${MINIMAL_DRIVER}
@@ -206778,15 +206483,16 @@ a second artifact to the same interview (questions 8–9 above): an edit
206778
206483
  command that opens the steering file the process is resting at right now,
206779
206484
  suggested at \`~/.local/bin/gtd-edit\`.
206780
206485
 
206781
- \`gtd next --sh\` already reports \`file\` (\`gtd_file\` under \`--sh\`) for every
206782
- state that declares one — the edit command needs no new engine surface, just
206783
- a read and an editor spawn. \`file\` is an OPTIONAL field: it is genuinely
206784
- unset under \`--sh\` at a state that declares no \`file:\`, so read it as
206785
- \`\${gtd_file:-.gtd/TODO.md}\`, never bare — a bare read, or a
206786
- \`gtd next --json | jq .file\` recipe (the idea, never the recipe: \`--json\`
206787
- needs \`jq\`, and \`jq .file\` prints the string \`null\` on an unset field, which
206788
- opens a file literally named \`null\` in the repository root), ships that bug
206789
- to every user.
206486
+ \`gtd next --json=file\` already reports the file for every state that
206487
+ declares one — the edit command needs no new engine surface, just a read and
206488
+ an editor spawn. \`file\` is an OPTIONAL field: \`--json=file\` prints nothing
206489
+ (not the string \`null\`) at a state that declares no \`file:\`, so read it into
206490
+ a shell variable and fall back explicitly — \`f="$(gtd next --json=file)"\`
206491
+ then \`f="\${f:-.gtd/TODO.md}"\`, never assume the read populated anything. This
206492
+ is also the general rule for every optional field \`--json=<path>\` exposes:
206493
+ an absent field is silence, not the string \`null\` a \`jq .field\` recipe would
206494
+ print on the same input — code that treats empty output as "field genuinely
206495
+ absent" is correct, code that expects a literal \`null\` string is not.
206790
206496
 
206791
206497
  This is also how a human starts a process at all: at the initial \`idle\`
206792
206498
  state the resting file IS \`.gtd/TODO.md\`, so on a clean repository the edit
@@ -206806,19 +206512,108 @@ const PREREQUISITES = `
206806
206512
  ## Prerequisites and portability
206807
206513
 
206808
206514
  - A POSIX \`sh\` (dash, ash, bash's own POSIX mode, etc.) — gtd's own emitted
206809
- scripts (\`gtd land\`, \`gtd --entry <state>\`, \`gtd abandon\`,
206515
+ scripts (\`gtd land --json=script\`, \`gtd --entry <state>\`, \`gtd abandon\`,
206810
206516
  \`gtd restore\`) are POSIX sh; captured, then piped into it (see obligation
206811
- 8 above). \`eval\`ing \`gtd next --sh\`/\`gtd land --sh\`'s own output needs
206812
- nothing beyond the same POSIX \`sh\`.
206517
+ 8 above). Reading \`gtd next --json=<path>\`/\`gtd land --json=<path>\`'s own
206518
+ output needs nothing beyond the same POSIX \`sh\` — no \`eval\`, no parser.
206813
206519
  - \`gtd\` on \`PATH\` — a seeded mode \`validate:\` command is literally the
206814
206520
  string \`gtd check <mode> '<file>'\`, resolved by NAME at script-run time.
206815
206521
  Keep one \`gtd\` on \`PATH\`, consistently.
206816
206522
 
206817
- Any runtime works: the contract is \`--json\`/\`--sh\` in, subprocesses out.
206523
+ Any runtime works: the contract is \`--json\`/\`--json=<path>\` in, subprocesses
206524
+ out.
206818
206525
  `;
206819
206526
  const renderBriefing = () => HEADER() + BEAT_PROTOCOL + JSON_FIELD_REFERENCE + DRIVER_OBLIGATIONS + RECOVERY + referenceImplementation() + editCommand() + PREREQUISITES;
206820
206527
  //#endregion
206528
+ //#region src/Select.ts
206529
+ /**
206530
+ * Renders a fully-walked leaf value: scalars/booleans stringify directly,
206531
+ * arrays become one `JSON.stringify` per entry, newline-joined.
206532
+ * `selectPath`'s loop already returns `absent` for `undefined`/`null` after
206533
+ * every segment (including the last), so neither ever reaches here.
206534
+ */
206535
+ const toSelection = (value) => {
206536
+ if (Array.isArray(value)) return {
206537
+ kind: "value",
206538
+ text: value.map((entry) => JSON.stringify(entry)).join("\n")
206539
+ };
206540
+ if (typeof value === "object") return {
206541
+ kind: "value",
206542
+ text: JSON.stringify(value)
206543
+ };
206544
+ return {
206545
+ kind: "value",
206546
+ text: String(value)
206547
+ };
206548
+ };
206549
+ /** A segment that resolves to no key at all (missing from an object, or numeric/absent against an array) — distinct from a present key holding `undefined`. */
206550
+ const NOT_FOUND = Symbol("not-found");
206551
+ /**
206552
+ * One step of the walk: looks `segment` up on `current`, or reports
206553
+ * `NOT_FOUND` for a key that was never there (no array indexing; a primitive
206554
+ * has no keys). Presence is an OWN-property test
206555
+ * (`Object.prototype.hasOwnProperty`), never the `in` operator — `in` walks
206556
+ * the prototype chain, so it would resolve inherited members
206557
+ * (`constructor`, `toString`, `hasOwnProperty`, `valueOf`, ...) as real
206558
+ * document fields. An array declares no non-index own key the document ever
206559
+ * uses, so every array segment is `NOT_FOUND` (all-digit ones already were;
206560
+ * this also now excludes `length`/`map`/every other inherited array member).
206561
+ */
206562
+ const resolveSegment = (current, segment) => {
206563
+ if (Array.isArray(current)) return NOT_FOUND;
206564
+ if (typeof current === "object" && current !== null) {
206565
+ const record = current;
206566
+ return Object.prototype.hasOwnProperty.call(record, segment) ? record[segment] : NOT_FOUND;
206567
+ }
206568
+ return NOT_FOUND;
206569
+ };
206570
+ /**
206571
+ * Walks `fields` by dotted key path. Never throws — any unwalkable shape
206572
+ * (a primitive mid-path, an array indexed by a numeric segment) degrades to
206573
+ * `unknown`, and a present-but-`undefined` value anywhere along the path
206574
+ * short-circuits the whole remaining path to `absent` rather than reporting
206575
+ * a key that was never actually missing.
206576
+ */
206577
+ const selectPath = (fields, path) => {
206578
+ try {
206579
+ let current = fields;
206580
+ for (const segment of path.split(".")) {
206581
+ current = resolveSegment(current, segment);
206582
+ if (current === NOT_FOUND) return {
206583
+ kind: "unknown",
206584
+ path
206585
+ };
206586
+ if (current === void 0 || current === null) return { kind: "absent" };
206587
+ }
206588
+ return toSelection(current);
206589
+ } catch {
206590
+ return {
206591
+ kind: "unknown",
206592
+ path
206593
+ };
206594
+ }
206595
+ };
206596
+ //#endregion
206821
206597
  //#region src/program.ts
206598
+ /**
206599
+ * A command-level failure that should exit like a CLI usage error (2), not a
206600
+ * generic runtime failure (1) — currently only the `--json=<unknown-path>`
206601
+ * selector case.
206602
+ */
206603
+ var SelectorUsageError = class extends Error {};
206604
+ /**
206605
+ * The `--json=<path>` select branch, shared by `runNextCommand` and
206606
+ * `runLandCommand` so the two never drift on the unknown-selector message: a
206607
+ * `value` writes its text plus exactly one trailing newline, `absent` writes
206608
+ * nothing (the caller's normal success path continues), and `unknown` fails
206609
+ * with `SelectorUsageError` (mapped to `EXIT_USAGE_ERROR` by `Cli.ts`'s
206610
+ * `report`).
206611
+ */
206612
+ const writeSelection = (out, fields, path) => gen(function* () {
206613
+ const selection = selectPath(fields, path);
206614
+ if (selection.kind === "value") out.write(`${selection.text}\n`);
206615
+ else if (selection.kind === "unknown") return yield* fail$2(new SelectorUsageError(`gtd: unknown --json selector "${selection.path}" — see \`gtd --help\``));
206616
+ });
206822
206617
  /** `needs: "none"` skips the repo-root guard — the server is keyed on file name, not workflow state. */
206823
206618
  const runLspCommand = () => startLspServer();
206824
206619
  /** Emitted by the binary itself (`src/Install.ts`'s `renderBriefing()`) so it's always current. Writes nothing else; runs from any directory since it resolves no workflow state. */
@@ -206855,98 +206650,27 @@ const runInitCommand = (out) => gen(function* () {
206855
206650
  * `normalizeTrailingNewline` rather than imported, to avoid a circular value
206856
206651
  * dependency (`Cli.ts` already depends on this module). `LandResult.script`
206857
206652
  * must carry its own trailing newline because it's embedded verbatim inside
206858
- * the `--json`/`--sh` documents too, never re-normalized at their tail.
206653
+ * the `--json` document, never re-normalized at its tail.
206859
206654
  */
206860
206655
  const normalizeScriptNewline = (script) => script.length === 0 ? script : `${script.replace(/\n+$/, "")}\n`;
206861
206656
  const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
206862
206657
  const headPreconditions = (currentCommit) => ({ expectedHead: currentCommit });
206863
- /**
206864
- * The resting state's own steering-mode format/validate commands, embedded
206865
- * ahead of the commit. Empty for a state declaring no `file:`+`mode:` pair;
206866
- * an unknown mode name is a refusal.
206867
- *
206868
- * Also empty when the step deletes that file, or when it's absent from the
206869
- * working tree — either way there's nothing left to format/validate.
206870
- * Emitting `format:` anyway would make the step unlandable for a formatter
206871
- * that errors on a missing path (e.g. `prettier --write`), since it's the
206872
- * first command in a `set -euo pipefail` script and would abort the whole
206873
- * thing — window close, commit and all — before anything could land.
206874
- */
206875
- const steeringModeSteps = (rest) => gen(function* () {
206876
- const file = rest.hints.file;
206877
- const mode = rest.stateDef.mode;
206878
- if (file === void 0 || mode === void 0) return [];
206879
- if (deletesFile(rest.changes, file)) return [];
206880
- if ((yield* RepoFiles).working(file) === void 0) return [];
206881
- const resolved = resolveSteeringMode(rest.def, mode);
206882
- if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
206883
- return yield* renderSteeringModeCommandSteps(resolved, file, rest.context);
206884
- });
206885
- /**
206886
- * The `optional` half: re-open the review checkout window when the step
206887
- * lands at a `reviewWindow: true` state, `""` otherwise. Takes the
206888
- * already-decided `OpenWindowDecision` rather than deciding itself, since
206889
- * narration belongs at the caller.
206890
- *
206891
- * `"HEAD"` is deliberately the literal string, not a resolved hash — git
206892
- * resolves it when this script runs, after the required script has already
206893
- * landed the new commit. For the same reason it carries no `expectedHead`
206894
- * precondition: that hash doesn't exist yet at decide time.
206895
- */
206896
- const openWindowScript = (decision) => {
206897
- if (!decision.shouldOpen) return "";
206898
- return emitScripts({}, [], [{
206899
- kind: "gitWrite",
206900
- command: buildOpenWindowScript({
206901
- base: decision.base,
206902
- head: decision.head
206903
- })
206904
- }]).optional;
206905
- };
206906
- /** `decideOpenWindow`'s result, narrated here (the one caller with `Narrator` in scope) — `decideOpenWindow` itself stays pure. */
206907
- const decideAndNarrateOpenWindow = (rest, targetState) => gen(function* () {
206908
- const decision = decideOpenWindow(rest.def, targetState, rest.run, "HEAD");
206909
- yield* (yield* Narrator).narrate(decision.shouldOpen ? `review-window: open (base ${decision.base.slice(0, 7)})` : "review-window: no-op");
206910
- return decision;
206911
- });
206912
- /** 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). */
206658
+ /** True for a `"commit"` decision that is an ATTEMPT (`PatternMachine.StepCommit.attempt`) — the one flag `enforceStepGuards` bypasses its own steps for (see its call site). */
206913
206659
  const isAttemptDecision = (decision) => decision.kind === "commit" && decision.attempt === true;
206914
206660
  /** The subject the required script will produce. */
206915
206661
  const previewSubject = (decision) => succeed$3(decision.subject);
206916
206662
  /**
206917
- * The `required` half: the review-window close (when one is open), the
206918
- * resting state's own steering-mode commands, then the commit steps.
206919
- * The steering-mode step is skipped for an attempt commit — nothing to
206920
- * format/validate in an empty diff, and running `format:` ahead of the
206921
- * commit could dirty the tree and turn an "empty" attempt non-empty, breaking
206922
- * `stalledAt`'s derivation. The window close still runs regardless —
206923
- * committing with a window open would land the attempt on the review base.
206663
+ * The `required` half: the HEAD assertion plus the commit steps, nothing
206664
+ * else. The steering-file format/validate pair is no longer part of the
206665
+ * landing script — that's a driver contract now (`gtd next --json`'s
206666
+ * `validate` field), not a gtd guarantee (see `resolveSelfValidateCommand`).
206924
206667
  */
206925
- const buildRequiredScript = (rest, decision, cost, model) => gen(function* () {
206926
- const git = yield* GitService;
206927
- const closeDecision = yield* decideCloseWindow;
206928
- yield* (yield* Narrator).narrate(closeDecision.shouldClose ? "review-window: close" : "review-window: no-op");
206929
- const isAttempt = isAttemptDecision(decision);
206930
- const steps = [
206931
- ...closeDecision.shouldClose ? [{
206932
- kind: "gitWrite",
206933
- command: buildCloseWindowScript(closeDecision.refs)
206934
- }] : [],
206935
- ...isAttempt ? [] : yield* steeringModeSteps(rest),
206936
- ...yield* renderDecision(git, rest, decision, cost, model)
206937
- ];
206938
- return emitScripts(headPreconditions(rest.context.currentCommit), steps).required;
206939
- });
206668
+ const buildRequiredScript = (rest, decision, cost, model) => succeed$3(emitScripts(headPreconditions(rest.context.currentCommit), renderDecision(rest, decision, cost, model)).required);
206940
206669
  /**
206941
206670
  * Decide the one resulting transition (a commit) for `gtd land`
206942
206671
  * without performing it, authenticating as `rest.actor`. Refusals fail the
206943
206672
  * Effect with a formatted message; a no-op returns `subject: null` and empty
206944
206673
  * scripts.
206945
- *
206946
- * `plan.scripts` (built by `Edge.ts`) is not reused here: its precondition
206947
- * uses `rest.context.currentCommit`, which while a review window is open is
206948
- * the window's base, not the real head, and it contains no window close/open
206949
- * steps. This function builds its own required/optional pair instead.
206950
206674
  */
206951
206675
  const planLanding = (opts = {}) => gen(function* () {
206952
206676
  const rest = yield* currentRest;
@@ -206974,33 +206698,28 @@ const planLanding = (opts = {}) => gen(function* () {
206974
206698
  context: rest.context,
206975
206699
  file: rest.hints.file,
206976
206700
  changes: rest.changes,
206977
- windowHead: rest.windowHead,
206978
206701
  kind: decision.kind,
206979
206702
  attempt: isAttemptDecision(decision)
206980
206703
  });
206981
206704
  const restingState = decision.to;
206982
- const settled = yield* collapsesToInitialState(rest, decision);
206983
- const openDecision = yield* decideAndNarrateOpenWindow(rest, restingState);
206984
206705
  const required = yield* buildRequiredScript(rest, decision, opts.cost, opts.model);
206985
206706
  return {
206986
206707
  state: restingState,
206987
206708
  subject: yield* previewSubject(decision),
206988
206709
  cost: opts.cost ?? null,
206989
206710
  model: opts.model ?? null,
206990
- script: normalizeScriptNewline(combinedScript(required, openWindowScript(openDecision))),
206991
- settled,
206711
+ script: normalizeScriptNewline(combinedScript(required, "")),
206712
+ settled: false,
206992
206713
  idle: restingState === initialStateOf(rest.def)
206993
206714
  };
206994
206715
  });
206995
206716
  /**
206996
206717
  * `gtd summary`: print the prompt for an agent to write the process HEAD
206997
206718
  * closes or sits inside its own closing message. Writes nothing — no git, no
206998
- * state transition, no file, no review-window bracket (see AGENTS.md: a
206999
- * write-nothing command emits its own bracket only when it needs one; this
207000
- * one never does). Refuses (throws, mapped to the runtime-error exit code) on
207001
- * either of the two conditions `src/Summary.ts`'s `buildSummary` folds into
207002
- * one `undefined`: the workflow declares no `summary:` template, or the
207003
- * resolved run has no commits to name.
206719
+ * state transition, no file. Refuses (throws, mapped to the runtime-error
206720
+ * exit code) on either of the two conditions `src/Summary.ts`'s
206721
+ * `buildSummary` folds into one `undefined`: the workflow declares no
206722
+ * `summary:` template, or the resolved run has no commits to name.
207004
206723
  */
207005
206724
  const runSummaryCommand = (out) => gen(function* () {
207006
206725
  const config = yield* (yield* ConfigService).load;
@@ -207011,18 +206730,37 @@ const runSummaryCommand = (out) => gen(function* () {
207011
206730
  out.write(rendered.endsWith("\n") ? rendered : rendered + "\n");
207012
206731
  });
207013
206732
  /**
206733
+ * `gtd base`: print the review anchor hash — `reviewBaseFor(rest.def,
206734
+ * rest.run)` — bare and newline-terminated, so an external tool (a diff, a
206735
+ * PR tool, another agent) can be pointed at the range under review. Shaped
206736
+ * exactly like `runSummaryCommand`: one `Rest` resolved, nothing written — no
206737
+ * git, no state transition, no session identity. Refuses (mapped to the
206738
+ * runtime-error exit code) when the resolved run has no commits to name —
206739
+ * the only case where the hash would name a range that corresponds to no
206740
+ * review, exactly the way `runSummaryCommand` refuses on an empty trace.
206741
+ */
206742
+ const runBaseCommand = (out) => gen(function* () {
206743
+ const rest = yield* currentRest;
206744
+ if (rest.run.trace.length === 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd base: refused — no process is underway at HEAD"));
206745
+ const base = reviewBaseFor(rest.def, rest.run);
206746
+ out.write(`${base}\n`);
206747
+ });
206748
+ /**
207014
206749
  * `gtd land [--cost=<n>] [--model=<name>]`: land whatever the tree shows at
207015
206750
  * the currently resolved rest, authenticated as the rest's own actor.
207016
- * Plain output writes `result.script` verbatim so `gtd land | sh` keeps
207017
- * working; `--json`/`--sh` emit `script` alongside `settled`/`idle`/`state`/
207018
- * `subject`/`cost`/`model`. Exit code is uniformly `EXIT_OK` on success —
207019
- * whose turn is next lives in the following `gtd next --json`'s `kind` field.
207020
- */
207021
- const runLandCommand = (opts, json, sh, out) => gen(function* () {
207022
- const result = yield* planLanding(opts);
207023
- if (json) out.write(renderLandJson(landFields(result)));
207024
- else if (sh) out.write(renderLandSh(landFields(result)));
207025
- else out.write(result.script);
206751
+ * Plain output is `renderLandPlain`'s prose — the commit subject plus a
206752
+ * pointer at `--json=script`, or the no-op note when nothing landed — never
206753
+ * the script itself; `--json` emits `script` (byte-identical to before)
206754
+ * alongside `settled`/`idle`/`state`/`subject`/`cost`/`model`, so it stays
206755
+ * the machine path a driver pipes to `sh`. Exit code is uniformly `EXIT_OK`
206756
+ * on success — whose turn is next lives in the following `gtd next --json`'s
206757
+ * `kind` field.
206758
+ */
206759
+ const runLandCommand = (opts, json, out) => gen(function* () {
206760
+ const built = landFields(yield* planLanding(opts));
206761
+ if (json.kind === "document") out.write(renderLandJson(built));
206762
+ else if (json.kind === "select") yield* writeSelection(out, built, json.path);
206763
+ else out.write(renderLandPlain(built));
207026
206764
  });
207027
206765
  /**
207028
206766
  * `gtd --entry <state> [--var <name>=<value> ...]` (`actor` always `"human"`):
@@ -207085,13 +206823,8 @@ const runAbandonCommand = (out) => gen(function* () {
207085
206823
  }
207086
206824
  const restState = run.trace[run.trace.length - 1].state;
207087
206825
  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`));
207088
- const closeDecision = yield* decideCloseWindow;
207089
- const tip = closeDecision.shouldClose ? closeDecision.refs.headHash : yield* git.resolveRef("HEAD");
206826
+ const tip = yield* git.resolveRef("HEAD");
207090
206827
  const steps = [
207091
- ...closeDecision.shouldClose ? [{
207092
- kind: "gitWrite",
207093
- command: buildCloseWindowScript(closeDecision.refs)
207094
- }] : [],
207095
206828
  {
207096
206829
  kind: "gitWrite",
207097
206830
  command: updateRef(HISTORY_REF, tip)
@@ -207105,12 +206838,11 @@ const runAbandonCommand = (out) => gen(function* () {
207105
206838
  command: abandonedOutcome(restState, run.startParentHash, initial)
207106
206839
  }
207107
206840
  ];
207108
- const required = emitScripts(headPreconditions(yield* git.resolveRef("HEAD")), steps).required;
206841
+ const required = emitScripts(headPreconditions(tip), steps).required;
207109
206842
  out.write(combinedScript(required, ""));
207110
206843
  });
207111
206844
  /**
207112
- * `gtd restore`: hard-reset HEAD back to the tip `gtd abandon` (or the
207113
- * initial-state collapse's own mixed reset) retained.
206845
+ * `gtd restore`: hard-reset HEAD back to the tip `gtd abandon` retained.
207114
206846
  *
207115
206847
  * Guarded by `restorability` so it never discards work it didn't create:
207116
206848
  * refuses on a dirty working tree, no retained history, or HEAD having
@@ -207172,9 +206904,8 @@ const emitsValidatablePrompt = (rendered) => rendered.kind === "prompt" && rende
207172
206904
  /**
207173
206905
  * Render `resolved`'s format:/validate: commands as `EmitStep[]`, wrapping
207174
206906
  * the last one with `onFailure: fixPromptInstruction(file)` when the
207175
- * validator is a command — shared by `resolveValidateScript` and
207176
- * `steeringModeSteps` so the two never drift on which command gets the
207177
- * routable fix prompt.
206907
+ * validator is a command — `resolveValidateScript`'s own helper, split out
206908
+ * so its render logic isn't inlined into that function's body.
207178
206909
  */
207179
206910
  const renderSteeringModeCommandSteps = (resolved, file, context) => gen(function* () {
207180
206911
  const commands = yield* renderSteeringCommands(resolved, file, context);
@@ -207288,20 +207019,20 @@ const restBeatKind = (rest) => beatKindOf({
207288
207019
  /** `idle` means exactly one thing: the machine rests at the workflow's initial state with a clean tree — the process is genuinely done. */
207289
207020
  const restIsIdle = (rest) => rest.state === initialStateOf(rest.def) && rest.changes.length === 0;
207290
207021
  /**
207291
- * `gtd next`: pure emitter of the resolved rest's beat, in three encodings —
207292
- * `--json`/`--sh` and plain (the default). No mutation: nothing is written,
207022
+ * `gtd next`: pure emitter of the resolved rest's beat, in two encodings —
207023
+ * `--json` and plain (the default). No mutation: nothing is written,
207293
207024
  * so a peek and a would-be dispatch are the same call. Exit code is
207294
207025
  * `EXIT_OK` unconditionally — whose turn is next lives in the beat
207295
207026
  * document's own `kind` field, never the exit code.
207296
207027
  */
207297
- const runNextCommand = (json, sh, out) => gen(function* () {
207028
+ const runNextCommand = (json, out) => gen(function* () {
207298
207029
  const rest = yield* currentRest;
207299
207030
  const rendered = yield* renderRest(rest);
207300
207031
  const fields = yield* gatherBeatFields(rest, rendered);
207301
207032
  const narrator = yield* Narrator;
207302
207033
  for (const change of fields.changes) yield* narrator.narrate(`pending: ${change.status} ${change.path} -> ${change.pattern ?? "(no match)"}`);
207303
- if (json) out.write(renderBeatJson(fields));
207304
- else if (sh) out.write(renderBeatSh(fields));
207034
+ if (json.kind === "document") out.write(renderBeatJson(fields));
207035
+ else if (json.kind === "select") yield* writeSelection(out, fields, json.path);
207305
207036
  else {
207306
207037
  const selfValidateCommand = emitsValidatablePrompt(rendered) ? yield* resolveSelfValidateCommand(rest.def, rendered.mode, rendered.file, rest.context).pipe(catchAll(() => succeed$3(void 0))) : void 0;
207307
207038
  out.write(renderBeatPlain(fields, selfValidateCommand));
@@ -207405,7 +207136,7 @@ const computeNextMatch = (onEdges, changes) => {
207405
207136
  }
207406
207137
  return null;
207407
207138
  };
207408
- /** Everything one beat needs beyond the resolved rest itself, gathered once so plain/`--json`/`--sh` can never describe different rests for the same beat. */
207139
+ /** Everything one beat needs beyond the resolved rest itself, gathered once so plain/`--json` can never describe different rests for the same beat. */
207409
207140
  const gatherBeatFields = (rest, rendered) => gen(function* () {
207410
207141
  const kind = restBeatKind(rest);
207411
207142
  const session = kind === "prompt" ? resolveSession(rendered.memory, rendered.memoryResumed) : void 0;
@@ -207430,14 +207161,11 @@ const gatherBeatFields = (rest, rendered) => gen(function* () {
207430
207161
  });
207431
207162
  /**
207432
207163
  * Best-effort resolution of the currently-rested state for the viewer's
207433
- * `/state.json` route: prefers the review window's saved head over HEAD
207434
- * itself, since a request landing mid-window would otherwise read a HEAD
207435
- * that's been temporarily rewound. Any failure is swallowed to `null` — the
207436
- * browser just hides the panel.
207164
+ * `/state.json` route. Any failure is swallowed to `null` — the browser just
207165
+ * hides the panel.
207437
207166
  */
207438
207167
  const computeCurrentState = (model) => gen(function* () {
207439
- const reviewHead = yield* (yield* GitService).readRefOption(REVIEW_HEAD_REF);
207440
- const rest = yield* restAt(getOrUndefined(reviewHead));
207168
+ const rest = yield* restAt(void 0);
207441
207169
  const group = model.states.find((s) => s.name === rest.state)?.group;
207442
207170
  return buildCurrentStateModel(rest, rest.changes, rest.on, group);
207443
207171
  });
@@ -207524,7 +207252,7 @@ const needsOf = (kind) => {
207524
207252
  * unrepresentable: every branch returns `Effect<void>`, and `Cli.ts`'s
207525
207253
  * `runCli` supplies `EXIT_OK` once, after this Effect succeeds.
207526
207254
  */
207527
- const dispatchVoidCommand = (command, json, sh, out) => {
207255
+ const dispatchVoidCommand = (command, json, out) => {
207528
207256
  switch (command.kind) {
207529
207257
  case "lsp": return runLspCommand();
207530
207258
  case "init": return runInitCommand(out);
@@ -207532,15 +207260,16 @@ const dispatchVoidCommand = (command, json, sh, out) => {
207532
207260
  case "land": return runLandCommand({
207533
207261
  ...command.cost !== void 0 ? { cost: command.cost } : {},
207534
207262
  ...command.model !== void 0 ? { model: command.model } : {}
207535
- }, json, sh, out);
207263
+ }, json, out);
207536
207264
  case "entry": return runEntryCommand(command.actor, command.state, command.vars, out, command.label);
207537
207265
  case "abandon": return runAbandonCommand(out);
207538
207266
  case "restore": return runRestoreCommand(out);
207539
- case "next": return runNextCommand(json, sh, out);
207267
+ case "next": return runNextCommand(json, out);
207540
207268
  case "validate": return runValidateCommand(out);
207541
207269
  case "check": return runCheckCommand(command.mode, command.file, command.openQuestions ?? false);
207542
207270
  case "install": return runInstallCommand(out);
207543
207271
  case "summary": return runSummaryCommand(out);
207272
+ case "base": return runBaseCommand(out);
207544
207273
  }
207545
207274
  };
207546
207275
  /**
@@ -207549,19 +207278,21 @@ const dispatchVoidCommand = (command, json, sh, out) => {
207549
207278
  * repo-root-and-commit guard exactly when `needsOf(command.kind) === "state"`
207550
207279
  * (the `standaloneKinds` run bare). Both guard checks run before `dispatch`,
207551
207280
  * so a refusal writes nothing and emits no script by construction.
207552
- *
207553
- * No review-window close/open bracket runs here — every state-command
207554
- * handler decides for itself, off `ReviewWindow.ts`'s pure decision
207555
- * functions, whether a close/open belongs in the script it emits.
207556
207281
  */
207557
- const runCommand = (command, json, sh, out) => {
207558
- const dispatch = dispatchVoidCommand(command, json, sh, out);
207282
+ const runCommand = (command, json, out) => {
207283
+ const dispatch = dispatchVoidCommand(command, json, out);
207559
207284
  if (needsOf(command.kind) !== "state") return dispatch;
207560
207285
  return gen(function* () {
207561
207286
  const git = yield* GitService;
207562
207287
  const fs = yield* FileSystem;
207563
207288
  yield* assertRunningFromRepoRoot(git, fs);
207564
207289
  yield* assertRepositoryHasCommits(git);
207290
+ const narrator = yield* Narrator;
207291
+ const config = yield* (yield* ConfigService).load.pipe(provideService(Narrator, {
207292
+ narrate: () => _void,
207293
+ warn: () => _void
207294
+ }));
207295
+ for (const warning of config.warnings) yield* narrator.warn(`gtd: warning: ${warning}`);
207565
207296
  yield* dispatch;
207566
207297
  });
207567
207298
  };
@@ -207576,29 +207307,24 @@ const nonNegativeNumber = (raw, flag) => {
207576
207307
  const FLAGS = [
207577
207308
  {
207578
207309
  name: "--json",
207579
- arity: 0,
207310
+ arity: "optional",
207580
207311
  repeatable: false,
207581
207312
  scope: (kind) => kind === "next" || kind === "land",
207582
- decode: () => right(true),
207313
+ decode: ([raw]) => right(raw ?? ""),
207583
207314
  scopeError: "gtd: --json is only valid for `gtd next`/`gtd land` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
207584
- valueHint: "",
207585
- help: ["(gtd next/gtd land only) output structured JSON", "instead of plain text. Mutually exclusive with --sh"],
207586
- conflicts: ["--sh"]
207587
- },
207588
- {
207589
- name: "--sh",
207590
- arity: 0,
207591
- repeatable: false,
207592
- scope: (kind) => kind === "next" || kind === "land",
207593
- decode: () => right(true),
207594
- scopeError: "gtd: --sh is only valid for `gtd next`/`gtd land` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
207595
- valueHint: "",
207315
+ valueHint: "<path>",
207596
207316
  help: [
207597
- "(gtd next/gtd land only) output gtd_-prefixed POSIX",
207598
- "shell assignments instead of plain text. Mutually",
207599
- "exclusive with --json"
207600
- ],
207601
- conflicts: ["--json"]
207317
+ "(gtd next/gtd land only) output structured JSON. Bare",
207318
+ "--json prints the whole document; --json=<path> (a dotted",
207319
+ "key path into that document, e.g. kind, content,",
207320
+ "session.id) prints just that value: a scalar raw and",
207321
+ "unquoted, a boolean as true/false, a list one JSON entry",
207322
+ "per line. An absent optional field prints nothing and",
207323
+ "exits 0 — including when an earlier segment of <path> is",
207324
+ "itself absent/null (e.g. session.id at a non-prompt rest),",
207325
+ "which never counts as unknown; an unknown path is a usage",
207326
+ "error (exit 2)."
207327
+ ]
207602
207328
  },
207603
207329
  {
207604
207330
  name: "--port",
@@ -207707,8 +207433,8 @@ const FLAGS = [
207707
207433
  help: [
207708
207434
  "enable stderr narration for this invocation: which rest",
207709
207435
  "resolved, which declared pattern each pending change",
207710
- "matched, which review-window action was emitted, and how",
207711
- "config resolved across layers. Aliased to -v"
207436
+ "matched, and how config resolved across layers. Aliased",
207437
+ "to -v"
207712
207438
  ]
207713
207439
  }
207714
207440
  ];
@@ -207754,13 +207480,15 @@ const COMMAND_ROWS = [
207754
207480
  "--cost=<n> (optionally --model=<name>) to record the",
207755
207481
  "just-finished invocation's token cost and model on the",
207756
207482
  "turn commit (summed into it.processCost/",
207757
- "processCostByModel). Plain (the default) prints ONLY the",
207758
- "script that records the landing; a driver runs it, e.g.",
207759
- "`gtd land | sh`. --json/--sh instead emit script (that",
207760
- "same script, byte-identical) alongside settled, idle,",
207761
- "state (the post-land target), subject, cost and model —",
207762
- "--json/--sh are mutually exclusive. Exits 0 on success, 1",
207763
- "on any refusal — see the Exit codes section below"
207483
+ "processCostByModel). Plain (the default) prints one",
207484
+ "human-readable sentence naming the commit subject (or a",
207485
+ "no-op note) plus a pointer to `gtd land --json=script | sh`",
207486
+ "to get the landing script — never the script itself.",
207487
+ "--json emits script (the actual POSIX sh",
207488
+ "a driver runs) alongside settled, idle, state (the",
207489
+ "post-land target), subject, cost and model. Exits 0 on",
207490
+ "success, 1 on any refusal — see the Exit codes section",
207491
+ "below"
207764
207492
  ]
207765
207493
  },
207766
207494
  {
@@ -207769,10 +207497,9 @@ const COMMAND_ROWS = [
207769
207497
  arity: "none",
207770
207498
  details: [
207771
207499
  "End the process currently underway without completing it:",
207772
- "close any open review checkout window, then rewind HEAD to",
207773
- "the commit the process started from, keeping everything it",
207774
- "produced as uncommitted changes. A no-op when no process is",
207775
- "underway"
207500
+ "rewind HEAD to the commit the process started from,",
207501
+ "keeping everything it produced as uncommitted changes. A",
207502
+ "no-op when no process is underway"
207776
207503
  ]
207777
207504
  },
207778
207505
  {
@@ -207795,19 +207522,22 @@ const COMMAND_ROWS = [
207795
207522
  details: [
207796
207523
  "Print the resolved rest's beat (no mutation, safe to",
207797
207524
  "poll), in one of three encodings. Plain (the default): a",
207798
- "status summary, a blank line, then the step verbatim —",
207799
- "except at a prompt rest, which is the bare step (plus the",
207800
- "self-validation instruction when applicable) with no",
207801
- "header, since those bytes are the agent's own input. --json",
207802
- "emits the one structured surface gtd has: kind",
207803
- "(capture|message|script|prompt|stalled) selects what a",
207804
- "driver does, content is what it runs or shows, idle marks",
207805
- "the workflow's initial state with a clean tree, plus the",
207806
- "prompt session, model, validate script, log path, changes,",
207807
- "next and the resting state's own fields. --sh emits the",
207808
- "same fields as gtd_-prefixed POSIX shell assignments.",
207809
- "--json/--sh are mutually exclusive. Exits 0 — see the",
207810
- "Exit codes section below"
207525
+ "status summary, a blank line, then the step verbatim — at a",
207526
+ "script/capture rest an instruction line ('Run this",
207527
+ "script:' / 'The edit is already made — run `gtd land` to",
207528
+ "land it.') precedes the status summary; at a prompt rest",
207529
+ "it's the bare step (plus the self-validation instruction",
207530
+ "when applicable) with no header at all, since those bytes",
207531
+ "are the agent's own input. --json emits the one structured",
207532
+ "surface gtd has: kind (capture|message|script|prompt|",
207533
+ "stalled) selects what a driver does, content is what it",
207534
+ "runs or shows, idle marks the workflow's initial state with",
207535
+ "a clean tree, plus the prompt session, model, validate",
207536
+ "script, log path, changes, next and the resting state's own",
207537
+ "fields. --json=<path> (a dotted key path into that same",
207538
+ "document, e.g. kind, content, session.id) prints just that",
207539
+ "value instead of the whole document — see --json's own help",
207540
+ "above. Exits 0 — see the Exit codes section below"
207811
207541
  ]
207812
207542
  },
207813
207543
  {
@@ -207890,6 +207620,20 @@ const COMMAND_ROWS = [
207890
207620
  "— runnable any time before the next thing lands on the",
207891
207621
  "branch"
207892
207622
  ]
207623
+ },
207624
+ {
207625
+ token: "base",
207626
+ kind: "base",
207627
+ arity: "none",
207628
+ details: [
207629
+ "Print the review anchor hash — the diff base an external",
207630
+ "tool (a diff, a PR tool, another agent) should point at —",
207631
+ "bare, newline-terminated, and nothing else. Writes nothing:",
207632
+ "no git, no state transition, no session identity. Before",
207633
+ "the first review round it's the process's diff base;",
207634
+ "afterward it's the most-recent review round's boundary.",
207635
+ "Refuses (exit 1) when no process is underway."
207636
+ ]
207893
207637
  }
207894
207638
  ];
207895
207639
  const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
@@ -207952,6 +207696,7 @@ const tokenize = (tail) => {
207952
207696
  jsonSeen
207953
207697
  };
207954
207698
  const eq = tok.indexOf("=");
207699
+ const alreadyPresent = present.has(row.name);
207955
207700
  present.add(row.name);
207956
207701
  if (row.name === "--json") jsonSeen = true;
207957
207702
  if (row.arity === 0) {
@@ -207963,6 +207708,25 @@ const tokenize = (tail) => {
207963
207708
  if (!byFlag.has(row.name)) byFlag.set(row.name, []);
207964
207709
  continue;
207965
207710
  }
207711
+ if (row.arity === "optional") {
207712
+ if (!row.repeatable && alreadyPresent) return {
207713
+ kind: "error",
207714
+ message: `gtd: ${row.name} may be given at most once`,
207715
+ jsonSeen
207716
+ };
207717
+ if (eq !== -1) {
207718
+ const value = tok.slice(eq + 1);
207719
+ if (value === "") return {
207720
+ kind: "error",
207721
+ message: `gtd: ${row.name} requires a value — use ${row.name}=${row.valueHint}`,
207722
+ jsonSeen
207723
+ };
207724
+ byFlag.set(row.name, [value]);
207725
+ continue;
207726
+ }
207727
+ if (!byFlag.has(row.name)) byFlag.set(row.name, []);
207728
+ continue;
207729
+ }
207966
207730
  let raw;
207967
207731
  if (eq !== -1) raw = tok.slice(eq + 1);
207968
207732
  else {
@@ -208027,6 +207791,7 @@ const decodeFlags = (byFlag) => {
208027
207791
  for (const row of FLAGS) {
208028
207792
  const raws = byFlag.get(row.name);
208029
207793
  if (raws === void 0) continue;
207794
+ if (row.arity === "optional" && raws.length === 0) continue;
208030
207795
  const decoded = row.decode(raws);
208031
207796
  if (isLeft(decoded)) return left(decoded.left);
208032
207797
  bag[row.name] = decoded.right;
@@ -208090,16 +207855,17 @@ const parseArgv = (argv) => {
208090
207855
  const decoded = decodeFlags(byFlag);
208091
207856
  if (isLeft(decoded)) return usagePlan(decoded.left, jsonSeen);
208092
207857
  const bag = decoded.right;
208093
- const json = present.has("--json");
208094
- const sh = present.has("--sh");
207858
+ const json = !present.has("--json") ? { kind: "off" } : bag["--json"] !== void 0 ? {
207859
+ kind: "select",
207860
+ path: bag["--json"]
207861
+ } : { kind: "document" };
208095
207862
  const verbose = present.has("--verbose");
208096
207863
  if (kind === "land") {
208097
- 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);
207864
+ 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", jsonSeen);
208098
207865
  return {
208099
207866
  kind: "command",
208100
207867
  command: buildLandCommand(bag),
208101
207868
  json,
208102
- sh,
208103
207869
  verbose
208104
207870
  };
208105
207871
  }
@@ -208115,7 +207881,6 @@ const parseArgv = (argv) => {
208115
207881
  label
208116
207882
  },
208117
207883
  json,
208118
- sh,
208119
207884
  verbose
208120
207885
  };
208121
207886
  }
@@ -208127,7 +207892,6 @@ const parseArgv = (argv) => {
208127
207892
  open: !(bag["--no-open"] ?? false)
208128
207893
  },
208129
207894
  json,
208130
- sh,
208131
207895
  verbose
208132
207896
  };
208133
207897
  if (kind === "check") return {
@@ -208139,14 +207903,12 @@ const parseArgv = (argv) => {
208139
207903
  ...bag["--open-questions"] !== void 0 ? { openQuestions: bag["--open-questions"] } : {}
208140
207904
  },
208141
207905
  json,
208142
- sh,
208143
207906
  verbose
208144
207907
  };
208145
207908
  return {
208146
207909
  kind: "command",
208147
207910
  command: { kind },
208148
207911
  json,
208149
- sh,
208150
207912
  verbose
208151
207913
  };
208152
207914
  };
@@ -208186,10 +207948,14 @@ const bufferedArtifactOut = (io) => {
208186
207948
  * The single envelope writer: `{state:"error",prompt}` on **stderr** under
208187
207949
  * `--json`, `renderFailure` on stderr always, exit `EXIT_RUNTIME_ERROR`.
208188
207950
  * stdout is never touched here — the command's `ArtifactOut` buffer was never
208189
- * flushed, so a `--json` driver piping stdout into `jq` on a failed run must
208190
- * read stderr or the exit code instead. `Effect.sandbox` means this also
208191
- * fires for a DEFECT, not just a typed error. Never reached for a USAGE
208192
- * error — those never build a layer at all.
207951
+ * flushed, so a `--json` driver reading stdout on a failed run reads nothing
207952
+ * — it must read stderr or the exit code instead. `Effect.sandbox` means this also
207953
+ * fires for a DEFECT, not just a typed error. Unreached by an ordinary usage
207954
+ * error (an unknown flag, bad arity, a scope violation) — those never build a
207955
+ * layer at all. The one exception is `SelectorUsageError`: an unknown
207956
+ * `--json=<path>` selector can only be judged after the layer is built and
207957
+ * the fields object it's reduced against is fully resolved, so it fails HERE
207958
+ * rather than in `parseArgv` — `EXIT_USAGE_ERROR` still applies to it below.
208193
207959
  */
208194
207960
  const report = (io, json) => (cause) => sync$1(() => {
208195
207961
  const error = squash(cause);
@@ -208201,7 +207967,7 @@ const report = (io, json) => (cause) => sync$1(() => {
208201
207967
  })}\n`);
208202
207968
  }
208203
207969
  io.stderr(`${renderFailure(error)}\n`);
208204
- io.exit(1);
207970
+ io.exit(error instanceof SelectorUsageError ? 2 : 1);
208205
207971
  });
208206
207972
  const runCli = (argv, io) => {
208207
207973
  const plan = parseArgv(argv);
@@ -208216,10 +207982,10 @@ const runCli = (argv, io) => {
208216
207982
  io.exit(2);
208217
207983
  });
208218
207984
  const out = bufferedArtifactOut(io);
208219
- return runCommand(plan.command, plan.json, plan.sh, out).pipe(map$4(() => {
207985
+ return runCommand(plan.command, plan.json, out).pipe(map$4(() => {
208220
207986
  out.flush();
208221
207987
  io.exit(0);
208222
- }), provide$2(io.layers(plan.verbose)), sandbox, catchAll((cause) => isInterruptedOnly(cause) ? failCause$2(cause) : report(io, plan.json)(cause)));
207988
+ }), provide$2(io.layers(plan.verbose)), sandbox, catchAll((cause) => isInterruptedOnly(cause) ? failCause$2(cause) : report(io, plan.json.kind !== "off")(cause)));
208223
207989
  };
208224
207990
  //#endregion
208225
207991
  //#region src/main.ts