@pmelab/gtd 3.2.0 → 3.3.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.
@@ -189536,7 +189536,8 @@ const validateDefinition = (def) => {
189536
189536
  * actor: <string> # forbidden on a commit state, required otherwise
189537
189537
  * script: <string> # exactly one of script/prompt/message/commit
189538
189538
  * on: # a mapping, DECLARATION ORDER PRESERVED
189539
- * "<pattern>": <targetState>
189539
+ * "<pattern>": <targetState> # short form
189540
+ * "<pattern>": { to: <targetState>, describe: <sentence> } # with a human-readable route description
189540
189541
  * initial: true # exactly one state across the whole workflow
189541
189542
  * retry:
189542
189543
  * max: <number>
@@ -189663,7 +189664,42 @@ const resolveContent = (value, configDir, where, errors) => {
189663
189664
  return;
189664
189665
  }
189665
189666
  };
189666
- /** The `on` mapping: pattern -> target, preserving declaration order as `OnEdge` tuples. */
189667
+ const KNOWN_EDGE_KEYS = /* @__PURE__ */ new Set(["to", "describe"]);
189668
+ /**
189669
+ * Compile one `on` row's value into an `OnEdge` (or `undefined`, pushing a
189670
+ * finding, when the value is malformed). The value is EITHER a target-state
189671
+ * name (a string) OR a `{ to: <target>, describe: <sentence> }` object — the
189672
+ * object form attaches an optional human-readable `describe` a `message:`
189673
+ * template can surface at a rest (see `PatternMachine.OnEdge`).
189674
+ */
189675
+ const compileOnEdge = (pattern, value, name, errors) => {
189676
+ if (typeof value === "string") return [pattern, value];
189677
+ if (!isPlainObject$1(value)) {
189678
+ errors.push(`state "${name}": "on" entry for pattern "${pattern}" must be a target state name (string) or a { to, describe } object`);
189679
+ return;
189680
+ }
189681
+ const unknownKeys = Object.keys(value).filter((k) => !KNOWN_EDGE_KEYS.has(k));
189682
+ if (unknownKeys.length > 0) errors.push(`state "${name}": "on" entry for pattern "${pattern}" has unknown key(s) ${unknownKeys.join(", ")}`);
189683
+ const { to, describe } = value;
189684
+ if (typeof to !== "string") {
189685
+ errors.push(`state "${name}": "on.${pattern}.to" must be a target state name (string)`);
189686
+ return;
189687
+ }
189688
+ if (describe !== void 0 && typeof describe !== "string") {
189689
+ errors.push(`state "${name}": "on.${pattern}.describe" must be a string`);
189690
+ return;
189691
+ }
189692
+ return describe !== void 0 ? [
189693
+ pattern,
189694
+ to,
189695
+ describe
189696
+ ] : [pattern, to];
189697
+ };
189698
+ /**
189699
+ * The `on` mapping: pattern -> edge, preserving declaration order as `OnEdge`
189700
+ * tuples. Each row's value is compiled by `compileOnEdge` (a target string or
189701
+ * a `{ to, describe }` object).
189702
+ */
189667
189703
  const compileOn = (raw, name, errors) => {
189668
189704
  if (raw === void 0) return void 0;
189669
189705
  if (!isPlainObject$1(raw)) {
@@ -189671,12 +189707,9 @@ const compileOn = (raw, name, errors) => {
189671
189707
  return;
189672
189708
  }
189673
189709
  const edges = [];
189674
- for (const [pattern, target] of Object.entries(raw)) {
189675
- if (typeof target !== "string") {
189676
- errors.push(`state "${name}": "on" target for pattern "${pattern}" must be a string`);
189677
- continue;
189678
- }
189679
- edges.push([pattern, target]);
189710
+ for (const [pattern, value] of Object.entries(raw)) {
189711
+ const edge = compileOnEdge(pattern, value, name, errors);
189712
+ if (edge !== void 0) edges.push(edge);
189680
189713
  }
189681
189714
  return edges;
189682
189715
  };
@@ -189863,7 +189896,7 @@ const compileWorkflowConfig = (raw, configDir) => {
189863
189896
  * default.yaml's content values start with `./`/`../`, so
189864
189897
  * `compileWorkflowConfig` never resolves a file reference against it.
189865
189898
  */
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"), ".");
189899
+ 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 # Every human-gate `message:` below closes with the SAME rendered block —\n # a \"What each change does next\" list built from `it.edges`, the resting\n # state's own `on` edges (see PatternTemplates.TemplateContext.edges). Each\n # edge's human-readable route lives in its `describe:` (the single source\n # of truth), so the list can never drift from the routing it documents.\n # The block skips edges without a `describe`, and each `<%~ ... \"\\n\" %>`\n # keeps the bullet's own newline (Eta's autoTrim strips the template's own\n # line breaks around `<% %>` tags, not text inside an interpolation).\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.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: grilling\n describe: >-\n Save your sketch to `.gtd/TODO.md` (any working-tree change) to start\n the cycle — the agent reads it, explores the codebase, and develops it\n into a concrete implementation plan (**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. Answer a question by editing its entry in place: replace its\n `Suggested default: ...` line with `Answer: ...`, or add further detail.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: building\n describe: >-\n Change nothing to accept every suggested default as-is and move on to\n implementation (**building**).\n \"* **\":\n to: grilling\n describe: >-\n Edit the plan — answer a question, add detail, or raise a new one — to\n send it back for another round of grilling (**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 What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: checking\n describe: >-\n Fix the failure yourself — edit the code and/or `.gtd/FEEDBACK.md` —\n to re-run the check (**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 — one `- [ ]` checkbox per reviewable item, grouped into chunks.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"D .gtd/REVIEW.md\":\n to: idle\n describe: >-\n Delete `.gtd/REVIEW.md` outright to approve the whole cycle at once\n and rest back at the start (**idle**).\n \"M .gtd/REVIEW.md\":\n to: review-deciding\n describe: >-\n Edit `.gtd/REVIEW.md` — tick a `- [ ]` box to `- [x]` to approve that\n item, or add notes — to hand it to a deterministic check\n (**review-deciding**) that closes the cycle out when every box is\n ticked, or extracts the still-unticked items into a fresh\n `.gtd/TODO.md` and loops back through **grilling** with that feedback.\n \"* **\":\n to: grilling\n describe: >-\n Change only code, leaving `.gtd/REVIEW.md` untouched, to send that as\n feedback straight back to **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"), ".");
189867
189900
  const defaultWorkflowDefinition = compiled.definition;
189868
189901
  /** 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`). */
189869
189902
  const defaultWorkflowVars = compiled.vars;
@@ -189956,8 +189989,25 @@ const workflowJsonSchema = {
189956
189989
  },
189957
189990
  on: {
189958
189991
  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" }
189992
+ 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). A value is either the target state name (string) or a { to, describe } object whose describe is a human-readable sentence templates can surface as it.edges (e.g. in a human gate's message).",
189993
+ additionalProperties: { oneOf: [{
189994
+ type: "string",
189995
+ description: "The target state name."
189996
+ }, {
189997
+ type: "object",
189998
+ additionalProperties: false,
189999
+ required: ["to"],
190000
+ properties: {
190001
+ to: {
190002
+ type: "string",
190003
+ description: "The target state name."
190004
+ },
190005
+ describe: {
190006
+ type: "string",
190007
+ description: "Human-readable sentence describing where this change routes; surfaced verbatim (never Eta-rendered) to templates as it.edges[].describe."
190008
+ }
190009
+ }
190010
+ }] }
189961
190011
  },
189962
190012
  initial: {
189963
190013
  type: "boolean",
@@ -191777,8 +191827,17 @@ const resolveVars = (workflowVars, rcVars, env) => ({
191777
191827
  ...rcVars,
191778
191828
  ...envVarsFrom(env)
191779
191829
  });
191780
- /** Build the `PatternTemplates.TemplateContext` for rendering `state`'s content at the resolved rest. `vars` is the already-merged three-layer map (see `resolveVars`). */
191781
- const buildTemplateContext = (git, read, state, actor, run, vars) => gen(function* () {
191830
+ /** Map a state's raw `on` edges to the `{ pattern, target, describe? }` shape templates see as `it.edges`. `undefined` (a commit state, no `on`) yields an empty list. */
191831
+ const toTemplateEdges = (edges) => (edges ?? []).map(([pattern, target, describe]) => describe !== void 0 ? {
191832
+ pattern,
191833
+ target,
191834
+ describe
191835
+ } : {
191836
+ pattern,
191837
+ target
191838
+ });
191839
+ /** Build the `PatternTemplates.TemplateContext` for rendering `state`'s content at the resolved rest. `vars` is the already-merged three-layer map (see `resolveVars`); `edges` is the resting state's own `on` edges (see `toTemplateEdges`). */
191840
+ const buildTemplateContext = (git, read, state, actor, run, vars, edges) => gen(function* () {
191782
191841
  const hasCommits = yield* git.hasCommits();
191783
191842
  const currentCommit = hasCommits ? yield* git.resolveRef("HEAD") : "";
191784
191843
  const previousCommit = hasCommits ? yield* git.resolveRef("HEAD~1").pipe(catchAll(() => succeed$3(run.startParentHash))) : "";
@@ -191793,7 +191852,8 @@ const buildTemplateContext = (git, read, state, actor, run, vars) => gen(functio
191793
191852
  processDiff,
191794
191853
  lastDiff,
191795
191854
  read,
191796
- vars
191855
+ vars,
191856
+ edges: toTemplateEdges(edges)
191797
191857
  };
191798
191858
  });
191799
191859
  /**
@@ -191838,7 +191898,8 @@ const renderRest = (rest, context) => gen(function* () {
191838
191898
  content,
191839
191899
  ...model !== void 0 ? { model } : {},
191840
191900
  ...file !== void 0 ? { file } : {},
191841
- ...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {}
191901
+ ...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {},
191902
+ edges: toTemplateEdges(rest.stateDef.on)
191842
191903
  };
191843
191904
  });
191844
191905
  /**
@@ -364628,7 +364689,8 @@ const buildFileModeMap = (def, vars, root) => {
364628
364689
  read: (path) => {
364629
364690
  throw new Error(`"file:" is not readable while building the path→mode map (path: ${path})`);
364630
364691
  },
364631
- vars
364692
+ vars,
364693
+ edges: []
364632
364694
  });
364633
364695
  } catch (e) {
364634
364696
  warnings.push(`state "${name}": "file:" failed to render, skipped — ${e instanceof Error ? e.message : String(e)}`);
@@ -364717,7 +364779,7 @@ const resolveSteeringFile = (root) => gen(function* () {
364717
364779
  const rest = yield* resolveRest();
364718
364780
  const run = yield* computeProcessRun(git, rest.def);
364719
364781
  const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
364720
- const context = yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars);
364782
+ const context = yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars, rest.stateDef.on);
364721
364783
  const file = yield* renderFile(rest.stateDef, context);
364722
364784
  return {
364723
364785
  state: rest.state,
@@ -364949,7 +365011,7 @@ const resolveRestContext = (git) => gen(function* () {
364949
365011
  return {
364950
365012
  rest,
364951
365013
  run,
364952
- context: yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars)
365014
+ context: yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars, rest.stateDef.on)
364953
365015
  };
364954
365016
  });
364955
365017
  /**
@@ -364982,7 +365044,8 @@ const stepAsActor = (invoker) => gen(function* () {
364982
365044
  };
364983
365045
  const executable = decision;
364984
365046
  const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
364985
- const outcome = yield* executeDecision(git, run, executable, yield* buildTemplateContext(git, worktree.read, decision.kind === "squash" ? decision.state : rest.state, invoker, run, vars));
365047
+ const renderedState = decision.kind === "squash" ? decision.state : rest.state;
365048
+ const outcome = yield* executeDecision(git, run, executable, yield* buildTemplateContext(git, worktree.read, renderedState, invoker, run, vars, rest.def.states[renderedState]?.on));
364986
365049
  return {
364987
365050
  state: rest.state,
364988
365051
  subject: outcome.kind === "noop" ? null : outcome.subject
@@ -365016,7 +365079,8 @@ const runNextCommand = (json, write) => gen(function* () {
365016
365079
  content: rendered.content,
365017
365080
  ...rendered.model !== void 0 ? { model: rendered.model } : {},
365018
365081
  ...rendered.file !== void 0 ? { file: rendered.file } : {},
365019
- ...rendered.mode !== void 0 ? { mode: rendered.mode } : {}
365082
+ ...rendered.mode !== void 0 ? { mode: rendered.mode } : {},
365083
+ ...rendered.edges.length > 0 ? { edges: rendered.edges } : {}
365020
365084
  }) + "\n");
365021
365085
  else write(rendered.content.endsWith("\n") ? rendered.content : rendered.content + "\n");
365022
365086
  });
@@ -365060,13 +365124,15 @@ const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
365060
365124
  });
365061
365125
  /** `gtd status --json`'s emission — `{state, actor, changes, model?, file?, mode?}`. */
365062
365126
  const writeStatusJson = (write, rest, statusChanges, model, file) => {
365127
+ const edges = toTemplateEdges(rest.stateDef.on);
365063
365128
  write(JSON.stringify({
365064
365129
  state: rest.state,
365065
365130
  actor: rest.actor,
365066
365131
  changes: statusChanges,
365067
365132
  ...model !== void 0 ? { model } : {},
365068
365133
  ...file !== void 0 ? { file } : {},
365069
- ...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {}
365134
+ ...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {},
365135
+ ...edges.length > 0 ? { edges } : {}
365070
365136
  }) + "\n");
365071
365137
  };
365072
365138
  /** `gtd status`'s plain-text emission — `State:`/`Awaits:`/`Model:`/`File:`/`Mode:`/`Pending:` lines. */
package/docs/cli.md CHANGED
@@ -86,8 +86,9 @@ process in the same step that entered it — see
86
86
  [STATES.md §5](../STATES.md#5-resolution)).
87
87
 
88
88
  Plain mode prints the rendered content verbatim (with exactly one trailing
89
- newline) — never the `model`/`file`/`mode` hints, which are JSON-only. `--json`
90
- emits `{state, actor, kind, content, model?, file?, mode?}`:
89
+ newline) — never the `model`/`file`/`mode`/`edges` structured keys, which are
90
+ JSON-only. `--json` emits
91
+ `{state, actor, kind, content, model?, file?, mode?, edges?}`:
91
92
 
92
93
  ```json
93
94
  {
@@ -113,6 +114,14 @@ emits `{state, actor, kind, content, model?, file?, mode?}`:
113
114
  [Configuration](configuration.md#filemode--the-steering-file-association)).
114
115
  Both present only when the state declares them; **omitted entirely** (never
115
116
  `null`) otherwise.
117
+ - `edges` — the resting state's `on` edges as `[{ pattern, target, describe? }]`
118
+ (declaration order) — the same list the content template sees as `it.edges`
119
+ (see
120
+ [Configuration](configuration.md#on-values--a-target-or-a--to-describe--route-description)).
121
+ A driver relaying a human gate's message has the routing (and each edge's
122
+ human-readable `describe`) alongside the rendered text. **Omitted entirely**
123
+ when the state has no `on` (a commit state); a per-edge `describe` is likewise
124
+ omitted when that edge declares none.
116
125
 
117
126
  ## `gtd run`
118
127
 
@@ -159,10 +168,12 @@ after `Awaits:` when the resolved state declares a `model:` hint, and
159
168
  declared — each independently, only when set.
160
169
 
161
170
  `--json` emits
162
- `{state, actor, changes: [{status, path, pattern}], model?, file?, mode?}` —
163
- `pattern` is `null` when no declared row matches that change; `model`/`file`/
171
+ `{state, actor, changes: [{status, path, pattern}], model?, file?, mode?, edges?}`
172
+ — `pattern` is `null` when no declared row matches that change; `model`/`file`/
164
173
  `mode` are present only when the resolved state declares them (omitted entirely,
165
- never `null`, otherwise):
174
+ never `null`, otherwise); `edges` is the resting state's `on` edges as
175
+ `[{ pattern, target, describe? }]` (same as `gtd next --json`), omitted when the
176
+ state has no `on`:
166
177
 
167
178
  ```json
168
179
  {
@@ -54,7 +54,8 @@ workflow:
54
54
  message: <string>
55
55
  commit: <string>
56
56
  on: # a mapping, DECLARATION ORDER PRESERVED
57
- "<pattern>": <targetState>
57
+ "<pattern>": <targetState> # short form
58
+ "<pattern>": { to: <targetState>, describe: <sentence> } # with a human-readable route description
58
59
  initial: true # exactly one state across the whole workflow
59
60
  retry:
60
61
  max: <number>
@@ -90,6 +91,51 @@ A `vars:` key sibling to `states:` inside `workflow:` declares the workflow's
90
91
  own defaults for `it.vars` — see ["Variables"](#variables) below for the full
91
92
  three-layer picture.
92
93
 
94
+ ### `on:` values — a target, or a `{ to, describe }` route description
95
+
96
+ An `on` row's value is normally the target state name. It may instead be an
97
+ object `{ to: <target>, describe: <sentence> }`, attaching a human-readable
98
+ `describe` — a plain sentence explaining what making that kind of change does
99
+ next. `describe` is **inert to the engine**: it plays no part in matching, is
100
+ never Eta-rendered (exactly like the pattern key itself — see the "Known
101
+ limitation" note below), and never affects a decision. It exists only to be
102
+ surfaced. A state's own edges are handed to its content template as `it.edges`
103
+ (an array of `{ pattern, target, describe? }`, in declaration order), so a human
104
+ gate's `message:` can render a "what each change does next" list from the same
105
+ routing the engine uses — one source of truth:
106
+
107
+ ```yaml
108
+ workflow:
109
+ states:
110
+ await-review:
111
+ actor: human
112
+ message: |
113
+ Review the diff, then run `gtd step human`.
114
+
115
+ What each change does next:
116
+ <% it.edges.forEach(function (e) { if (e.describe) { %>
117
+ <%~ "- " + e.describe + "\n" %>
118
+ <% } }) %>
119
+ on:
120
+ "D .gtd/REVIEW.md":
121
+ to: idle
122
+ describe:
123
+ "Delete `.gtd/REVIEW.md` to approve the whole cycle and rest at
124
+ idle."
125
+ "* **":
126
+ to: grilling
127
+ describe:
128
+ "Change any source file to leave feedback and start another round."
129
+ ```
130
+
131
+ The `<%~ "- " + e.describe + "\n" %>` idiom keeps each bullet's own newline: Eta
132
+ strips the template's line breaks around `<% %>` control tags (its `autoTrim`
133
+ default), not text inside an interpolation. Edges without a `describe` are
134
+ skipped by the `if (e.describe)` guard. `gtd next --json` and
135
+ `gtd status --json` also emit the `edges` array; both omit it when the state has
136
+ no `on` (a commit state), and omit a per-edge `describe` when that edge declares
137
+ none.
138
+
93
139
  ### `model:` — the opaque harness hint, template-rendered
94
140
 
95
141
  A state may declare `model: <string>` — an OPAQUE label (e.g. `smart`, `fast`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "3.2.0",
3
+ "version": "3.3.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
@@ -47,9 +47,29 @@
47
47
  },
48
48
  "on": {
49
49
  "type": "object",
50
- "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).",
50
+ "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). A value is either the target state name (string) or a { to, describe } object whose describe is a human-readable sentence templates can surface as it.edges (e.g. in a human gate's message).",
51
51
  "additionalProperties": {
52
- "type": "string"
52
+ "oneOf": [
53
+ {
54
+ "type": "string",
55
+ "description": "The target state name."
56
+ },
57
+ {
58
+ "type": "object",
59
+ "additionalProperties": false,
60
+ "required": ["to"],
61
+ "properties": {
62
+ "to": {
63
+ "type": "string",
64
+ "description": "The target state name."
65
+ },
66
+ "describe": {
67
+ "type": "string",
68
+ "description": "Human-readable sentence describing where this change routes; surfaced verbatim (never Eta-rendered) to templates as it.edges[].describe."
69
+ }
70
+ }
71
+ }
72
+ ]
53
73
  }
54
74
  },
55
75
  "initial": {