@pmelab/gtd 3.0.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 = [];
@@ -189422,6 +189439,37 @@ const validateRetry = (name, state, names) => {
189422
189439
  if (!Number.isInteger(state.retry.max) || state.retry.max < 0) errors.push(`state "${name}": retry.max must be a non-negative integer`);
189423
189440
  return errors;
189424
189441
  };
189442
+ /**
189443
+ * Every state is reachable from the initial state by walking `on` targets and
189444
+ * `retry.otherwise` redirects (a redirect ENTERS its `otherwise` state exactly
189445
+ * like an `on` match enters its target — see `applyRetry` — so both are real
189446
+ * edges). Plain BFS; targets naming undefined states are skipped here (they
189447
+ * are `validateOnEdges`/`validateRetry` findings of their own). Only called
189448
+ * when `validateInitial` found no problem: with zero or several `initial`
189449
+ * states there is no well-defined start to walk from, and reporting every
189450
+ * state as unreachable would bury the real finding.
189451
+ *
189452
+ * An unreachable state is an ERROR, not a warning: a workflow is bound to a
189453
+ * project and edited as a project-wide change, so "kept on purpose for manual
189454
+ * entry via a hand-authored subject" is not a supported authoring pattern —
189455
+ * an unreachable state is a typo'd rename or a leftover, and silently-dead
189456
+ * config is exactly what load-time validation exists to catch.
189457
+ */
189458
+ const validateReachability = (def, names) => {
189459
+ const initial = names.find((name) => def.states[name].initial === true);
189460
+ const visited = /* @__PURE__ */ new Set([initial]);
189461
+ const queue = [initial];
189462
+ while (queue.length > 0) {
189463
+ const state = def.states[queue.shift()];
189464
+ const targets = (state.on ?? []).map(([, target]) => target);
189465
+ if (state.retry !== void 0) targets.push(state.retry.otherwise);
189466
+ for (const target of targets) if (def.states[target] !== void 0 && !visited.has(target)) {
189467
+ visited.add(target);
189468
+ queue.push(target);
189469
+ }
189470
+ }
189471
+ return names.filter((name) => !visited.has(name)).map((name) => `state "${name}" is unreachable from initial state "${initial}" (no "on" target or "retry.otherwise" leads to it)`);
189472
+ };
189425
189473
  /** All per-state rule checkers, run over one state. */
189426
189474
  const validateState = (def, name, names) => {
189427
189475
  const state = def.states[name];
@@ -189432,7 +189480,8 @@ const validateState = (def, name, names) => {
189432
189480
  ...validateRetry(name, state, names),
189433
189481
  ...validateModel(name, state),
189434
189482
  ...validateFile(name, state),
189435
- ...validateMode(name, state)
189483
+ ...validateMode(name, state),
189484
+ ...validateReviewWindow(name, state)
189436
189485
  ];
189437
189486
  };
189438
189487
  /**
@@ -189449,12 +189498,21 @@ const validateState = (def, name, names) => {
189449
189498
  * never declared on a commit state; `file`, when present, is a non-empty
189450
189499
  * string and is never declared on a commit state; `mode`, when present, is
189451
189500
  * one of the closed vocabulary (`qa`/`review`), requires a sibling `file`,
189452
- * and is never declared on a commit state.
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
189504
+ * (checked only when the initial-state rule itself passed — see
189505
+ * `validateReachability`).
189453
189506
  */
189454
189507
  const validateDefinition = (def) => {
189455
189508
  const names = Object.keys(def.states);
189456
189509
  if (names.length === 0) return ["workflow must declare at least one state"];
189457
- return [...validateInitial(def, names), ...names.flatMap((name) => validateState(def, name, names))];
189510
+ const initialErrors = validateInitial(def, names);
189511
+ return [
189512
+ ...initialErrors,
189513
+ ...names.flatMap((name) => validateState(def, name, names)),
189514
+ ...initialErrors.length === 0 ? validateReachability(def, names) : []
189515
+ ];
189458
189516
  };
189459
189517
  //#endregion
189460
189518
  //#region src/PatternConfig.ts
@@ -189578,7 +189636,9 @@ const KNOWN_STATE_KEYS = /* @__PURE__ */ new Set([
189578
189636
  "retry",
189579
189637
  "model",
189580
189638
  "file",
189581
- "mode"
189639
+ "mode",
189640
+ "reviewWindow",
189641
+ "reviewBase"
189582
189642
  ]);
189583
189643
  const KNOWN_TOP_KEYS = /* @__PURE__ */ new Set(["vars", "states"]);
189584
189644
  const formatErrors = (errors) => `workflow config:\n${errors.map((e) => ` - ${e}`).join("\n")}`;
@@ -189691,13 +189751,21 @@ const compileMode = (raw, name, errors) => {
189691
189751
  return raw.mode;
189692
189752
  };
189693
189753
  /** The `initial` field: `true` only when the raw value is the literal boolean `true`. */
189694
- const compileInitial = (raw, name, errors) => {
189695
- if (raw.initial === void 0) return void 0;
189696
- if (raw.initial !== true && raw.initial !== false) {
189697
- 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`);
189698
189766
  return;
189699
189767
  }
189700
- return raw.initial === true ? true : void 0;
189768
+ return value === true ? true : void 0;
189701
189769
  };
189702
189770
  const assembleContentFields = (content) => ({
189703
189771
  ...content.script !== void 0 ? { script: content.script } : {},
@@ -189721,7 +189789,9 @@ const assembleStateDef = (parts) => ({
189721
189789
  retry: parts.retry,
189722
189790
  model: parts.model,
189723
189791
  file: parts.file,
189724
- mode: parts.mode
189792
+ mode: parts.mode,
189793
+ reviewWindow: parts.reviewWindow,
189794
+ reviewBase: parts.reviewBase
189725
189795
  }),
189726
189796
  ...assembleContentFields(parts.content)
189727
189797
  });
@@ -189741,7 +189811,9 @@ const compileState = (name, raw, configDir, errors) => {
189741
189811
  retry: compileRetry(raw.retry, name, errors),
189742
189812
  model: compileModel(raw, name, errors),
189743
189813
  file: compileFile(raw, name, errors),
189744
- 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)
189745
189817
  });
189746
189818
  };
189747
189819
  /**
@@ -189791,7 +189863,7 @@ const compileWorkflowConfig = (raw, configDir) => {
189791
189863
  * default.yaml's content values start with `./`/`../`, so
189792
189864
  * `compileWorkflowConfig` never resolves a file reference against it.
189793
189865
  */
189794
- 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"), ".");
189795
189867
  const defaultWorkflowDefinition = compiled.definition;
189796
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`). */
189797
189869
  const defaultWorkflowVars = compiled.vars;
@@ -189811,6 +189883,22 @@ var Cwd = class Cwd extends Tag("Cwd")() {
189811
189883
  * `./Edge.js`'s `resolveVars`). There are no other blessed config keys (see
189812
189884
  * `./Config.js`'s module docstring for why).
189813
189885
  *
189886
+ * Both keys decode as `Schema.Unknown`: the shape is validated structurally
189887
+ * by the workflow compiler (`src/PatternConfig.ts`), not by effect/schema —
189888
+ * the shape is deep and recursive, and the compiler's errors carry rule
189889
+ * coordinates a flat schema error cannot. The `jsonSchema` ANNOTATIONS below
189890
+ * exist for one consumer only: `scripts/generate-schema.ts`, which publishes
189891
+ * `schema.json` for editor-side autocompletion/validation
189892
+ * (yaml-language-server et al). They describe the same shape the compiler
189893
+ * enforces, minus the rules JSON Schema cannot express (exactly one content
189894
+ * kind per state, exactly one `initial: true` across the workflow, `on`/
189895
+ * `retry.otherwise` targets naming defined states, reachability) — the
189896
+ * compiler stays the source of truth; the annotation is the editor's first
189897
+ * net, never a second validator to keep behaviorally in sync. When the
189898
+ * compiler's accepted shape changes (a new state key, a new content kind),
189899
+ * update the annotation here alongside `src/PatternConfig.ts`'s
189900
+ * `KNOWN_STATE_KEYS`.
189901
+ *
189814
189902
  * Kept in its own module, separate from `./Config.js`, so `scripts/generate-
189815
189903
  * schema.ts` (run via `jiti`, a plain TS-via-Babel loader with no bundler-
189816
189904
  * style pluggable per-extension loaders) can import JUST the schema without
@@ -189819,9 +189907,108 @@ var Cwd = class Cwd extends Tag("Cwd")() {
189819
189907
  * loader, something `jiti` has no equivalent for and doesn't need here: the
189820
189908
  * schema shape never depends on the bundled default workflow's content.
189821
189909
  */
189910
+ /** The `vars:` shape (top-level AND inside `workflow:`): a flat name -> scalar map (`compileVarsMap` coerces every scalar to a string). */
189911
+ const varsJsonSchema = {
189912
+ type: "object",
189913
+ description: "Flat name -> scalar map merged into every template's it.vars. Scalars are coerced to strings.",
189914
+ additionalProperties: { type: [
189915
+ "string",
189916
+ "number",
189917
+ "boolean"
189918
+ ] }
189919
+ };
189920
+ /** The whole `workflow:` value — see `PatternConfig.ts`'s module docstring for the authoritative schema. */
189921
+ const workflowJsonSchema = {
189922
+ type: "object",
189923
+ description: "The whole machine definition: named states (plus the workflow's own vars: defaults). Compiled and validated by gtd at load time; content strings starting with ./ or ../ are file references inlined from the config file's directory.",
189924
+ additionalProperties: false,
189925
+ required: ["states"],
189926
+ properties: {
189927
+ vars: varsJsonSchema,
189928
+ states: {
189929
+ type: "object",
189930
+ description: "The workflow's named states. At least one; exactly one with initial: true.",
189931
+ minProperties: 1,
189932
+ additionalProperties: {
189933
+ type: "object",
189934
+ description: "One workflow state. Declare exactly one content kind (script/prompt/message/commit). A commit state is final: no actor, no on.",
189935
+ additionalProperties: false,
189936
+ properties: {
189937
+ actor: {
189938
+ type: "string",
189939
+ description: "Who acts at this state. Required on every non-commit state; forbidden on a commit state."
189940
+ },
189941
+ script: {
189942
+ type: "string",
189943
+ description: "Content kind: a shell script (Eta template). `gtd run` executes it verbatim via bash, then steps this state's actor."
189944
+ },
189945
+ prompt: {
189946
+ type: "string",
189947
+ description: "Content kind: an agent prompt (Eta template), emitted by `gtd next`."
189948
+ },
189949
+ message: {
189950
+ type: "string",
189951
+ description: "Content kind: a human-facing message (Eta template), emitted by `gtd next`."
189952
+ },
189953
+ commit: {
189954
+ type: "string",
189955
+ description: "Content kind: entering this state ends the process by squashing it into one commit with this message (Eta template). Final — no actor, no on."
189956
+ },
189957
+ on: {
189958
+ type: "object",
189959
+ description: "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise).",
189960
+ additionalProperties: { type: "string" }
189961
+ },
189962
+ initial: {
189963
+ type: "boolean",
189964
+ description: "Exactly one state in the workflow must declare initial: true (and it must not be a commit state)."
189965
+ },
189966
+ retry: {
189967
+ type: "object",
189968
+ description: "Redirect transitions INTO this state once it has been entered `max` times in the current process.",
189969
+ additionalProperties: false,
189970
+ required: ["max", "otherwise"],
189971
+ properties: {
189972
+ max: {
189973
+ type: "integer",
189974
+ minimum: 0,
189975
+ description: "Entries allowed this process before redirecting."
189976
+ },
189977
+ otherwise: {
189978
+ type: "string",
189979
+ description: "Defined state to redirect to once over the cap."
189980
+ }
189981
+ }
189982
+ },
189983
+ model: {
189984
+ type: "string",
189985
+ description: "Opaque harness hint passed through `gtd next --json`/`gtd status --json` (e.g. \"smart\"). Never interpreted by gtd. Forbidden on a commit state."
189986
+ },
189987
+ file: {
189988
+ type: "string",
189989
+ description: "The state's steering file: an Eta template naming the file a human/editor should look at while the machine rests here. Forbidden on a commit state."
189990
+ },
189991
+ mode: {
189992
+ type: "string",
189993
+ enum: ["qa", "review"],
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."
190003
+ }
190004
+ }
190005
+ }
190006
+ }
190007
+ }
190008
+ };
189822
190009
  const ConfigSchema = Struct({
189823
- workflow: optional(Unknown),
189824
- vars: optional(Unknown)
190010
+ workflow: optional(Unknown.annotations({ jsonSchema: workflowJsonSchema })),
190011
+ vars: optional(Unknown.annotations({ jsonSchema: varsJsonSchema }))
189825
190012
  });
189826
190013
  //#endregion
189827
190014
  //#region src/Config.ts
@@ -190735,7 +190922,7 @@ const renderDiff = (files) => {
190735
190922
  };
190736
190923
  //#endregion
190737
190924
  //#region src/Git.ts
190738
- const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
190925
+ const EMPTY_TREE$2 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
190739
190926
  /**
190740
190927
  * Parse `git diff --name-status` output into `{ path, status }` pairs.
190741
190928
  * Status codes: A (added), D (deleted), M (modified), R (renamed), C (copied), etc.
@@ -190865,7 +191052,7 @@ const makeGitImpl = (executor, root) => {
190865
191052
  }),
190866
191053
  commitDiff: (hash, exclude = []) => gen(function* () {
190867
191054
  yield* exec("git", "rev-parse", "--verify", hash);
190868
- 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)));
190869
191056
  const nameStatusOut = yield* exec("git", "diff", "--name-status", parent, hash).pipe(catchAll(() => succeed$3("")));
190870
191057
  const allPaths = parseNameStatus(nameStatusOut);
190871
191058
  const filtered = applyExcludes(allPaths, exclude);
@@ -190909,6 +191096,8 @@ const makeGitImpl = (executor, root) => {
190909
191096
  return all;
190910
191097
  }),
190911
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))),
190912
191101
  topLevel: () => exec("git", "rev-parse", "--show-toplevel").pipe(map$4((s) => s.trim())),
190913
191102
  commitHistory: (base) => {
190914
191103
  const range = base !== void 0 ? `${base}..HEAD` : void 0;
@@ -190943,7 +191132,12 @@ const makeGitImpl = (executor, root) => {
190943
191132
  discardPending: () => gen(function* () {
190944
191133
  yield* exec("git", "add", "-A");
190945
191134
  yield* exec("git", "reset", "--hard", "HEAD");
190946
- }).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)
190947
191141
  };
190948
191142
  };
190949
191143
  const makeLiveEffect = gen(function* () {
@@ -191493,8 +191687,8 @@ const renderStateTemplate = (template, context) => eta.renderString(template, co
191493
191687
  * one commit (or one squash). A caller that wants several transitions issues
191494
191688
  * several invocations.
191495
191689
  */
191496
- const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
191497
- const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
191690
+ const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
191691
+ const subjectOf$1 = (message) => (message.split("\n")[0] ?? "").trim();
191498
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). */
191499
191693
  const normalizeStatus = (raw) => raw === "A" ? "A" : raw === "D" ? "D" : "M";
191500
191694
  /** Resolve HEAD's subject against the active workflow definition (the bundled default, or a compiled `.gtdrc` `workflow:` key). */
@@ -191538,20 +191732,20 @@ const pendingChanges = (git) => git.changedPaths().pipe(map$4((entries) => entri
191538
191732
  const computeProcessRun = (git, def) => gen(function* () {
191539
191733
  if (!(yield* git.hasCommits())) return {
191540
191734
  startHash: "",
191541
- startParentHash: EMPTY_TREE,
191735
+ startParentHash: EMPTY_TREE$1,
191542
191736
  trace: []
191543
191737
  };
191544
191738
  const initialState = initialStateOf(def);
191545
191739
  const history = yield* git.commitHistory();
191546
191740
  let i = history.length - 1;
191547
191741
  while (i >= 0) {
191548
- const parsed = parseStateSubject(subjectOf(history[i].message));
191742
+ const parsed = parseStateSubject(subjectOf$1(history[i].message));
191549
191743
  if (parsed === void 0 || parsed.state === initialState) break;
191550
191744
  i--;
191551
191745
  }
191552
191746
  const startIdx = i + 1;
191553
- const trace = history.slice(startIdx).map((h) => parseStateSubject(subjectOf(h.message)).state);
191554
- 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;
191555
191749
  return {
191556
191750
  startHash: startIdx < history.length ? history[startIdx].hash : history[history.length - 1].hash,
191557
191751
  startParentHash,
@@ -191675,7 +191869,7 @@ const executeDecision = (git, run, decision, context) => gen(function* () {
191675
191869
  yield* git.discardPending();
191676
191870
  return {
191677
191871
  kind: "squash",
191678
- subject: subjectOf(message)
191872
+ subject: subjectOf$1(message)
191679
191873
  };
191680
191874
  }
191681
191875
  case "noop": return {
@@ -191685,6 +191879,120 @@ const executeDecision = (git, run, decision, context) => gen(function* () {
191685
191879
  }
191686
191880
  });
191687
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
191688
191996
  //#region node_modules/prettier/doc.mjs
191689
191997
  var doc_exports = /* @__PURE__ */ __exportAll({
191690
191998
  builders: () => builders,
@@ -364489,6 +364797,76 @@ const startLspServer = () => gen(function* () {
364489
364797
  });
364490
364798
  });
364491
364799
  //#endregion
364800
+ //#region src/Mermaid.ts
364801
+ /**
364802
+ * `gtd mermaid`: a pure emitter of the active workflow's SHAPE as Mermaid
364803
+ * `stateDiagram-v2` source — one node per state, the `[*] -->` initial-state
364804
+ * marker, one edge per declared `on` row (labeled with its raw pattern
364805
+ * string), `--> [*]` for every commit state (final, no outgoing edges), and
364806
+ * one `note right of` per rest naming its actor/content-kind/retry cap. No
364807
+ * git, no Effect, no template rendering: same purity discipline as
364808
+ * `PatternMachine.ts` — a plain function of an already-`validateDefinition`d
364809
+ * `WorkflowDefinition` (see `Config.ts`'s `toOperations`/`compileWorkflowConfig`
364810
+ * — every `WorkflowDefinition` this module ever sees has already been
364811
+ * validated, so an `on` target or `retry.otherwise` is trusted to name a
364812
+ * defined state rather than re-checked here).
364813
+ */
364814
+ /**
364815
+ * Mermaid identifiers must avoid quotes/newlines inside a quoted label —
364816
+ * collapse both so the label stays on one line and the diagram stays
364817
+ * parseable even if a state name or `on` pattern happens to carry either.
364818
+ */
364819
+ const escapeLabel = (text) => text.replace(/"/g, "'").replace(/\r?\n/g, " ");
364820
+ /**
364821
+ * A Mermaid-safe node id for one state name: every non-word character folds
364822
+ * to `_` (state names may carry hyphens — `todo-validating`,
364823
+ * `review-deciding` — which Mermaid's bare identifier grammar doesn't
364824
+ * guarantee), and a leading digit gets an `s_` prefix (Mermaid ids can't
364825
+ * start with one). The exact original name still reaches the diagram via
364826
+ * this alias's `state "<name>" as <alias>` declaration.
364827
+ */
364828
+ const aliasFor = (name) => {
364829
+ const folded = name.replace(/[^A-Za-z0-9_]/g, "_");
364830
+ return /^[0-9]/.test(folded) ? `s_${folded}` : folded;
364831
+ };
364832
+ /** One rest's actor/content-kind/retry-cap summary, for a `note right of` line. */
364833
+ const stateNote = (state) => {
364834
+ const parts = [state.actor, contentKindOf(state)].filter((p) => p !== void 0);
364835
+ const retry = state.retry !== void 0 ? [`retry ${state.retry.max}→${state.retry.otherwise}`] : [];
364836
+ return [...parts, ...retry].join(" · ");
364837
+ };
364838
+ /**
364839
+ * Render `def`'s shape as Mermaid `stateDiagram-v2` source. One
364840
+ * `state "<name>" as <alias>` declaration per state up front (so every
364841
+ * transition/note below can reference the safe alias while the rendered
364842
+ * diagram still shows the exact declared name), then the initial-state
364843
+ * marker, then one edge per `on` row (declaration order — same "first match
364844
+ * wins" order the engine itself evaluates), then `<alias> --> [*]` for every
364845
+ * commit state, then one actor/content-kind/retry note per rest.
364846
+ */
364847
+ const renderMermaid = (def) => {
364848
+ const names = Object.keys(def.states);
364849
+ const alias = new Map(names.map((name) => [name, aliasFor(name)]));
364850
+ const lines = ["stateDiagram-v2"];
364851
+ for (const name of names) lines.push(` state "${escapeLabel(name)}" as ${alias.get(name)}`);
364852
+ lines.push(` [*] --> ${alias.get(initialStateOf(def))}`);
364853
+ for (const name of names) {
364854
+ const state = def.states[name];
364855
+ const from = alias.get(name);
364856
+ if (isCommitState(state)) {
364857
+ lines.push(` ${from} --> [*]`);
364858
+ continue;
364859
+ }
364860
+ for (const [pattern, target] of state.on ?? []) lines.push(` ${from} --> ${alias.get(target)} : ${escapeLabel(pattern)}`);
364861
+ }
364862
+ for (const name of names) {
364863
+ const state = def.states[name];
364864
+ if (isCommitState(state)) continue;
364865
+ lines.push(` note right of ${alias.get(name)} : ${escapeLabel(stateNote(state))}`);
364866
+ }
364867
+ return lines.join("\n") + "\n";
364868
+ };
364869
+ //#endregion
364492
364870
  //#region src/program.ts
364493
364871
  const _require = createRequire(import.meta.url);
364494
364872
  const GTD_VERSION = _require("../package.json").version;
@@ -364504,6 +364882,8 @@ Commands:
364504
364882
  actor (the built-in script driver)
364505
364883
  status Print the resolved rest's state/actor and which declared
364506
364884
  pattern (if any) each pending change matches (no mutation)
364885
+ mermaid Print the active workflow's shape as Mermaid
364886
+ stateDiagram-v2 source (no mutation)
364507
364887
  format <file> Format a markdown file in place
364508
364888
  lsp Start the LSP server for .gtd/ steering files (stdio)
364509
364889
 
@@ -364714,11 +365094,27 @@ const runStatusCommand = (argv, json, write) => gen(function* () {
364714
365094
  if (json) writeStatusJson(write, rest, statusChanges, model, file);
364715
365095
  else writeStatusPlain(write, rest, statusChanges, model, file);
364716
365096
  });
365097
+ /**
365098
+ * `gtd mermaid`: pure emitter of the active workflow's SHAPE (not the
365099
+ * resolved rest) as Mermaid `stateDiagram-v2` source — see `src/Mermaid.ts`.
365100
+ * Needs only `ConfigService` (no HEAD resolution, no rendering), but is
365101
+ * dispatched alongside `next`/`status` since it still depends on the active
365102
+ * `.gtdrc` — unlike `format`/`lsp`, which need neither git nor config — so it
365103
+ * goes through the same repository-root guard and auto-init. Rejects
365104
+ * `--json`: there is no structured shape to emit beyond the Mermaid source
365105
+ * itself.
365106
+ */
365107
+ const runMermaidCommand = (argv, json, write) => gen(function* () {
365108
+ if (json) return yield* fail$2(/* @__PURE__ */ new Error("gtd mermaid does not accept --json"));
365109
+ yield* rejectExtraArgs("mermaid", argv);
365110
+ write(renderMermaid((yield* ConfigService).workflow));
365111
+ });
364717
365112
  const KNOWN_SUBCOMMANDS = [
364718
365113
  "step",
364719
365114
  "next",
364720
365115
  "run",
364721
- "status"
365116
+ "status",
365117
+ "mermaid"
364722
365118
  ];
364723
365119
  /**
364724
365120
  * `--version`/`-v` or `--help`/`-h`: short-circuits before any git or state
@@ -364768,6 +365164,7 @@ const dispatchKnownSubcommand = (sub, argv, json, write) => {
364768
365164
  case "next": return runNextCommand(json, write);
364769
365165
  case "run": return runRunCommand(argv, json, write);
364770
365166
  case "status": return runStatusCommand(argv, json, write);
365167
+ case "mermaid": return runMermaidCommand(argv, json, write);
364771
365168
  }
364772
365169
  };
364773
365170
  /**
@@ -364799,8 +365196,12 @@ function makeProgram(opts = {}) {
364799
365196
  const git = yield* GitService;
364800
365197
  const fs = yield* FileSystem;
364801
365198
  yield* assertRunningFromRepoRoot(git, fs);
365199
+ yield* closeReviewWindow;
364802
365200
  yield* (yield* ConfigInit).ensure;
364803
- 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);
364804
365205
  }).pipe(json ? catchAll((error) => sync$1(() => write(JSON.stringify({
364805
365206
  state: "error",
364806
365207
  prompt: error.message