@pmelab/gtd 3.1.0 → 3.2.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.
@@ -189117,6 +189117,10 @@ const contentKindOf = (state) => {
189117
189117
  };
189118
189118
  /** True when a state is a commit (final, squash) state. */
189119
189119
  const isCommitState = (state) => state.commit !== void 0;
189120
+ /** True when a rest at `state` should open the review checkout window (see `StateDef.reviewWindow`). Safe for an unknown state name (returns `false`). */
189121
+ const isReviewWindowState = (def, state) => def.states[state]?.reviewWindow === true;
189122
+ /** True when `state` anchors the review window's diff base (see `StateDef.reviewBase`). Safe for an unknown state name (returns `false`). */
189123
+ const isReviewBaseState = (def, state) => def.states[state]?.reviewBase === true;
189120
189124
  /**
189121
189125
  * `gtd(<actor>): <state>` — the subject a step commit carries. Per decision
189122
189126
  * 2, `<actor>` is WHO AUTHORED THE STEP (the invoker), and `<state>` is the
@@ -189405,6 +189409,19 @@ const validateMode = (name, state) => {
189405
189409
  if (isCommitState(state)) errors.push(`state "${name}": a commit state cannot declare "mode"`);
189406
189410
  return errors;
189407
189411
  };
189412
+ /**
189413
+ * `reviewWindow`/`reviewBase`, when present, are booleans (the compiler
189414
+ * enforces the type) — forbidden on a commit state, same rule family as
189415
+ * `model`/`file`/`mode`: a commit state is never at rest, so no window ever
189416
+ * opens or anchors there.
189417
+ */
189418
+ const validateReviewWindow = (name, state) => {
189419
+ if (!isCommitState(state)) return [];
189420
+ const errors = [];
189421
+ if (state.reviewWindow !== void 0) errors.push(`state "${name}": a commit state cannot declare "reviewWindow"`);
189422
+ if (state.reviewBase !== void 0) errors.push(`state "${name}": a commit state cannot declare "reviewBase"`);
189423
+ return errors;
189424
+ };
189408
189425
  /** Every `on` row parses, and its target names a defined state. */
189409
189426
  const validateOnEdges = (name, state, names) => {
189410
189427
  const errors = [];
@@ -189463,7 +189480,8 @@ const validateState = (def, name, names) => {
189463
189480
  ...validateRetry(name, state, names),
189464
189481
  ...validateModel(name, state),
189465
189482
  ...validateFile(name, state),
189466
- ...validateMode(name, state)
189483
+ ...validateMode(name, state),
189484
+ ...validateReviewWindow(name, state)
189467
189485
  ];
189468
189486
  };
189469
189487
  /**
@@ -189480,8 +189498,9 @@ const validateState = (def, name, names) => {
189480
189498
  * never declared on a commit state; `file`, when present, is a non-empty
189481
189499
  * string and is never declared on a commit state; `mode`, when present, is
189482
189500
  * one of the closed vocabulary (`qa`/`review`), requires a sibling `file`,
189483
- * and is never declared on a commit state; every state is reachable from the
189484
- * initial state by walking `on` targets and `retry.otherwise` redirects
189501
+ * and is never declared on a commit state; `reviewWindow`/`reviewBase`, when
189502
+ * present, are never declared on a commit state; every state is reachable from
189503
+ * the initial state by walking `on` targets and `retry.otherwise` redirects
189485
189504
  * (checked only when the initial-state rule itself passed — see
189486
189505
  * `validateReachability`).
189487
189506
  */
@@ -189617,7 +189636,9 @@ const KNOWN_STATE_KEYS = /* @__PURE__ */ new Set([
189617
189636
  "retry",
189618
189637
  "model",
189619
189638
  "file",
189620
- "mode"
189639
+ "mode",
189640
+ "reviewWindow",
189641
+ "reviewBase"
189621
189642
  ]);
189622
189643
  const KNOWN_TOP_KEYS = /* @__PURE__ */ new Set(["vars", "states"]);
189623
189644
  const formatErrors = (errors) => `workflow config:\n${errors.map((e) => ` - ${e}`).join("\n")}`;
@@ -189730,13 +189751,21 @@ const compileMode = (raw, name, errors) => {
189730
189751
  return raw.mode;
189731
189752
  };
189732
189753
  /** The `initial` field: `true` only when the raw value is the literal boolean `true`. */
189733
- const compileInitial = (raw, name, errors) => {
189734
- if (raw.initial === void 0) return void 0;
189735
- if (raw.initial !== true && raw.initial !== false) {
189736
- errors.push(`state "${name}": "initial" must be a boolean`);
189754
+ const compileInitial = (raw, name, errors) => compileBooleanFlag(raw, "initial", name, errors);
189755
+ /**
189756
+ * A boolean state flag (`initial`/`reviewWindow`/`reviewBase`): `true` only
189757
+ * when the raw value is the literal `true`; a non-boolean is a config error;
189758
+ * `false` (or absent) compiles away to `undefined` so it never lands in the
189759
+ * `StateDef` — `false` and "unset" mean the same thing for every such flag.
189760
+ */
189761
+ const compileBooleanFlag = (raw, key, name, errors) => {
189762
+ const value = raw[key];
189763
+ if (value === void 0) return void 0;
189764
+ if (value !== true && value !== false) {
189765
+ errors.push(`state "${name}": "${key}" must be a boolean`);
189737
189766
  return;
189738
189767
  }
189739
- return raw.initial === true ? true : void 0;
189768
+ return value === true ? true : void 0;
189740
189769
  };
189741
189770
  const assembleContentFields = (content) => ({
189742
189771
  ...content.script !== void 0 ? { script: content.script } : {},
@@ -189760,7 +189789,9 @@ const assembleStateDef = (parts) => ({
189760
189789
  retry: parts.retry,
189761
189790
  model: parts.model,
189762
189791
  file: parts.file,
189763
- mode: parts.mode
189792
+ mode: parts.mode,
189793
+ reviewWindow: parts.reviewWindow,
189794
+ reviewBase: parts.reviewBase
189764
189795
  }),
189765
189796
  ...assembleContentFields(parts.content)
189766
189797
  });
@@ -189780,7 +189811,9 @@ const compileState = (name, raw, configDir, errors) => {
189780
189811
  retry: compileRetry(raw.retry, name, errors),
189781
189812
  model: compileModel(raw, name, errors),
189782
189813
  file: compileFile(raw, name, errors),
189783
- mode: compileMode(raw, name, errors)
189814
+ mode: compileMode(raw, name, errors),
189815
+ reviewWindow: compileBooleanFlag(raw, "reviewWindow", name, errors),
189816
+ reviewBase: compileBooleanFlag(raw, "reviewBase", name, errors)
189784
189817
  });
189785
189818
  };
189786
189819
  /**
@@ -189830,7 +189863,7 @@ const compileWorkflowConfig = (raw, configDir) => {
189830
189863
  * default.yaml's content values start with `./`/`../`, so
189831
189864
  * `compileWorkflowConfig` never resolves a file reference against it.
189832
189865
  */
189833
- const compiled = compileWorkflowConfig((0, import_dist$1.parse)("# The bundled default gtd workflow (v3 \"pattern machine\" — see\n# docs/design/pattern-machine-plan.md, and\n# docs/design/steering-file-loops.md for the two validation loops below).\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/default.ts does the compiling).\n#\n# Every content string below is INLINE (no `./`-relative file references):\n# the bundled default ships inside a single-file build\n# (`dist/gtd.bundle.mjs`), so its content must already be resolved at BUILD\n# time rather than read from disk at RUNTIME — the same reason\n# `src/prompts/*.md` are imported through tsdown's `.md`-as-text loader\n# instead of read via `fs` at runtime. `default.yaml` itself is imported the\n# same way (tsdown/vitest both treat `.yaml` as text — see\n# tsdown.config.ts / tests/vitest.rawMd.ts), so this file never needs a\n# sibling prompts/ directory to exist on disk once installed.\n#\n# States, in cycle order — 12 states, no squash:\n# idle (initial, human, message)\n# -> grilling (agent, prompt): reads .gtd/TODO.md's sketch (or a prior\n# lap's feedback), explores the codebase, and develops it into a concrete\n# implementation plan, stating any open question it can't settle itself\n# under a `## Open Questions` heading (see \"Loop 1\" below) rather than\n# guessing silently.\n# grilling -> todo-validating (check, script): mechanics-only parser port\n# of the open-questions format (see src/OpenQuestions.ts's module doc for\n# the executable spec this is a bash rendition of). Malformed ->\n# .gtd/FORMAT.md -> back to grilling; valid -> grilling-answer.\n# todo-validating -> grilling-answer (human, message): tick off suggested\n# defaults by replacing them with `Answer:` lines; a clean step accepts\n# every remaining default -> building; any edit loops back to grilling.\n# grilling-answer -> building (agent, prompt): implements the plan in\n# .gtd/TODO.md directly (no task decomposition, no per-task queue) and\n# deletes .gtd/TODO.md once the work is complete and verified.\n# building -> checking (check, script): runs the test command inline;\n# A/M .gtd/FEEDBACK.md -> fixing (capped, redirects to escalate); D\n# .gtd/FEEDBACK.md (a green run that just cleaned up prior feedback) or C\n# (green, nothing to clean) -> reviewing.\n# fixing (agent, prompt) <-> checking\n# escalate (human, message): manual intervention, then back to checking\n# checking (green) -> reviewing (agent, prompt): writes .gtd/REVIEW.md in\n# the checkbox review format (see \"Loop 2\" below), grouping the cycle's\n# full diff into reviewable chunks.\n# reviewing -> review-validating (check, script): mechanics-only parser\n# port of the review format (see src/ReviewDoc.ts's module doc for the\n# executable spec). Malformed -> .gtd/FORMAT.md -> back to reviewing;\n# valid -> await-review.\n# review-validating -> await-review (human, message): tick a pointer's `- [\n# ]` to `- [x]` to approve that item; deleting .gtd/REVIEW.md outright is\n# the power-user approve-everything shortcut; anything else pending\n# (code edits with REVIEW.md untouched) is feedback straight to grilling.\n# await-review -> review-deciding (check, script), entered by any `M\n# .gtd/REVIEW.md` step: if no unticked `- [ ]` pointer remains, the cycle\n# is approved (rm .gtd/REVIEW.md -> idle); otherwise the still-unticked\n# pointers (with their notes) are extracted into a fresh .gtd/TODO.md and\n# .gtd/REVIEW.md is removed (-> grilling, feedback lap).\n#\n# The cycle ends at human approval, resting back at idle — there is no\n# squash state. The cycle's commits stay in history; whether/how to squash\n# them (amend, interactive rebase, a PR's squash-merge) is entirely the\n# human's business once they're done reviewing — gtd makes no assumption.\n# That squash-flavored finale (a `squashing` prompt state + a `done` commit\n# state) still exists as an ENGINE capability (see STATES.md §8) and is kept\n# as part of the fuller machine at docs/examples/advanced-workflow.md.\n#\n# A `gtd(<actor>): idle` commit — entering the workflow's own initial state —\n# is therefore a PROCESS BOUNDARY (see `computeProcessRun` in src/Edge.ts):\n# the next cycle's `retry` counts, `startCommit`, and diffs never reach back\n# across an approved cycle's own idle-entering commit.\n#\n# HYGIENE INVARIANT (asserted in e2e — see default-workflow.feature): an\n# approved cycle leaves `.gtd/` empty. FEEDBACK.md is cleaned up by a green\n# `checking` run; FORMAT.md by a valid validator run (either loop); REVIEW.md\n# by the review-deciding approval branch; TODO.md by `building`.\n#\n# Loop 1 — TODO.md open questions (src/OpenQuestions.ts is this format's\n# EXECUTABLE SPEC — its unit tests are the format's spec tests; the\n# `todo-validating`/`review-validating` bash scripts below are a pragmatic\n# grep/awk PORT of the exact same rules, not a full markdown parser. Keep\n# both in sync by hand — there is no shared code path between the two\n# implementations, by design: the engine stays git/filesystem/Effect-free,\n# and the LSP's parsers stay dependency-free).\n#\n# Loop 2 — REVIEW.md checkboxes: same dual-implementation contract, this time\n# against src/ReviewDoc.ts.\n#\n# `grilling` and `reviewing` both declare `model: smart` — an opaque harness\n# hint (see the comment at `grilling` below) for their heavier one-shot\n# planning/reviewing turns. Every other state is unset (harness default).\n#\n# `vars.testCommand` is this workflow's own declared default for `checking`'s\n# script (see it.vars in PatternTemplates.ts) — override it per-repo with a\n# top-level `.gtdrc` `vars: { testCommand: ... }` key, or per-invocation with\n# a `GTD_VAR_testCommand` environment variable (highest precedence).\n#\n# `vars.todoFile`/`reviewFile`/`feedbackFile` are this workflow's ONE source\n# of truth for the three steering-file paths every prompt/script below reads\n# or writes — each appears in templates as `<%~ it.vars.todoFile %>` etc\n# (see docs/design/state-file-association.md §2). Every state that rests on\n# one of these files also declares a `file:`/`mode:` pair (see STATES.md's\n# property table) so `gtd status`/`gtd next --json` and the LSP (`gtd lsp`)\n# know which file a human/editor should look at, and in what format\n# (`qa` for TODO.md's open-questions format, `review` for REVIEW.md's\n# checkbox format) — `fixing`/`escalate` declare `file:` alone (FEEDBACK.md\n# is plain text, no LSP format). KNOWN LIMITATION: `on` pattern keys below are\n# NOT Eta templates — they keep the LITERAL `.gtd/...` paths matching these\n# vars' DEFAULT values. Repointing `todoFile`/`reviewFile`/`feedbackFile` via\n# `.gtdrc`'s top-level `vars:` or a `GTD_VAR_` override changes what `file:`\n# renders to (and what a template reads/writes) WITHOUT changing what the\n# `on` patterns match against — desyncing the machine. See\n# docs/configuration.md's `file:`/`mode:` section for the full caveat.\n#\n# A fuller machine — two-phase Q&A planning, an architecture phase, task\n# decomposition, a deterministic per-task builder loop, agent-prepared\n# review, and the squash finale (`squashing` + `done`) — is preserved as a\n# copy-paste `.gtdrc` example at docs/examples/advanced-workflow.md.\n\nvars:\n testCommand: npm test\n todoFile: .gtd/TODO.md\n reviewFile: .gtd/REVIEW.md\n feedbackFile: .gtd/FEEDBACK.md\n\nstates:\n idle:\n actor: human\n initial: true\n message: |\n No active gtd cycle.\n\n To start one: write `.gtd/TODO.md` with a short sketch of what you want\n built — a few sentences is enough — then run `gtd step human`.\n on:\n \"* **\": grilling\n\n grilling:\n actor: agent\n file: <%= it.vars.todoFile %>\n mode: qa\n # `model` is an opaque harness hint — gtd never interprets this string, it\n # only passes it through to `gtd next --json`/`gtd status --json` so the\n # driving loop can map it onto whatever models its agent harness\n # provides. \"smart\" here just names the harness-chosen tier for the\n # heavier planning turn; states without a `model` use the harness's\n # default.\n model: smart\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.todoFile %>`, which this prompt tells you to develop. Never\n touch `.gtd/FORMAT.md` yourself if it exists — a validator writes it,\n and only the validator removes it once you've fixed the underlying\n problem.\n\n If `.gtd/FORMAT.md` exists, it holds format findings from your last\n draft of `<%= it.vars.todoFile %>` — fix those findings first, then\n continue below.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build (or\n feedback from a prior review lap). Read it, explore the codebase, and\n develop it into a concrete implementation plan.\n\n For every question you cannot settle yourself, add it under a\n `## Open Questions` heading, one `### <question>` sub-heading each,\n whose body's FIRST non-blank line is exactly\n `Suggested default: <your answer>` — the human can accept it as-is or\n override it. Omit the section entirely once there are no open\n questions. If `<%= it.vars.todoFile %>` already carries\n `## Open Questions` entries from a previous lap, fold any\n `Answer: <text>` line into the plan; every remaining entry's body must\n still start with either `Suggested default: ...` or `Answer: ...` —\n never anything else.\n\n Leave `<%= it.vars.todoFile %>` uncommitted and finish your turn — do\n not commit, and do not run `gtd step agent` yourself; the harness does\n that.\n on:\n \"* **\": todo-validating\n\n todo-validating:\n actor: check\n file: <%= it.vars.todoFile %>\n mode: qa\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: a pragmatic grep/awk port of\n # src/OpenQuestions.ts's parser rules (the format's executable spec) —\n # free-form prose, plus an OPTIONAL \"## Open Questions\" section; every\n # \"### \" sub-heading directly under it is one question whose body's\n # first non-blank line must start with \"Suggested default: \" or\n # \"Answer: \". A violation is one finding line (file + line + rule) in\n # .gtd/FORMAT.md; a clean parse removes any stale .gtd/FORMAT.md\n # instead. What FORMAT.md's presence/absence MEANS (another drafting\n # round vs. proceeding to the human) is decided by this state's own\n # `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n awk '\n BEGIN { in_section = 0; have_question = 0; question = \"\"; qline = 0; body_seen = 0 }\n {\n trimmed = $0\n gsub(/^[ \\t]+|[ \\t]+$/, \"\", trimmed)\n\n level = 0\n if (match(trimmed, /^#+/)) level = RLENGTH\n\n if (in_section && level >= 1 && level <= 2) {\n if (have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n in_section = 0\n have_question = 0\n }\n\n if (trimmed == \"## Open Questions\") { in_section = 1; next }\n\n if (in_section && level == 3) {\n if (have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n question = trimmed\n sub(/^### */, \"\", question)\n qline = NR\n have_question = 1\n body_seen = 0\n next\n }\n\n if (in_section && have_question && !body_seen && trimmed != \"\") {\n if (trimmed ~ /^Suggested default: *.+/ || trimmed ~ /^Answer: *.+/) {\n body_seen = 1\n } else {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n body_seen = 1\n }\n }\n }\n END {\n if (in_section && have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n }\n ' <%~ it.vars.todoFile %> > .gtd/.format-findings\n\n if [ -s .gtd/.format-findings ]; then\n mv .gtd/.format-findings .gtd/FORMAT.md\n else\n rm -f .gtd/.format-findings\n rm -f .gtd/FORMAT.md\n fi\n on:\n \"A .gtd/FORMAT.md\": grilling\n \"M .gtd/FORMAT.md\": grilling\n \"D .gtd/FORMAT.md\": grilling-answer\n \"C\": grilling-answer\n\n grilling-answer:\n actor: human\n file: <%= it.vars.todoFile %>\n mode: qa\n message: |\n `<%= it.vars.todoFile %>` holds the plan under development, with any\n open questions under `## Open Questions` — each carrying a suggested\n default.\n\n To answer a question, edit its entry in place (replace\n `Suggested default: ...` with `Answer: ...`, or add further detail),\n then run `gtd step human`.\n\n To accept all suggested defaults as-is and move on to implementation,\n run `gtd step human` with no edits.\n on:\n \"C\": building\n \"* **\": grilling\n\n building:\n actor: agent\n file: <%= it.vars.todoFile %>\n mode: qa\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create or edit anything under `.gtd/` except deleting\n `<%= it.vars.todoFile %>`, which this prompt tells you to delete once\n its work is complete.\n\n The plan to implement is: <%~ it.read(it.vars.todoFile) %>\n\n Read that plan and implement it in full. Use TDD discipline: one test,\n then the implementation that passes it, then the next — never write\n all the tests first.\n\n Delete `<%= it.vars.todoFile %>` once the work it describes is complete\n and verified. Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": checking\n\n checking:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: run the test suite and record a\n # red run's output as .gtd/FEEDBACK.md. What that output MEANS (another\n # fix round vs escalation) is decided by the workflow's own `on`/`retry`\n # rules at capture time — never here.\n #\n # `it.vars.testCommand` defaults to \"npm test\" (this workflow's own\n # `vars:` above) — override it with a top-level `.gtdrc`\n # `vars: { testCommand: ... }` key, or a `GTD_VAR_testCommand`\n # environment variable (highest precedence).\n #\n # A green run also removes any `<%= it.vars.feedbackFile %>` left over\n # from an earlier red run — there is no squash to discard it for us\n # anymore, so the script cleans it up itself before moving on to\n # review.\n set +e\n mkdir -p .gtd\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 <%~ it.vars.feedbackFile %>\n else\n rm -f .gtd/.check-output\n printf 'npm test failed with exit code %s and produced no output.' \"$code\" > <%~ it.vars.feedbackFile %>\n fi\n else\n rm -f .gtd/.check-output\n rm -f <%~ it.vars.feedbackFile %>\n fi\n on:\n \"A .gtd/FEEDBACK.md\": fixing\n \"M .gtd/FEEDBACK.md\": fixing\n \"D .gtd/FEEDBACK.md\": reviewing\n \"C\": reviewing\n\n fixing:\n actor: agent\n file: <%= it.vars.feedbackFile %>\n retry:\n max: 3\n otherwise: escalate\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.feedbackFile %>`, which this prompt tells you to address.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix\n the code so the suite passes. Keep the change focused — do not\n refactor unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue; the\n machine picks the dispute up either way.\n\n Leave everything uncommitted and finish your turn — do not commit.\n on:\n \"* **\": checking\n\n escalate:\n actor: human\n file: <%= it.vars.feedbackFile %>\n message: |\n The agent could not get the check to pass after repeated attempts.\n `<%= it.vars.feedbackFile %>` holds the last failing output.\n\n Investigate and fix it yourself (editing code and/or\n `<%= it.vars.feedbackFile %>`), then run `gtd step human` to try the\n check again.\n on:\n \"* **\": checking\n\n reviewing:\n actor: agent\n file: <%= it.vars.reviewFile %>\n mode: review\n model: smart\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewFile %>`, which this prompt tells you to write.\n Never touch `.gtd/FORMAT.md` yourself if it exists — a validator writes\n it, and only the validator removes it once you've fixed the underlying\n problem.\n\n If `.gtd/FORMAT.md` exists, it holds format findings from your last\n draft of `<%= it.vars.reviewFile %>` — fix those findings first, then\n continue below.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the cycle's\n full diff (inlined below) in this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.startCommit %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation\n of what changed and why, then one\n `- [ ] ./path/to/file.ts#42 — note` file pointer per hunk (checkboxes\n are for the human to tick, not you; every path is `./`-relative, with\n an optional `#line` and an optional ` — note`).\n\n Leave `<%= it.vars.reviewFile %>` uncommitted and finish your turn.\n <% if (it.processDiff.trim()) { %>\n\n ## Full cycle diff\n\n ```diff\n <%~ it.processDiff %>\n ```\n <% } %>\n on:\n \"* **\": review-validating\n\n review-validating:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: a pragmatic grep/awk port of\n # src/ReviewDoc.ts's parser rules (the format's executable spec) — a\n # `# Review: <hash>` header as the first non-blank line, a\n # `<!-- base: <hash> -->` comment, and at least one `##` chunk with at\n # least one `- [ ]`/`- [x]` file pointer. A violation is one finding\n # line (file + line + rule) in .gtd/FORMAT.md; a clean parse removes any\n # stale .gtd/FORMAT.md instead. What FORMAT.md's presence/absence MEANS\n # (another drafting round vs. handing off to the human) is decided by\n # this state's own `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n awk '\n BEGIN {\n first_nonblank_seen = 0\n header_ok = 0\n base_ok = 0\n chunk_open = 0\n chunk_title = \"\"\n chunk_line = 0\n chunk_files = 0\n chunk_count = 0\n }\n {\n trimmed = $0\n gsub(/^[ \\t]+|[ \\t]+$/, \"\", trimmed)\n\n if (!first_nonblank_seen && trimmed != \"\") {\n first_nonblank_seen = 1\n if (trimmed ~ /^# Review: [^ ]+$/) {\n header_ok = 1\n } else {\n printf \"<%~ it.vars.reviewFile %>:%d: first non-blank line must be \\\"# Review: <hash>\\\"\\n\", NR\n }\n }\n\n if (trimmed ~ /^<!-- *base: *[^ ]+ *-->$/) base_ok = 1\n\n if (trimmed ~ /^## [^ ]/) {\n if (chunk_open && chunk_files == 0) {\n printf \"<%~ it.vars.reviewFile %>:%d: chunk \\\"%s\\\" has no file pointers\\n\", chunk_line, chunk_title\n }\n chunk_open = 1\n chunk_title = trimmed\n sub(/^## /, \"\", chunk_title)\n chunk_line = NR\n chunk_files = 0\n chunk_count++\n next\n }\n\n if (chunk_open) {\n if (trimmed ~ /^-[ \\t]*\\[[ xX]\\][ \\t]*\\.\\/[^ \\t]+/) {\n chunk_files++\n } else if (trimmed ~ /^-[ \\t]*\\[/) {\n printf \"<%~ it.vars.reviewFile %>:%d: file pointer does not match \\\"- [ ] ./path#line — note\\\"\\n\", NR\n }\n }\n }\n END {\n if (chunk_open && chunk_files == 0) {\n printf \"<%~ it.vars.reviewFile %>:%d: chunk \\\"%s\\\" has no file pointers\\n\", chunk_line, chunk_title\n }\n if (!header_ok) {\n print \"<%~ it.vars.reviewFile %>:1: missing or malformed \\\"# Review: <hash>\\\" header as first line\"\n }\n if (!base_ok) {\n print \"<%~ it.vars.reviewFile %>:1: missing \\\"<!-- base: <hash> -->\\\" comment\"\n }\n if (chunk_count == 0) {\n print \"<%~ it.vars.reviewFile %>:1: no \\\"##\\\" chunks found\"\n }\n }\n ' <%~ it.vars.reviewFile %> > .gtd/.format-findings\n\n if [ -s .gtd/.format-findings ]; then\n mv .gtd/.format-findings .gtd/FORMAT.md\n else\n rm -f .gtd/.format-findings\n rm -f .gtd/FORMAT.md\n fi\n on:\n \"A .gtd/FORMAT.md\": reviewing\n \"M .gtd/FORMAT.md\": reviewing\n \"D .gtd/FORMAT.md\": await-review\n \"C\": await-review\n\n await-review:\n actor: human\n file: <%= it.vars.reviewFile %>\n mode: review\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the completed\n cycle — tick a pointer's `- [ ]` box to `- [x]` to approve that item;\n add notes, untick, or edit code for feedback.\n\n To approve: tick every box, then run `gtd step human`.\n\n To approve the whole cycle without ticking every box individually:\n delete `<%= it.vars.reviewFile %>` and run `gtd step human`.\n\n To request changes: edit `<%= it.vars.reviewFile %>`\n (ticking/unticking boxes, adding notes) and/or the code, then run\n `gtd step human` — any edit to `<%= it.vars.reviewFile %>` sends the\n cycle to a deterministic check that either closes the cycle out (every\n box ticked) or extracts the still-unticked items into a fresh\n `<%= it.vars.todoFile %>` and routes back through grilling with that\n feedback. Code-only edits (leaving `<%= it.vars.reviewFile %>`\n untouched) go straight back to grilling.\n on:\n \"D .gtd/REVIEW.md\": idle\n \"M .gtd/REVIEW.md\": review-deciding\n \"* **\": grilling\n\n review-deciding:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: if .gtd/REVIEW.md still has any\n # unticked \"- [ ]\" pointer, extract those pointers (with their notes)\n # into a fresh .gtd/TODO.md; either way, remove .gtd/REVIEW.md. What\n # the resulting diff MEANS (feedback lap vs. full approval) is decided\n # by this state's own `on` rules at capture time — never here. The\n # A/M .gtd/TODO.md row is declared BEFORE the D .gtd/REVIEW.md row (see\n # this workflow's `on` map below) so a feedback lap — which always also\n # deletes .gtd/REVIEW.md — is captured as feedback, not mistaken for an\n # approval.\n set +e\n mkdir -p .gtd\n if grep -qE '^[[:space:]]*-[[:space:]]*\\[[[:space:]]*\\]' <%~ it.vars.reviewFile %> 2>/dev/null; then\n {\n echo \"Feedback from review — address these before continuing:\"\n echo\n grep -E '^[[:space:]]*-[[:space:]]*\\[[[:space:]]*\\]' <%~ it.vars.reviewFile %>\n } > <%~ it.vars.todoFile %>\n fi\n rm -f <%~ it.vars.reviewFile %>\n on:\n \"A .gtd/TODO.md\": grilling\n \"M .gtd/TODO.md\": grilling\n \"D .gtd/REVIEW.md\": idle\n \"C\": await-review\n"), ".");
189866
+ const compiled = compileWorkflowConfig((0, import_dist$1.parse)("# The bundled default gtd workflow (v3 \"pattern machine\" — see\n# docs/design/pattern-machine-plan.md, and\n# docs/design/steering-file-loops.md for the two validation loops below).\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/default.ts does the compiling).\n#\n# Every content string below is INLINE (no `./`-relative file references):\n# the bundled default ships inside a single-file build\n# (`dist/gtd.bundle.mjs`), so its content must already be resolved at BUILD\n# time rather than read from disk at RUNTIME — the same reason\n# `src/prompts/*.md` are imported through tsdown's `.md`-as-text loader\n# instead of read via `fs` at runtime. `default.yaml` itself is imported the\n# same way (tsdown/vitest both treat `.yaml` as text — see\n# tsdown.config.ts / tests/vitest.rawMd.ts), so this file never needs a\n# sibling prompts/ directory to exist on disk once installed.\n#\n# States, in cycle order — 12 states, no squash:\n# idle (initial, human, message)\n# -> grilling (agent, prompt): reads .gtd/TODO.md's sketch (or a prior\n# lap's feedback), explores the codebase, and develops it into a concrete\n# implementation plan, stating any open question it can't settle itself\n# under a `## Open Questions` heading (see \"Loop 1\" below) rather than\n# guessing silently.\n# grilling -> todo-validating (check, script): mechanics-only parser port\n# of the open-questions format (see src/OpenQuestions.ts's module doc for\n# the executable spec this is a bash rendition of). Malformed ->\n# .gtd/FORMAT.md -> back to grilling; valid -> grilling-answer.\n# todo-validating -> grilling-answer (human, message): tick off suggested\n# defaults by replacing them with `Answer:` lines; a clean step accepts\n# every remaining default -> building; any edit loops back to grilling.\n# grilling-answer -> building (agent, prompt): implements the plan in\n# .gtd/TODO.md directly (no task decomposition, no per-task queue) and\n# deletes .gtd/TODO.md once the work is complete and verified.\n# building -> checking (check, script): runs the test command inline;\n# A/M .gtd/FEEDBACK.md -> fixing (capped, redirects to escalate); D\n# .gtd/FEEDBACK.md (a green run that just cleaned up prior feedback) or C\n# (green, nothing to clean) -> reviewing.\n# fixing (agent, prompt) <-> checking\n# escalate (human, message): manual intervention, then back to checking\n# checking (green) -> reviewing (agent, prompt): writes .gtd/REVIEW.md in\n# the checkbox review format (see \"Loop 2\" below), grouping the cycle's\n# full diff into reviewable chunks.\n# reviewing -> review-validating (check, script): mechanics-only parser\n# port of the review format (see src/ReviewDoc.ts's module doc for the\n# executable spec). Malformed -> .gtd/FORMAT.md -> back to reviewing;\n# valid -> await-review.\n# review-validating -> await-review (human, message): tick a pointer's `- [\n# ]` to `- [x]` to approve that item; deleting .gtd/REVIEW.md outright is\n# the power-user approve-everything shortcut; anything else pending\n# (code edits with REVIEW.md untouched) is feedback straight to grilling.\n# await-review -> review-deciding (check, script), entered by any `M\n# .gtd/REVIEW.md` step: if no unticked `- [ ]` pointer remains, the cycle\n# is approved (rm .gtd/REVIEW.md -> idle); otherwise the still-unticked\n# pointers (with their notes) are extracted into a fresh .gtd/TODO.md and\n# .gtd/REVIEW.md is removed (-> grilling, feedback lap).\n#\n# The cycle ends at human approval, resting back at idle — there is no\n# squash state. The cycle's commits stay in history; whether/how to squash\n# them (amend, interactive rebase, a PR's squash-merge) is entirely the\n# human's business once they're done reviewing — gtd makes no assumption.\n# That squash-flavored finale (a `squashing` prompt state + a `done` commit\n# state) still exists as an ENGINE capability (see STATES.md §8) and is kept\n# as part of the fuller machine at docs/examples/advanced-workflow.md.\n#\n# A `gtd(<actor>): idle` commit — entering the workflow's own initial state —\n# is therefore a PROCESS BOUNDARY (see `computeProcessRun` in src/Edge.ts):\n# the next cycle's `retry` counts, `startCommit`, and diffs never reach back\n# across an approved cycle's own idle-entering commit.\n#\n# HYGIENE INVARIANT (asserted in e2e — see default-workflow.feature): an\n# approved cycle leaves `.gtd/` empty. FEEDBACK.md is cleaned up by a green\n# `checking` run; FORMAT.md by a valid validator run (either loop); REVIEW.md\n# by the review-deciding approval branch; TODO.md by `building`.\n#\n# Loop 1 — TODO.md open questions (src/OpenQuestions.ts is this format's\n# EXECUTABLE SPEC — its unit tests are the format's spec tests; the\n# `todo-validating`/`review-validating` bash scripts below are a pragmatic\n# grep/awk PORT of the exact same rules, not a full markdown parser. Keep\n# both in sync by hand — there is no shared code path between the two\n# implementations, by design: the engine stays git/filesystem/Effect-free,\n# and the LSP's parsers stay dependency-free).\n#\n# Loop 2 — REVIEW.md checkboxes: same dual-implementation contract, this time\n# against src/ReviewDoc.ts.\n#\n# `grilling` and `reviewing` both declare `model: smart` — an opaque harness\n# hint (see the comment at `grilling` below) for their heavier one-shot\n# planning/reviewing turns. Every other state is unset (harness default).\n#\n# `vars.testCommand` is this workflow's own declared default for `checking`'s\n# script (see it.vars in PatternTemplates.ts) — override it per-repo with a\n# top-level `.gtdrc` `vars: { testCommand: ... }` key, or per-invocation with\n# a `GTD_VAR_testCommand` environment variable (highest precedence).\n#\n# `vars.todoFile`/`reviewFile`/`feedbackFile` are this workflow's ONE source\n# of truth for the three steering-file paths every prompt/script below reads\n# or writes — each appears in templates as `<%~ it.vars.todoFile %>` etc\n# (see docs/design/state-file-association.md §2). Every state that rests on\n# one of these files also declares a `file:`/`mode:` pair (see STATES.md's\n# property table) so `gtd status`/`gtd next --json` and the LSP (`gtd lsp`)\n# know which file a human/editor should look at, and in what format\n# (`qa` for TODO.md's open-questions format, `review` for REVIEW.md's\n# checkbox format) — `fixing`/`escalate` declare `file:` alone (FEEDBACK.md\n# is plain text, no LSP format). KNOWN LIMITATION: `on` pattern keys below are\n# NOT Eta templates — they keep the LITERAL `.gtd/...` paths matching these\n# vars' DEFAULT values. Repointing `todoFile`/`reviewFile`/`feedbackFile` via\n# `.gtdrc`'s top-level `vars:` or a `GTD_VAR_` override changes what `file:`\n# renders to (and what a template reads/writes) WITHOUT changing what the\n# `on` patterns match against — desyncing the machine. See\n# docs/configuration.md's `file:`/`mode:` section for the full caveat.\n#\n# A fuller machine — two-phase Q&A planning, an architecture phase, task\n# decomposition, a deterministic per-task builder loop, agent-prepared\n# review, and the squash finale (`squashing` + `done`) — is preserved as a\n# copy-paste `.gtdrc` example at docs/examples/advanced-workflow.md.\n\nvars:\n testCommand: npm test\n todoFile: .gtd/TODO.md\n reviewFile: .gtd/REVIEW.md\n feedbackFile: .gtd/FEEDBACK.md\n\nstates:\n idle:\n actor: human\n initial: true\n message: |\n No active gtd cycle.\n\n To start one: write `.gtd/TODO.md` with a short sketch of what you want\n built — a few sentences is enough — then run `gtd step human`.\n on:\n \"* **\": grilling\n\n grilling:\n actor: agent\n file: <%= it.vars.todoFile %>\n mode: qa\n # `model` is an opaque harness hint — gtd never interprets this string, it\n # only passes it through to `gtd next --json`/`gtd status --json` so the\n # driving loop can map it onto whatever models its agent harness\n # provides. \"smart\" here just names the harness-chosen tier for the\n # heavier planning turn; states without a `model` use the harness's\n # default.\n model: smart\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.todoFile %>`, which this prompt tells you to develop. Never\n touch `.gtd/FORMAT.md` yourself if it exists — a validator writes it,\n and only the validator removes it once you've fixed the underlying\n problem.\n\n If `.gtd/FORMAT.md` exists, it holds format findings from your last\n draft of `<%= it.vars.todoFile %>` — fix those findings first, then\n continue below.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build (or\n feedback from a prior review lap). Read it, explore the codebase, and\n develop it into a concrete implementation plan.\n\n For every question you cannot settle yourself, add it under a\n `## Open Questions` heading, one `### <question>` sub-heading each,\n whose body's FIRST non-blank line is exactly\n `Suggested default: <your answer>` — the human can accept it as-is or\n override it. Omit the section entirely once there are no open\n questions. If `<%= it.vars.todoFile %>` already carries\n `## Open Questions` entries from a previous lap, fold any\n `Answer: <text>` line into the plan; every remaining entry's body must\n still start with either `Suggested default: ...` or `Answer: ...` —\n never anything else.\n\n Leave `<%= it.vars.todoFile %>` uncommitted and finish your turn — do\n not commit, and do not run `gtd step agent` yourself; the harness does\n that.\n on:\n \"* **\": todo-validating\n\n todo-validating:\n actor: check\n file: <%= it.vars.todoFile %>\n mode: qa\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: a pragmatic grep/awk port of\n # src/OpenQuestions.ts's parser rules (the format's executable spec) —\n # free-form prose, plus an OPTIONAL \"## Open Questions\" section; every\n # \"### \" sub-heading directly under it is one question whose body's\n # first non-blank line must start with \"Suggested default: \" or\n # \"Answer: \". A violation is one finding line (file + line + rule) in\n # .gtd/FORMAT.md; a clean parse removes any stale .gtd/FORMAT.md\n # instead. What FORMAT.md's presence/absence MEANS (another drafting\n # round vs. proceeding to the human) is decided by this state's own\n # `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n awk '\n BEGIN { in_section = 0; have_question = 0; question = \"\"; qline = 0; body_seen = 0 }\n {\n trimmed = $0\n gsub(/^[ \\t]+|[ \\t]+$/, \"\", trimmed)\n\n level = 0\n if (match(trimmed, /^#+/)) level = RLENGTH\n\n if (in_section && level >= 1 && level <= 2) {\n if (have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n in_section = 0\n have_question = 0\n }\n\n if (trimmed == \"## Open Questions\") { in_section = 1; next }\n\n if (in_section && level == 3) {\n if (have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n question = trimmed\n sub(/^### */, \"\", question)\n qline = NR\n have_question = 1\n body_seen = 0\n next\n }\n\n if (in_section && have_question && !body_seen && trimmed != \"\") {\n if (trimmed ~ /^Suggested default: *.+/ || trimmed ~ /^Answer: *.+/) {\n body_seen = 1\n } else {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n body_seen = 1\n }\n }\n }\n END {\n if (in_section && have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n }\n ' <%~ it.vars.todoFile %> > .gtd/.format-findings\n\n if [ -s .gtd/.format-findings ]; then\n mv .gtd/.format-findings .gtd/FORMAT.md\n else\n rm -f .gtd/.format-findings\n rm -f .gtd/FORMAT.md\n fi\n on:\n \"A .gtd/FORMAT.md\": grilling\n \"M .gtd/FORMAT.md\": grilling\n \"D .gtd/FORMAT.md\": grilling-answer\n \"C\": grilling-answer\n\n grilling-answer:\n actor: human\n file: <%= it.vars.todoFile %>\n mode: qa\n message: |\n `<%= it.vars.todoFile %>` holds the plan under development, with any\n open questions under `## Open Questions` — each carrying a suggested\n default.\n\n To answer a question, edit its entry in place (replace\n `Suggested default: ...` with `Answer: ...`, or add further detail),\n then run `gtd step human`.\n\n To accept all suggested defaults as-is and move on to implementation,\n run `gtd step human` with no edits.\n on:\n \"C\": building\n \"* **\": grilling\n\n building:\n actor: agent\n file: <%= it.vars.todoFile %>\n mode: qa\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create or edit anything under `.gtd/` except deleting\n `<%= it.vars.todoFile %>`, which this prompt tells you to delete once\n its work is complete.\n\n The plan to implement is: <%~ it.read(it.vars.todoFile) %>\n\n Read that plan and implement it in full. Use TDD discipline: one test,\n then the implementation that passes it, then the next — never write\n all the tests first.\n\n Delete `<%= it.vars.todoFile %>` once the work it describes is complete\n and verified. Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": checking\n\n checking:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: run the test suite and record a\n # red run's output as .gtd/FEEDBACK.md. What that output MEANS (another\n # fix round vs escalation) is decided by the workflow's own `on`/`retry`\n # rules at capture time — never here.\n #\n # `it.vars.testCommand` defaults to \"npm test\" (this workflow's own\n # `vars:` above) — override it with a top-level `.gtdrc`\n # `vars: { testCommand: ... }` key, or a `GTD_VAR_testCommand`\n # environment variable (highest precedence).\n #\n # A green run also removes any `<%= it.vars.feedbackFile %>` left over\n # from an earlier red run — there is no squash to discard it for us\n # anymore, so the script cleans it up itself before moving on to\n # review.\n set +e\n mkdir -p .gtd\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 <%~ it.vars.feedbackFile %>\n else\n rm -f .gtd/.check-output\n printf 'npm test failed with exit code %s and produced no output.' \"$code\" > <%~ it.vars.feedbackFile %>\n fi\n else\n rm -f .gtd/.check-output\n rm -f <%~ it.vars.feedbackFile %>\n fi\n on:\n \"A .gtd/FEEDBACK.md\": fixing\n \"M .gtd/FEEDBACK.md\": fixing\n \"D .gtd/FEEDBACK.md\": reviewing\n \"C\": reviewing\n\n fixing:\n actor: agent\n file: <%= it.vars.feedbackFile %>\n retry:\n max: 3\n otherwise: escalate\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.feedbackFile %>`, which this prompt tells you to address.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix\n the code so the suite passes. Keep the change focused — do not\n refactor unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue; the\n machine picks the dispute up either way.\n\n Leave everything uncommitted and finish your turn — do not commit.\n on:\n \"* **\": checking\n\n escalate:\n actor: human\n file: <%= it.vars.feedbackFile %>\n message: |\n The agent could not get the check to pass after repeated attempts.\n `<%= it.vars.feedbackFile %>` holds the last failing output.\n\n Investigate and fix it yourself (editing code and/or\n `<%= it.vars.feedbackFile %>`), then run `gtd step human` to try the\n check again.\n on:\n \"* **\": checking\n\n reviewing:\n actor: agent\n file: <%= it.vars.reviewFile %>\n mode: review\n model: smart\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewFile %>`, which this prompt tells you to write.\n Never touch `.gtd/FORMAT.md` yourself if it exists — a validator writes\n it, and only the validator removes it once you've fixed the underlying\n problem.\n\n If `.gtd/FORMAT.md` exists, it holds format findings from your last\n draft of `<%= it.vars.reviewFile %>` — fix those findings first, then\n continue below.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the cycle's\n full diff (inlined below) in this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.startCommit %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation\n of what changed and why, then one\n `- [ ] ./path/to/file.ts#42 — note` file pointer per hunk (checkboxes\n are for the human to tick, not you; every path is `./`-relative, with\n an optional `#line` and an optional ` — note`).\n\n Leave `<%= it.vars.reviewFile %>` uncommitted and finish your turn.\n <% if (it.processDiff.trim()) { %>\n\n ## Full cycle diff\n\n ```diff\n <%~ it.processDiff %>\n ```\n <% } %>\n on:\n \"* **\": review-validating\n\n review-validating:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: a pragmatic grep/awk port of\n # src/ReviewDoc.ts's parser rules (the format's executable spec) — a\n # `# Review: <hash>` header as the first non-blank line, a\n # `<!-- base: <hash> -->` comment, and at least one `##` chunk with at\n # least one `- [ ]`/`- [x]` file pointer. A violation is one finding\n # line (file + line + rule) in .gtd/FORMAT.md; a clean parse removes any\n # stale .gtd/FORMAT.md instead. What FORMAT.md's presence/absence MEANS\n # (another drafting round vs. handing off to the human) is decided by\n # this state's own `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n awk '\n BEGIN {\n first_nonblank_seen = 0\n header_ok = 0\n base_ok = 0\n chunk_open = 0\n chunk_title = \"\"\n chunk_line = 0\n chunk_files = 0\n chunk_count = 0\n }\n {\n trimmed = $0\n gsub(/^[ \\t]+|[ \\t]+$/, \"\", trimmed)\n\n if (!first_nonblank_seen && trimmed != \"\") {\n first_nonblank_seen = 1\n if (trimmed ~ /^# Review: [^ ]+$/) {\n header_ok = 1\n } else {\n printf \"<%~ it.vars.reviewFile %>:%d: first non-blank line must be \\\"# Review: <hash>\\\"\\n\", NR\n }\n }\n\n if (trimmed ~ /^<!-- *base: *[^ ]+ *-->$/) base_ok = 1\n\n if (trimmed ~ /^## [^ ]/) {\n if (chunk_open && chunk_files == 0) {\n printf \"<%~ it.vars.reviewFile %>:%d: chunk \\\"%s\\\" has no file pointers\\n\", chunk_line, chunk_title\n }\n chunk_open = 1\n chunk_title = trimmed\n sub(/^## /, \"\", chunk_title)\n chunk_line = NR\n chunk_files = 0\n chunk_count++\n next\n }\n\n if (chunk_open) {\n if (trimmed ~ /^-[ \\t]*\\[[ xX]\\][ \\t]*\\.\\/[^ \\t]+/) {\n chunk_files++\n } else if (trimmed ~ /^-[ \\t]*\\[/) {\n printf \"<%~ it.vars.reviewFile %>:%d: file pointer does not match \\\"- [ ] ./path#line — note\\\"\\n\", NR\n }\n }\n }\n END {\n if (chunk_open && chunk_files == 0) {\n printf \"<%~ it.vars.reviewFile %>:%d: chunk \\\"%s\\\" has no file pointers\\n\", chunk_line, chunk_title\n }\n if (!header_ok) {\n print \"<%~ it.vars.reviewFile %>:1: missing or malformed \\\"# Review: <hash>\\\" header as first line\"\n }\n if (!base_ok) {\n print \"<%~ it.vars.reviewFile %>:1: missing \\\"<!-- base: <hash> -->\\\" comment\"\n }\n if (chunk_count == 0) {\n print \"<%~ it.vars.reviewFile %>:1: no \\\"##\\\" chunks found\"\n }\n }\n ' <%~ it.vars.reviewFile %> > .gtd/.format-findings\n\n if [ -s .gtd/.format-findings ]; then\n mv .gtd/.format-findings .gtd/FORMAT.md\n else\n rm -f .gtd/.format-findings\n rm -f .gtd/FORMAT.md\n fi\n on:\n \"A .gtd/FORMAT.md\": reviewing\n \"M .gtd/FORMAT.md\": reviewing\n \"D .gtd/FORMAT.md\": await-review\n \"C\": await-review\n\n await-review:\n actor: human\n file: <%= it.vars.reviewFile %>\n mode: review\n # While the cycle rests here for human review, gtd opens a \"review checkout\n # window\": HEAD and the index are temporarily rewound to this cycle's start\n # (the process boundary — no `reviewBase` state is declared, so the base\n # defaults to it) with the working tree untouched, so the whole cycle diff\n # surfaces as ordinary uncommitted changes in the editor's git integration.\n # The window closes automatically on the next invocation, once the cycle\n # rests anywhere else (see src/ReviewWindow.ts, STATES.md §11).\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the completed\n cycle — tick a pointer's `- [ ]` box to `- [x]` to approve that item;\n add notes, untick, or edit code for feedback.\n\n To approve: tick every box, then run `gtd step human`.\n\n To approve the whole cycle without ticking every box individually:\n delete `<%= it.vars.reviewFile %>` and run `gtd step human`.\n\n To request changes: edit `<%= it.vars.reviewFile %>`\n (ticking/unticking boxes, adding notes) and/or the code, then run\n `gtd step human` — any edit to `<%= it.vars.reviewFile %>` sends the\n cycle to a deterministic check that either closes the cycle out (every\n box ticked) or extracts the still-unticked items into a fresh\n `<%= it.vars.todoFile %>` and routes back through grilling with that\n feedback. Code-only edits (leaving `<%= it.vars.reviewFile %>`\n untouched) go straight back to grilling.\n on:\n \"D .gtd/REVIEW.md\": idle\n \"M .gtd/REVIEW.md\": review-deciding\n \"* **\": grilling\n\n review-deciding:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: if .gtd/REVIEW.md still has any\n # unticked \"- [ ]\" pointer, extract those pointers (with their notes)\n # into a fresh .gtd/TODO.md; either way, remove .gtd/REVIEW.md. What\n # the resulting diff MEANS (feedback lap vs. full approval) is decided\n # by this state's own `on` rules at capture time — never here. The\n # A/M .gtd/TODO.md row is declared BEFORE the D .gtd/REVIEW.md row (see\n # this workflow's `on` map below) so a feedback lap — which always also\n # deletes .gtd/REVIEW.md — is captured as feedback, not mistaken for an\n # approval.\n set +e\n mkdir -p .gtd\n if grep -qE '^[[:space:]]*-[[:space:]]*\\[[[:space:]]*\\]' <%~ it.vars.reviewFile %> 2>/dev/null; then\n {\n echo \"Feedback from review — address these before continuing:\"\n echo\n grep -E '^[[:space:]]*-[[:space:]]*\\[[[:space:]]*\\]' <%~ it.vars.reviewFile %>\n } > <%~ it.vars.todoFile %>\n fi\n rm -f <%~ it.vars.reviewFile %>\n on:\n \"A .gtd/TODO.md\": grilling\n \"M .gtd/TODO.md\": grilling\n \"D .gtd/REVIEW.md\": idle\n \"C\": await-review\n"), ".");
189834
189867
  const defaultWorkflowDefinition = compiled.definition;
189835
189868
  /** The default workflow's own declared `vars:` defaults — `{ testCommand: "npm test" }`, overridable via a top-level `.gtdrc` `vars:` key or a `GTD_VAR_testCommand` environment variable (see `src/Edge.ts`'s `resolveVars`). */
189836
189869
  const defaultWorkflowVars = compiled.vars;
@@ -189959,6 +189992,14 @@ const workflowJsonSchema = {
189959
189992
  type: "string",
189960
189993
  enum: ["qa", "review"],
189961
189994
  description: "The steering file's format, dispatched on by the LSP. Requires a sibling `file:`. Forbidden on a commit state."
189995
+ },
189996
+ reviewWindow: {
189997
+ type: "boolean",
189998
+ description: "When true, gtd opens a review checkout window while the machine rests here — HEAD/index are rewound to the review base so the whole base..HEAD diff surfaces as uncommitted changes in the editor. Forbidden on a commit state."
189999
+ },
190000
+ reviewBase: {
190001
+ type: "boolean",
190002
+ description: "Marks the state whose most-recent in-process commit anchors the review window's diff base; absent any, the base is the process start. Forbidden on a commit state."
189962
190003
  }
189963
190004
  }
189964
190005
  }
@@ -190881,7 +190922,7 @@ const renderDiff = (files) => {
190881
190922
  };
190882
190923
  //#endregion
190883
190924
  //#region src/Git.ts
190884
- const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
190925
+ const EMPTY_TREE$2 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
190885
190926
  /**
190886
190927
  * Parse `git diff --name-status` output into `{ path, status }` pairs.
190887
190928
  * Status codes: A (added), D (deleted), M (modified), R (renamed), C (copied), etc.
@@ -191011,7 +191052,7 @@ const makeGitImpl = (executor, root) => {
191011
191052
  }),
191012
191053
  commitDiff: (hash, exclude = []) => gen(function* () {
191013
191054
  yield* exec("git", "rev-parse", "--verify", hash);
191014
- const parent = yield* exec("git", "rev-parse", "--verify", `${hash}~1`).pipe(map$4((s) => s.trim()), catchAll(() => succeed$3(EMPTY_TREE$1)));
191055
+ const parent = yield* exec("git", "rev-parse", "--verify", `${hash}~1`).pipe(map$4((s) => s.trim()), catchAll(() => succeed$3(EMPTY_TREE$2)));
191015
191056
  const nameStatusOut = yield* exec("git", "diff", "--name-status", parent, hash).pipe(catchAll(() => succeed$3("")));
191016
191057
  const allPaths = parseNameStatus(nameStatusOut);
191017
191058
  const filtered = applyExcludes(allPaths, exclude);
@@ -191055,6 +191096,8 @@ const makeGitImpl = (executor, root) => {
191055
191096
  return all;
191056
191097
  }),
191057
191098
  resolveRef: (ref) => exec("git", "rev-parse", "--verify", ref).pipe(map$4((s) => s.trim()), flatMap$4((hash) => /^[0-9a-f]{40}$/.test(hash) ? succeed$3(hash) : fail$2(/* @__PURE__ */ new Error(`Invalid ref: ${ref}`)))),
191099
+ readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
191100
+ isAncestor: (a, b) => run(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$1(succeed$2(CommandExecutor, executor)), map$4(() => true), catchAll(() => succeed$3(false))),
191058
191101
  topLevel: () => exec("git", "rev-parse", "--show-toplevel").pipe(map$4((s) => s.trim())),
191059
191102
  commitHistory: (base) => {
191060
191103
  const range = base !== void 0 ? `${base}..HEAD` : void 0;
@@ -191089,7 +191132,12 @@ const makeGitImpl = (executor, root) => {
191089
191132
  discardPending: () => gen(function* () {
191090
191133
  yield* exec("git", "add", "-A");
191091
191134
  yield* exec("git", "reset", "--hard", "HEAD");
191092
- }).pipe(asVoid)
191135
+ }).pipe(asVoid),
191136
+ updateRef: (ref, hash) => exec("git", "update-ref", ref, hash).pipe(asVoid),
191137
+ deleteRef: (ref) => exec("git", "update-ref", "-d", ref).pipe(asVoid),
191138
+ mixedResetTo: (ref) => exec("git", "reset", "--mixed", ref).pipe(asVoid),
191139
+ restoreStagedFrom: (source, paths) => paths.length === 0 ? _void : exec("git", "restore", "--staged", `--source=${source}`, "--", ...paths).pipe(catchAll(() => _void)),
191140
+ addIntentToAdd: () => exec("git", "add", "--intent-to-add", ".").pipe(asVoid)
191093
191141
  };
191094
191142
  };
191095
191143
  const makeLiveEffect = gen(function* () {
@@ -191639,8 +191687,8 @@ const renderStateTemplate = (template, context) => eta.renderString(template, co
191639
191687
  * one commit (or one squash). A caller that wants several transitions issues
191640
191688
  * several invocations.
191641
191689
  */
191642
- const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
191643
- const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
191690
+ const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
191691
+ const subjectOf$1 = (message) => (message.split("\n")[0] ?? "").trim();
191644
191692
  /** Collapse an arbitrary git status letter to the pattern grammar's closed `A|M|D` set (mirrors the plan's decision 5 — only those three are meaningful statuses). */
191645
191693
  const normalizeStatus = (raw) => raw === "A" ? "A" : raw === "D" ? "D" : "M";
191646
191694
  /** Resolve HEAD's subject against the active workflow definition (the bundled default, or a compiled `.gtdrc` `workflow:` key). */
@@ -191684,20 +191732,20 @@ const pendingChanges = (git) => git.changedPaths().pipe(map$4((entries) => entri
191684
191732
  const computeProcessRun = (git, def) => gen(function* () {
191685
191733
  if (!(yield* git.hasCommits())) return {
191686
191734
  startHash: "",
191687
- startParentHash: EMPTY_TREE,
191735
+ startParentHash: EMPTY_TREE$1,
191688
191736
  trace: []
191689
191737
  };
191690
191738
  const initialState = initialStateOf(def);
191691
191739
  const history = yield* git.commitHistory();
191692
191740
  let i = history.length - 1;
191693
191741
  while (i >= 0) {
191694
- const parsed = parseStateSubject(subjectOf(history[i].message));
191742
+ const parsed = parseStateSubject(subjectOf$1(history[i].message));
191695
191743
  if (parsed === void 0 || parsed.state === initialState) break;
191696
191744
  i--;
191697
191745
  }
191698
191746
  const startIdx = i + 1;
191699
- const trace = history.slice(startIdx).map((h) => parseStateSubject(subjectOf(h.message)).state);
191700
- const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE;
191747
+ const trace = history.slice(startIdx).map((h) => parseStateSubject(subjectOf$1(h.message)).state);
191748
+ const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$1;
191701
191749
  return {
191702
191750
  startHash: startIdx < history.length ? history[startIdx].hash : history[history.length - 1].hash,
191703
191751
  startParentHash,
@@ -191821,7 +191869,7 @@ const executeDecision = (git, run, decision, context) => gen(function* () {
191821
191869
  yield* git.discardPending();
191822
191870
  return {
191823
191871
  kind: "squash",
191824
- subject: subjectOf(message)
191872
+ subject: subjectOf$1(message)
191825
191873
  };
191826
191874
  }
191827
191875
  case "noop": return {
@@ -191831,6 +191879,120 @@ const executeDecision = (git, run, decision, context) => gen(function* () {
191831
191879
  }
191832
191880
  });
191833
191881
  //#endregion
191882
+ //#region src/ReviewWindow.ts
191883
+ /**
191884
+ * The review checkout window (v3 re-introduction — see
191885
+ * `docs/design/pattern-machine-plan.md`'s follow-up note and STATES.md §11).
191886
+ * While a process RESTS at a state declaring `reviewWindow: true` (the
191887
+ * bundled default's `await-review`), HEAD and the index are temporarily
191888
+ * rewound to the review base with the working tree untouched, so the entire
191889
+ * `base..HEAD` diff surfaces as ordinary uncommitted changes in any editor's
191890
+ * standard git integration (SCM panel, gutters, per-file diffs, discard-hunk).
191891
+ *
191892
+ * v2 hard-wired this to a single `awaiting-review` gate; v3 drives it purely
191893
+ * from the DECLARATIVE `reviewWindow`/`reviewBase` state properties (see
191894
+ * `PatternMachine.StateDef`) — the pure engine never observes an open window,
191895
+ * exactly as before.
191896
+ *
191897
+ * Lifecycle — driven from the program edge (`src/program.ts`), bracketing
191898
+ * every state subcommand:
191899
+ *
191900
+ * - `closeReviewWindow` runs BEFORE anything reads or mutates state, keyed
191901
+ * solely on `refs/gtd/review-head` existing: `git reset --mixed
191902
+ * refs/gtd/review-head` restores HEAD/index exactly, leaving only the
191903
+ * reviewer's own edits dirty (captured by the resting state's own `on`
191904
+ * patterns like any other pending change). The pure machine therefore never
191905
+ * sees the window.
191906
+ * - `openReviewWindow` runs AFTER the subcommand finishes and self-guards on
191907
+ * the resolved rest declaring `reviewWindow: true`: it saves HEAD to
191908
+ * `refs/gtd/review-head` (the base to `refs/gtd/review-base`), then `git
191909
+ * reset --mixed <base>`. It re-arms after read-only commands (`gtd next` /
191910
+ * `gtd status`) and refused invocations too, so the editor's diff view stays
191911
+ * consistent no matter which command the loop last ran.
191912
+ *
191913
+ * Every open/close step is idempotent under re-entry, so a crash at any point
191914
+ * is recovered by the next invocation's close (the saved ref also keeps the
191915
+ * real head GC-reachable for the window's whole lifetime).
191916
+ */
191917
+ const REVIEW_HEAD_REF = "refs/gtd/review-head";
191918
+ const REVIEW_BASE_REF = "refs/gtd/review-base";
191919
+ const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
191920
+ const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
191921
+ /**
191922
+ * The diff base while the window is open: the hash of the most-recent
191923
+ * in-process turn commit that ENTERED a `reviewBase` state, or `undefined`
191924
+ * when the workflow declares no such state (the caller then falls back to the
191925
+ * process start). Walks the current process run's commits (oldest→newest, via
191926
+ * `commitHistory(startParentHash)`), so it never reaches across the process
191927
+ * boundary into a previous cycle.
191928
+ */
191929
+ const reviewBaseHash = (git, def, run) => gen(function* () {
191930
+ const history = yield* git.commitHistory(run.startParentHash);
191931
+ let base;
191932
+ for (const commit of history) {
191933
+ const parsed = parseStateSubject(subjectOf(commit.message));
191934
+ if (parsed !== void 0 && isReviewBaseState(def, parsed.state)) base = commit.hash;
191935
+ }
191936
+ return base;
191937
+ });
191938
+ /**
191939
+ * Restore the real head if a review checkout window is open; no-op otherwise.
191940
+ * Keyed on `refs/gtd/review-head` existence, NOT on any config or machine
191941
+ * state, so it always runs first and the machine never sees the window.
191942
+ *
191943
+ * Fails loudly — refs left in place — when HEAD is no longer on the reviewed
191944
+ * branch (the base is not an ancestor of HEAD, e.g. after a branch switch): a
191945
+ * mixed reset there would rewrite the wrong branch's tip. Manual commits on
191946
+ * top of the base pass the guard: the reset keeps their content in the working
191947
+ * tree, where the next turn's capture picks it up as review feedback.
191948
+ */
191949
+ const closeReviewWindow = gen(function* () {
191950
+ const git = yield* GitService;
191951
+ const savedHead = yield* git.readRefOption(REVIEW_HEAD_REF);
191952
+ if (isNone(savedHead)) return { closed: false };
191953
+ const base = yield* git.readRefOption(REVIEW_BASE_REF);
191954
+ if (isSome(base) && base.value !== EMPTY_TREE) {
191955
+ 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 `git reset --mixed refs/gtd/review-head && git update-ref -d refs/gtd/review-head && git update-ref -d refs/gtd/review-base`"));
191956
+ }
191957
+ yield* git.mixedResetTo(REVIEW_HEAD_REF);
191958
+ yield* git.deleteRef(REVIEW_HEAD_REF);
191959
+ yield* git.deleteRef(REVIEW_BASE_REF);
191960
+ return { closed: true };
191961
+ });
191962
+ /**
191963
+ * Open (or re-arm) the review checkout window. Self-guarded: a no-op unless
191964
+ * the resolved rest's state declares `reviewWindow: true` and a distinct base
191965
+ * commit exists, so the caller invokes it unconditionally after every state
191966
+ * subcommand.
191967
+ *
191968
+ * The base is the most-recent in-process `reviewBase` commit
191969
+ * (`reviewBaseHash`) or, absent any such state, the process start
191970
+ * (`startParentHash`). When that resolves to the empty tree (a process with no
191971
+ * prior commit) or to HEAD itself (an empty process), there is nothing to
191972
+ * surface and the window stays closed.
191973
+ *
191974
+ * Ordering is crash-safe: base ref → head ref → mixed reset → `.gtd/` index
191975
+ * pin → intent-to-add. A crash before the head-ref write leaves only a stale
191976
+ * base ref (overwritten on the next open); a crash after it leaves HEAD ==
191977
+ * review-head, which the next invocation's close restores as a no-op.
191978
+ */
191979
+ const openReviewWindow = gen(function* () {
191980
+ const git = yield* GitService;
191981
+ const def = (yield* ConfigService).workflow;
191982
+ if (!(yield* git.hasCommits())) return { opened: false };
191983
+ if (!isReviewWindowState(def, resolveState(def, yield* git.lastCommitSubject()))) return { opened: false };
191984
+ const run = yield* computeProcessRun(git, def);
191985
+ const base = (yield* reviewBaseHash(git, def, run)) ?? run.startParentHash;
191986
+ const headHash = yield* git.resolveRef("HEAD");
191987
+ if (base === EMPTY_TREE || base === headHash) return { opened: false };
191988
+ yield* git.updateRef(REVIEW_BASE_REF, base);
191989
+ yield* git.updateRef(REVIEW_HEAD_REF, headHash);
191990
+ yield* git.mixedResetTo(base);
191991
+ yield* git.restoreStagedFrom(REVIEW_HEAD_REF, [".gtd"]);
191992
+ yield* git.addIntentToAdd();
191993
+ return { opened: true };
191994
+ });
191995
+ //#endregion
191834
191996
  //#region node_modules/prettier/doc.mjs
191835
191997
  var doc_exports = /* @__PURE__ */ __exportAll({
191836
191998
  builders: () => builders,
@@ -365034,8 +365196,12 @@ function makeProgram(opts = {}) {
365034
365196
  const git = yield* GitService;
365035
365197
  const fs = yield* FileSystem;
365036
365198
  yield* assertRunningFromRepoRoot(git, fs);
365199
+ yield* closeReviewWindow;
365037
365200
  yield* (yield* ConfigInit).ensure;
365038
- yield* dispatchKnownSubcommand(sub, argv, json, write);
365201
+ const outcome = yield* either(dispatchKnownSubcommand(sub, argv, json, write));
365202
+ const rearm = yield* either(openReviewWindow);
365203
+ if (isLeft(outcome)) return yield* fail$2(outcome.left);
365204
+ if (isLeft(rearm)) return yield* fail$2(rearm.left);
365039
365205
  }).pipe(json ? catchAll((error) => sync$1(() => write(JSON.stringify({
365040
365206
  state: "error",
365041
365207
  prompt: error.message
@@ -172,6 +172,49 @@ change to that path MEANS keeps matching the old literal path. The vars are a
172
172
  DRY mechanism inside templates and the state↔file association, not a rename
173
173
  switch. (Making pattern keys var-aware at compile time is possible future work.)
174
174
 
175
+ ### `reviewWindow:`/`reviewBase:` — the review checkout window
176
+
177
+ A state may declare `reviewWindow: true`. While the machine RESTS there, gtd
178
+ opens a **review checkout window**: it rewinds HEAD and the index to the review
179
+ base (`git reset --mixed`) with the working tree untouched, so the whole
180
+ `base..HEAD` diff surfaces as ordinary uncommitted changes in the editor's git
181
+ integration (SCM panel, gutters, per-file diff, discard-hunk). The window closes
182
+ automatically on the next invocation, once the machine rests anywhere else — and
183
+ a reviewer's own edits, made while it was open, become the resting state's
184
+ ordinary pending changes, captured by its `on` patterns like any other diff.
185
+ Forbidden on a commit state (never at rest).
186
+
187
+ The diff base defaults to the current process's start. To narrow it, mark an
188
+ earlier state `reviewBase: true`: the most-recent in-process commit that entered
189
+ such a state becomes the base, so only work committed after that milestone
190
+ surfaces.
191
+
192
+ ```yaml
193
+ workflow:
194
+ states:
195
+ # …
196
+ building:
197
+ actor: agent
198
+ reviewBase: true # the diff shown at review starts here…
199
+ prompt: implement the plan
200
+ on:
201
+ "* **": review
202
+ review:
203
+ actor: human
204
+ reviewWindow: true # …and is surfaced while resting here
205
+ message: review the diff in your editor, then `gtd step human`
206
+ on:
207
+ "C": done
208
+ "* **": building
209
+ ```
210
+
211
+ The bundled default enables `reviewWindow: true` on `await-review` (no
212
+ `reviewBase` state, so the base is the whole cycle). The pure engine never
213
+ observes an open window — it is opened/closed entirely at the edge; the real
214
+ head is preserved under `refs/gtd/review-head` (the base under
215
+ `refs/gtd/review-base`) for the window's lifetime. See
216
+ [STATES.md §11](../STATES.md) for the full lifecycle.
217
+
175
218
  ### Template variables
176
219
 
177
220
  Every `script`/`prompt`/`message`/`commit`/`model` template is rendered as an
package/docs/upgrading.md CHANGED
@@ -49,8 +49,14 @@ upgrading.
49
49
  tiering — a workflow author is free to build any of that shape back with
50
50
  states of their own, but gtd no longer bakes it in.
51
51
  - **The review checkout window.** v2 rewound HEAD/index to the review base while
52
- a human review rested, so editors would show the diff directly. Deleted in v3
53
- (may return later as an explicit state property — not in scope now).
52
+ a human review rested, so editors would show the diff directly. Dropped in the
53
+ v3.0 rewrite, then RE-INTRODUCED as the explicit state property it was always
54
+ meant to become: a state declaring `reviewWindow: true` opens the window while
55
+ the machine rests there (base = the process start, or a state marked
56
+ `reviewBase: true`). The bundled default enables it on `await-review`. Unlike
57
+ v2's hardwired gate, it is pure workflow DATA and the engine stays oblivious —
58
+ see [STATES.md §11](../STATES.md) and the `file:`/`mode:` neighbours in
59
+ [configuration.md](configuration.md).
54
60
  - **`forceApprove`, content-inspection verdicts.** FEEDBACK.md emptiness,
55
61
  checkbox-only REVIEW.md diffs, and doc-structure validation are gone as ENGINE
56
62
  mechanisms — verdicts are now expressed purely by which file a turn writes or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
package/schema.json CHANGED
@@ -85,6 +85,14 @@
85
85
  "type": "string",
86
86
  "enum": ["qa", "review"],
87
87
  "description": "The steering file's format, dispatched on by the LSP. Requires a sibling `file:`. Forbidden on a commit state."
88
+ },
89
+ "reviewWindow": {
90
+ "type": "boolean",
91
+ "description": "When true, gtd opens a review checkout window while the machine rests here — HEAD/index are rewound to the review base so the whole base..HEAD diff surfaces as uncommitted changes in the editor. Forbidden on a commit state."
92
+ },
93
+ "reviewBase": {
94
+ "type": "boolean",
95
+ "description": "Marks the state whose most-recent in-process commit anchors the review window's diff base; absent any, the base is the process start. Forbidden on a commit state."
88
96
  }
89
97
  }
90
98
  }