@pmelab/gtd 3.4.0 → 3.6.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.
@@ -189919,7 +189919,7 @@ const compileWorkflowConfig = (raw, configDir) => {
189919
189919
  * default.yaml's content values start with `./`/`../`, so
189920
189920
  * `compileWorkflowConfig` never resolves a file reference against it.
189921
189921
  */
189922
- 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# The four agent states each declare a `memory:` scope label (`plan`,\n# `build`, `fix`, `review` — see the comment at `grilling` below) — another\n# opaque hint a memory-aware driver reads to decide when an agent turn keeps\n# the previous turn's memory and when it starts fresh: a loop that re-enters\n# one state (grilling around its answer loop, fixing around checking) keeps\n# the same label and retains memory across its laps; crossing to the next\n# phase's differently-labelled state clears it. gtd never interprets the\n# label (see STATES.md §1 and skills/loop/SKILL.md's driver contract).\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 # `memory` is an opaque memory-scope label — gtd never interprets it, it\n # only passes it through to `gtd next --json`/`gtd status --json` so a\n # memory-aware driving loop can decide when an agent turn continues from\n # the previous turn's memory and when it starts fresh: consecutive agent\n # turns that emit the SAME label share memory, a change (or the first\n # turn) is where the driver starts fresh. `grilling` re-enters itself\n # around the todo-validating/grilling-answer loop, so labelling every lap\n # `plan` keeps the planner's accumulated exploration across the loop; the\n # next phase (`building`) carries a different label, clearing it.\n memory: plan\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 # A distinct memory scope from `plan`: implementation starts fresh, with\n # the plan already settled in `.gtd/TODO.md` (which this prompt reads\n # below) rather than carried in the planner's working memory.\n memory: build\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 # Its own memory scope: the fix loop re-enters `fixing` around `checking`\n # (up to the retry cap), so labelling every attempt `fix` lets the agent\n # remember what it already tried across the loop; a green check moves on\n # to the differently-labelled `reviewing`, clearing it.\n memory: fix\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 # A distinct memory scope again: the reviewer looks at the cycle's full\n # diff (inlined into its prompt) with a fresh perspective, not the\n # builder's or fixer's working memory.\n memory: review\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"), ".");
189922
+ 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-validation-command.md for the `gtd validate`\n# self-validation model that replaced the old in-machine validation loops).\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 — 10 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 rather than guessing silently. Its\n# `file:`/`mode:` (TODO.md/`qa`) makes the plan's format checkable — it\n# self-validates before finishing (see \"Steering-file validation\" below).\n# grilling -> 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, grouping the cycle's full diff into\n# reviewable chunks; its `file:`/`mode:` (REVIEW.md/`review`) makes that\n# format checkable, so it self-validates before finishing.\n# reviewing -> 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; REVIEW.md by the review-deciding approval branch; TODO.md by\n# `building`.\n#\n# Steering-file validation. `grilling` and `reviewing` each declare a\n# `file:`/`mode:` pair (TODO.md as `qa`, REVIEW.md as `review`), so their\n# output has a checkable format. src/OpenQuestions.ts and src/ReviewDoc.ts are\n# those two formats' EXECUTABLE SPECS (their unit tests are the format spec\n# tests), and `gtd validate` FORMATS the resolved state's rendered `file:` in\n# place then runs the SAME parser over it, exiting non-zero with findings when\n# it violates the format. There is no in-machine validation state and no bash\n# port to keep in sync — the producing agent self-validates before finishing\n# (plain `gtd next` appends the \"run `gtd validate` and fix all violations\"\n# instruction; under `--json` the driving loop runs it after the turn and\n# re-prompts on findings — see skills/loop/SKILL.md), AND `gtd step` runs the\n# same format-and-validate gate on capture, refusing a turn whose steering file\n# is invalid. So the check runs after a human edits too — at the human gates\n# `grilling-answer` (TODO.md) and `await-review` (REVIEW.md) — and a human gate\n# is only ever handed a tidy, well-formed file. The same parsers back the LSP's\n# live diagnostics (src/Lsp.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# The four agent states each declare a `memory:` scope label (`plan`,\n# `build`, `fix`, `review` — see the comment at `grilling` below) — another\n# opaque hint a memory-aware driver reads to decide when an agent turn keeps\n# the previous turn's memory and when it starts fresh: a loop that re-enters\n# one state (grilling around its answer loop, fixing around checking) keeps\n# the same label and retains memory across its laps; crossing to the next\n# phase's differently-labelled state clears it. gtd never interprets the\n# label (see STATES.md §1 and skills/loop/SKILL.md's driver contract).\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 # `memory` is an opaque memory-scope label — gtd never interprets it, it\n # only passes it through to `gtd next --json`/`gtd status --json` so a\n # memory-aware driving loop can decide when an agent turn continues from\n # the previous turn's memory and when it starts fresh: consecutive agent\n # turns that emit the SAME label share memory, a change (or the first\n # turn) is where the driver starts fresh. `grilling` re-enters itself\n # around the grilling-answer loop, so labelling every lap\n # `plan` keeps the planner's accumulated exploration across the loop; the\n # next phase (`building`) carries a different label, clearing it.\n memory: plan\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.\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 \"* **\": 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 # A distinct memory scope from `plan`: implementation starts fresh, with\n # the plan already settled in `.gtd/TODO.md` (which this prompt reads\n # below) rather than carried in the planner's working memory.\n memory: build\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 # Its own memory scope: the fix loop re-enters `fixing` around `checking`\n # (up to the retry cap), so labelling every attempt `fix` lets the agent\n # remember what it already tried across the loop; a green check moves on\n # to the differently-labelled `reviewing`, clearing it.\n memory: fix\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 # A distinct memory scope again: the reviewer looks at the cycle's full\n # diff (inlined into its prompt) with a fresh perspective, not the\n # builder's or fixer's working memory.\n memory: review\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\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 \"* **\": 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"), ".");
189923
189923
  const defaultWorkflowDefinition = compiled.definition;
189924
189924
  /** 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`). */
189925
189925
  const defaultWorkflowVars = compiled.vars;
@@ -191766,6 +191766,40 @@ const renderStateTemplate = (template, context) => eta.renderString(template, co
191766
191766
  */
191767
191767
  const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
191768
191768
  const subjectOf$1 = (message) => (message.split("\n")[0] ?? "").trim();
191769
+ const COST_TRAILER_PREFIX = "Gtd-Cost: ";
191770
+ const COST_TRAILER_RE = /^Gtd-Cost:[ \t]*([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+?))?[ \t]*$/gm;
191771
+ /** The bucket a cost with no `--model` tag is grouped under, kept distinct so a mixed history still totals correctly. */
191772
+ const UNATTRIBUTED_MODEL = "unspecified";
191773
+ /**
191774
+ * Append a `Gtd-Cost: <cost>[ <model>]` trailer (after a blank line) to a
191775
+ * commit `subject`, or return the subject unchanged when no cost was supplied.
191776
+ * The subject (first line) is never touched, so `parseStateSubject`/
191777
+ * `resolveState` still read `gtd(<actor>): <state>` back exactly as before.
191778
+ */
191779
+ const withCostTrailer = (subject, cost, model) => cost === void 0 ? subject : `${subject}\n\n${COST_TRAILER_PREFIX}${cost}${model !== void 0 ? ` ${model}` : ""}`;
191780
+ /** Every `Gtd-Cost:` entry found across the given commit messages (each entry's model defaults to `UNATTRIBUTED_MODEL`). */
191781
+ const parseCostTrailers = (messages) => {
191782
+ const entries = [];
191783
+ for (const message of messages) for (const match of message.matchAll(COST_TRAILER_RE)) {
191784
+ const model = match[2]?.trim();
191785
+ entries.push({
191786
+ cost: Number(match[1]),
191787
+ model: model !== void 0 && model !== "" ? model : UNATTRIBUTED_MODEL
191788
+ });
191789
+ }
191790
+ return entries;
191791
+ };
191792
+ /** The total token cost across the given entries (`0` when none). */
191793
+ const totalCostOf = (entries) => entries.reduce((sum, entry) => sum + entry.cost, 0);
191794
+ /** Per-model token totals, highest-cost first (ties broken by model name for a stable order). */
191795
+ const costByModel = (entries) => {
191796
+ const byModel = /* @__PURE__ */ new Map();
191797
+ for (const entry of entries) byModel.set(entry.model, (byModel.get(entry.model) ?? 0) + entry.cost);
191798
+ return [...byModel.entries()].map(([model, cost]) => ({
191799
+ model,
191800
+ cost
191801
+ })).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
191802
+ };
191769
191803
  /** 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). */
191770
191804
  const normalizeStatus = (raw) => raw === "A" ? "A" : raw === "D" ? "D" : "M";
191771
191805
  /** Resolve HEAD's subject against the active workflow definition (the bundled default, or a compiled `.gtdrc` `workflow:` key). */
@@ -191810,7 +191844,8 @@ const computeProcessRun = (git, def) => gen(function* () {
191810
191844
  if (!(yield* git.hasCommits())) return {
191811
191845
  startHash: "",
191812
191846
  startParentHash: EMPTY_TREE$1,
191813
- trace: []
191847
+ trace: [],
191848
+ costEntries: []
191814
191849
  };
191815
191850
  const initialState = initialStateOf(def);
191816
191851
  const history = yield* git.commitHistory();
@@ -191821,12 +191856,15 @@ const computeProcessRun = (git, def) => gen(function* () {
191821
191856
  i--;
191822
191857
  }
191823
191858
  const startIdx = i + 1;
191824
- const trace = history.slice(startIdx).map((h) => parseStateSubject(subjectOf$1(h.message)).state);
191859
+ const processCommits = history.slice(startIdx);
191860
+ const trace = processCommits.map((h) => parseStateSubject(subjectOf$1(h.message)).state);
191861
+ const costEntries = parseCostTrailers(processCommits.map((h) => h.message));
191825
191862
  const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$1;
191826
191863
  return {
191827
191864
  startHash: startIdx < history.length ? history[startIdx].hash : history[history.length - 1].hash,
191828
191865
  startParentHash,
191829
- trace
191866
+ trace,
191867
+ costEntries
191830
191868
  };
191831
191869
  });
191832
191870
  /** The `GTD_VAR_`-prefix stripped, exact-case, from every matching entry — the highest-precedence `it.vars` layer. A `value === undefined` entry (a name declared-but-unset in the environment) is skipped, never coerced to the string `"undefined"`. */
@@ -191863,13 +191901,27 @@ const toTemplateEdges = (edges) => (edges ?? []).map(([pattern, target, describe
191863
191901
  pattern,
191864
191902
  target
191865
191903
  });
191866
- /** 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`). */
191867
- const buildTemplateContext = (git, read, state, actor, run, vars, edges) => gen(function* () {
191904
+ /**
191905
+ * Build the `PatternTemplates.TemplateContext` for rendering `state`'s content
191906
+ * at the resolved rest. `vars` is the already-merged three-layer map (see
191907
+ * `resolveVars`); `edges` is the resting state's own `on` edges (see
191908
+ * `toTemplateEdges`). `currentCost`/`currentModel` are the in-flight step's own
191909
+ * `--cost`/`--model` (folded into the process's committed cost entries so a
191910
+ * `commit:` squash template sees the whole-process total AND per-model
191911
+ * breakdown including the squashing step) — `0`/absent for the pure emitters
191912
+ * (`gtd next`/`gtd status`), where no step is being performed.
191913
+ */
191914
+ const buildTemplateContext = (git, read, state, actor, run, vars, edges, currentCost = 0, currentModel) => gen(function* () {
191868
191915
  const hasCommits = yield* git.hasCommits();
191869
191916
  const currentCommit = hasCommits ? yield* git.resolveRef("HEAD") : "";
191870
191917
  const previousCommit = hasCommits ? yield* git.resolveRef("HEAD~1").pipe(catchAll(() => succeed$3(run.startParentHash))) : "";
191871
191918
  const processDiff = [yield* git.diffRef(run.startParentHash).pipe(catchAll(() => succeed$3(""))), yield* git.diffHead().pipe(catchAll(() => succeed$3("")))].filter((d) => d.trim().length > 0).join("\n\n");
191872
191919
  const lastDiff = run.trace.length > 0 ? yield* git.commitDiff(currentCommit).pipe(catchAll(() => succeed$3(""))) : "";
191920
+ const stepEntry = currentCost > 0 || currentModel !== void 0 ? [{
191921
+ cost: currentCost,
191922
+ model: currentModel ?? "unspecified"
191923
+ }] : [];
191924
+ const allCostEntries = [...run.costEntries, ...stepEntry];
191873
191925
  return {
191874
191926
  startCommit: run.startHash,
191875
191927
  currentCommit,
@@ -191878,6 +191930,8 @@ const buildTemplateContext = (git, read, state, actor, run, vars, edges) => gen(
191878
191930
  actor,
191879
191931
  processDiff,
191880
191932
  lastDiff,
191933
+ processCost: totalCostOf(allCostEntries),
191934
+ processCostByModel: costByModel(allCostEntries),
191881
191935
  read,
191882
191936
  vars,
191883
191937
  edges: toTemplateEdges(edges)
@@ -191945,18 +191999,22 @@ const renderRest = (rest, context) => gen(function* () {
191945
191999
  });
191946
192000
  /**
191947
192001
  * Execute a `PatternMachine.step` decision: a `"commit"` decision stages and
191948
- * commits everything pending under the decided subject; a `"squash"`
191949
- * decision renders the commit-state template against the PENDING tree — a
191950
- * render failure REFUSES the step, touching nothing — then soft-resets to
191951
- * the process's start parent, writes ONE commit with the rendered message
191952
- * (via `commitAsIs`, so the still-uncommitted template file is excluded), and
191953
- * discards everything left pending (the template file included). A `"noop"`
191954
- * performs no IO.
191955
- */
191956
- const executeDecision = (git, run, decision, context) => gen(function* () {
192002
+ * commits everything pending under the decided subject, with an optional
192003
+ * `Gtd-Cost: <cost>[ <model>]` trailer (the invocation's `--cost`/`--model`,
192004
+ * persisted in the git log); a `"squash"` decision renders the commit-state
192005
+ * template against the PENDING tree — a render failure REFUSES the step,
192006
+ * touching nothing — then soft-resets to the process's start parent, writes
192007
+ * ONE commit with the rendered message (via `commitAsIs`, so the
192008
+ * still-uncommitted template file is excluded), and discards everything left
192009
+ * pending (the template file included). A squash records no trailer of its own
192010
+ * — the whole-process total and per-model breakdown reach the message through
192011
+ * `it.processCost`/`it.processCostByModel` in the rendered template. A
192012
+ * `"noop"` performs no IO.
192013
+ */
192014
+ const executeDecision = (git, run, decision, context, cost, model) => gen(function* () {
191957
192015
  switch (decision.kind) {
191958
192016
  case "commit":
191959
- yield* git.commitAllWithPrefix(decision.subject);
192017
+ yield* git.commitAllWithPrefix(withCostTrailer(decision.subject, cost, model));
191960
192018
  return {
191961
192019
  kind: "commit",
191962
192020
  subject: decision.subject
@@ -364338,8 +364396,8 @@ const parseQuestionBlock = (block) => {
364338
364396
  * always returns a result, never throws. `errors` is non-empty exactly when
364339
364397
  * the document violates the required structure (a `###` question under
364340
364398
  * `## Open Questions` with no recognized response line) — the caller decides
364341
- * what to do with that (the machine refuses the agent's turn capture; the
364342
- * `gtd questions` CLI command just reports it alongside whatever parsed).
364399
+ * what to do with that (`gtd validate` exits non-zero with them; the LSP
364400
+ * publishes them as diagnostics).
364343
364401
  */
364344
364402
  const parseOpenQuestions = (content) => {
364345
364403
  const lines = content.split(/\r?\n/);
@@ -364459,9 +364517,8 @@ const parseChangesets = (lines) => {
364459
364517
  * Parses the review structure out of `content` (the raw text of
364460
364518
  * `.gtd/REVIEW.md`). Total and side-effect-free: always returns a result,
364461
364519
  * never throws. `errors` is non-empty exactly when the document violates the
364462
- * required structure — the caller decides what to do with that (the machine
364463
- * refuses the agent's turn capture; the `gtd changesets` CLI command just
364464
- * reports it alongside whatever parsed).
364520
+ * required structure — the caller decides what to do with that (`gtd validate`
364521
+ * exits non-zero with them; the LSP publishes them as diagnostics).
364465
364522
  */
364466
364523
  const parseReviewDoc = (content) => {
364467
364524
  const lines = content.split(/\r?\n/);
@@ -364486,10 +364543,10 @@ const parseReviewDoc = (content) => {
364486
364543
  * LSP server for `.gtd/` steering files — document symbols for a `qa`-mode
364487
364544
  * file's open questions and a `review`-mode file's review chunks/hunks, code
364488
364545
  * actions to check/uncheck a hunk or a whole chunk, and diagnostics publishing
364489
- * the same parsers' `errors` the bundled workflow's `.gtd/FORMAT.md`
364490
- * validators produce (see `src/OpenQuestions.ts` / `src/ReviewDoc.ts`'s
364491
- * module docs for the "executable spec ↔ bash validator" contract this server
364492
- * rides on top of).
364546
+ * the same parsers' `errors` the `gtd validate` CLI command reports (see
364547
+ * `src/OpenQuestions.ts` / `src/ReviewDoc.ts`'s module docs — those parsers
364548
+ * are each format's single source of truth, shared by this server and the
364549
+ * command).
364493
364550
  *
364494
364551
  * CONFIG-DRIVEN (see `docs/design/state-file-association.md` §3): the server
364495
364552
  * locates the active gtd config the SAME way the CLI does (`ConfigService`'s
@@ -364555,7 +364612,7 @@ const questionSymbols = (content) => {
364555
364612
  };
364556
364613
  });
364557
364614
  };
364558
- /** Diagnostics for `.gtd/TODO.md` — the same findings the workflow's `todo-validating` script would write to `.gtd/FORMAT.md`, published live over LSP instead. Whole-document range: `OpenQuestionsDoc.errors` carries no per-line position. */
364615
+ /** Diagnostics for `.gtd/TODO.md` — the same findings `gtd validate` reports for a `qa`-mode file, published live over LSP instead. Whole-document range: `OpenQuestionsDoc.errors` carries no per-line position. */
364559
364616
  const questionDiagnostics = (content) => {
364560
364617
  const { errors } = parseOpenQuestions(content);
364561
364618
  const lines = content.split(/\r?\n/);
@@ -364595,7 +364652,7 @@ const reviewSymbols = (content) => {
364595
364652
  };
364596
364653
  });
364597
364654
  };
364598
- /** Diagnostics for `.gtd/REVIEW.md` — the same findings the workflow's `review-validating` script would write to `.gtd/FORMAT.md`, published live over LSP instead. Whole-document range: `ReviewDoc.errors` carries no per-line position. */
364655
+ /** Diagnostics for `.gtd/REVIEW.md` — the same findings `gtd validate` reports for a `review`-mode file, published live over LSP instead. Whole-document range: `ReviewDoc.errors` carries no per-line position. */
364599
364656
  const reviewDiagnostics = (content) => {
364600
364657
  const { errors } = parseReviewDoc(content);
364601
364658
  const lines = content.split(/\r?\n/);
@@ -364727,6 +364784,8 @@ const buildFileModeMap = (def, vars, root) => {
364727
364784
  actor: "",
364728
364785
  processDiff: "",
364729
364786
  lastDiff: "",
364787
+ processCost: 0,
364788
+ processCostByModel: [],
364730
364789
  read: (path) => {
364731
364790
  throw new Error(`"file:" is not readable while building the path→mode map (path: ${path})`);
364732
364791
  },
@@ -364922,7 +364981,7 @@ const startLspServer = () => gen(function* () {
364922
364981
  const escapeLabel = (text) => text.replace(/"/g, "'").replace(/\r?\n/g, " ");
364923
364982
  /**
364924
364983
  * A Mermaid-safe node id for one state name: every non-word character folds
364925
- * to `_` (state names may carry hyphens — `todo-validating`,
364984
+ * to `_` (state names may carry hyphens — `grilling-answer`,
364926
364985
  * `review-deciding` — which Mermaid's bare identifier grammar doesn't
364927
364986
  * guarantee), and a leading digit gets an `s_` prefix (Mermaid ids can't
364928
364987
  * start with one). The exact original name still reaches the diagram via
@@ -364978,13 +365037,20 @@ const HELP_TEXT = `Usage: gtd [command] [options]
364978
365037
  Commands:
364979
365038
  step <actor> Authenticate as <actor>, match the resolved rest's
364980
365039
  declared patterns against the pending changes, and commit
364981
- (or squash) the one resulting transition
365040
+ (or squash) the one resulting transition. Pass
365041
+ --cost=<n> (optionally --model=<name>) to record the
365042
+ just-finished invocation's token cost and model on the
365043
+ turn commit (summed into it.processCost/processCostByModel)
364982
365044
  next Print the resolved rest's rendered script/prompt/message
364983
365045
  (no mutation)
364984
365046
  run Execute the resolved rest's emitted script, then step its
364985
365047
  actor (the built-in script driver)
364986
365048
  status Print the resolved rest's state/actor and which declared
364987
365049
  pattern (if any) each pending change matches (no mutation)
365050
+ validate Validate the steering file the resolved rest declares
365051
+ (its file:/mode:) against that mode's format; exits
365052
+ non-zero with findings when the file violates it (no
365053
+ mutation)
364988
365054
  mermaid Print the active workflow's shape as Mermaid
364989
365055
  stateDiagram-v2 source (no mutation)
364990
365056
  format <file> Format a markdown file in place
@@ -364992,6 +365058,8 @@ Commands:
364992
365058
 
364993
365059
  Options:
364994
365060
  --json Output structured JSON instead of plain text
365061
+ --cost=<n> (gtd step only) record the invocation's token cost
365062
+ --model=<name> (gtd step only, with --cost) tag that cost's model
364995
365063
  --version, -v Print version and exit
364996
365064
  --help, -h Print this help and exit
364997
365065
  `;
@@ -365007,6 +365075,58 @@ const markEnveloped = (error) => Object.assign(error, { [ENVELOPED]: true });
365007
365075
  const isEnveloped = (error) => typeof error === "object" && error !== null && error[ENVELOPED] === true;
365008
365076
  /** Non-flag positional arguments past the subcommand name (argv[3..]). */
365009
365077
  const commandArgs = (argv) => argv.slice(3).filter((a) => a.length > 0 && !a.startsWith("--"));
365078
+ const COST_FLAG = "--cost=";
365079
+ /**
365080
+ * Parse the optional `--cost=<n>` flag (only `gtd step` accepts it — see
365081
+ * `makeProgram`). `<n>` is the token cost of the invocation that produced the
365082
+ * pending changes, recorded as a `Gtd-Cost:` trailer on the turn commit. Must
365083
+ * be a non-negative finite number; a bare `--cost` (no `=`) or a non-numeric/
365084
+ * negative value is a usage error. Returns `undefined` when the flag is absent.
365085
+ */
365086
+ const parseCostFlag = (argv) => {
365087
+ if (argv.slice(2).includes("--cost")) return fail$2(/* @__PURE__ */ new Error("gtd: --cost requires a value — use --cost=<number>"));
365088
+ const flag = argv.slice(2).find((a) => a.startsWith(COST_FLAG));
365089
+ if (flag === void 0) return succeed$3(void 0);
365090
+ const raw = flag.slice(7);
365091
+ const n = Number(raw);
365092
+ if (raw.trim() === "" || !Number.isFinite(n) || n < 0) return fail$2(/* @__PURE__ */ new Error(`gtd: --cost must be a non-negative number — got "${raw}"`));
365093
+ return succeed$3(n);
365094
+ };
365095
+ const MODEL_FLAG = "--model=";
365096
+ /**
365097
+ * Parse the optional `--model=<name>` flag (only `gtd step`, and only
365098
+ * alongside `--cost` — see `makeProgram`). `<name>` tags the recorded cost
365099
+ * with the model the invocation ran on, appended to the `Gtd-Cost:` trailer
365100
+ * and grouped in `it.processCostByModel`. Must be non-empty and single-line
365101
+ * (it rides on one trailer line); a bare `--model` (no `=`) or an empty/
365102
+ * multiline value is a usage error. Returns `undefined` when the flag is absent.
365103
+ */
365104
+ const parseModelFlag = (argv) => {
365105
+ if (argv.slice(2).includes("--model")) return fail$2(/* @__PURE__ */ new Error("gtd: --model requires a value — use --model=<name>"));
365106
+ const flag = argv.slice(2).find((a) => a.startsWith(MODEL_FLAG));
365107
+ if (flag === void 0) return succeed$3(void 0);
365108
+ const raw = flag.slice(8);
365109
+ if (raw.trim() === "" || /[\r\n]/.test(raw)) return fail$2(/* @__PURE__ */ new Error("gtd: --model must be a non-empty, single-line value"));
365110
+ return succeed$3(raw);
365111
+ };
365112
+ /**
365113
+ * Parse and validate the `--cost`/`--model` step flags together. Both are
365114
+ * orthogonal to `--json` but only meaningful to `gtd step` — rejected on any
365115
+ * other command rather than silently ignored (same discipline as `--json` on
365116
+ * `format`) — and `--model` requires `--cost` (a model tag with no token count
365117
+ * records nothing to sum).
365118
+ */
365119
+ const parseStepFlags = (argv, positional) => gen(function* () {
365120
+ const cost = yield* parseCostFlag(argv);
365121
+ const model = yield* parseModelFlag(argv);
365122
+ if (cost !== void 0 && positional !== "step") return yield* fail$2(/* @__PURE__ */ new Error("gtd: --cost is only valid for `gtd step`"));
365123
+ if (model !== void 0 && positional !== "step") return yield* fail$2(/* @__PURE__ */ new Error("gtd: --model is only valid for `gtd step`"));
365124
+ if (model !== void 0 && cost === void 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd: --model requires --cost — it tags the recorded cost with the model that ran"));
365125
+ return {
365126
+ cost,
365127
+ model
365128
+ };
365129
+ });
365010
365130
  /** Rejects extra positional arguments for a subcommand that takes none (`status`, `run`). */
365011
365131
  const rejectExtraArgs = (command, argv) => {
365012
365132
  const args = commandArgs(argv);
@@ -365055,6 +365175,8 @@ const resolveRestContext = (git) => gen(function* () {
365055
365175
  context: yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars, rest.stateDef.on)
365056
365176
  };
365057
365177
  });
365178
+ /** The user-facing message for a `step` refusal — out-of-turn names the awaited actor, no-match names every declared pattern. */
365179
+ const formatStepRefusal = (invoker, refusal) => refusal.reason === "out-of-turn" ? `gtd step ${invoker}: out of turn — "${refusal.state}" awaits ${refusal.awaits}` : `gtd step ${invoker}: no declared pattern matches the pending changes at "${refusal.state}" — declared patterns: ${refusal.patterns.length > 0 ? refusal.patterns.join(", ") : "(none)"}`;
365058
365180
  /**
365059
365181
  * Authenticate `invoker` against the resolved rest and perform the one
365060
365182
  * resulting transition (commit or squash), shared by `gtd step` and
@@ -365063,7 +365185,7 @@ const resolveRestContext = (git) => gen(function* () {
365063
365185
  * `subject: null` rather than failing (exit zero, per the plan's "clean
365064
365186
  * no-op exits zero").
365065
365187
  */
365066
- const stepAsActor = (invoker) => gen(function* () {
365188
+ const stepAsActor = (invoker, cost, model) => gen(function* () {
365067
365189
  const git = yield* GitService;
365068
365190
  const config = yield* ConfigService;
365069
365191
  const worktree = yield* WorktreeReader;
@@ -365075,56 +365197,166 @@ const stepAsActor = (invoker) => gen(function* () {
365075
365197
  changes,
365076
365198
  processTrace: run.trace
365077
365199
  });
365078
- if (decision.kind === "refusal") {
365079
- const message = decision.reason === "out-of-turn" ? `gtd step ${invoker}: out of turn — "${decision.state}" awaits ${decision.awaits}` : `gtd step ${invoker}: no declared pattern matches the pending changes at "${decision.state}" — declared patterns: ${decision.patterns.length > 0 ? decision.patterns.join(", ") : "(none)"}`;
365080
- return yield* fail$2(new Error(message));
365081
- }
365200
+ if (decision.kind === "refusal") return yield* fail$2(new Error(formatStepRefusal(invoker, decision)));
365082
365201
  if (decision.kind === "noop") return {
365083
365202
  state: decision.state,
365084
- subject: null
365203
+ subject: null,
365204
+ cost: null,
365205
+ model: null
365085
365206
  };
365086
365207
  const executable = decision;
365087
365208
  const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
365088
365209
  const renderedState = decision.kind === "squash" ? decision.state : rest.state;
365089
- const outcome = yield* executeDecision(git, run, executable, yield* buildTemplateContext(git, worktree.read, renderedState, invoker, run, vars, rest.def.states[renderedState]?.on));
365210
+ const context = yield* buildTemplateContext(git, worktree.read, renderedState, invoker, run, vars, rest.def.states[renderedState]?.on, cost ?? 0, model);
365211
+ yield* enforceSteeringGate(worktree.read, invoker, rest, context, decision.kind);
365212
+ const outcome = yield* executeDecision(git, run, executable, context, cost, model);
365090
365213
  return {
365091
365214
  state: rest.state,
365092
- subject: outcome.kind === "noop" ? null : outcome.subject
365215
+ subject: outcome.kind === "noop" ? null : outcome.subject,
365216
+ cost: cost ?? null,
365217
+ model: model ?? null
365093
365218
  };
365094
365219
  });
365095
365220
  /** Renders `stepAsActor`'s result the same way for both `gtd step` and `gtd run`. */
365096
365221
  const reportStepResult = (result, json, write) => {
365097
365222
  if (json) write(JSON.stringify({
365098
365223
  state: result.state,
365099
- subject: result.subject
365224
+ subject: result.subject,
365225
+ ...result.cost !== null ? { cost: result.cost } : {},
365226
+ ...result.model !== null ? { model: result.model } : {}
365100
365227
  }) + "\n");
365101
365228
  else write(result.subject !== null ? `committed: ${result.subject}\n` : `nothing to do at "${result.state}"\n`);
365102
365229
  };
365103
- /** `gtd step <actor>`: authenticate as `<actor>` and perform the one resulting transition. */
365104
- const runStepCommand = (argv, json, write) => gen(function* () {
365230
+ /** `gtd step <actor> [--cost=<n>] [--model=<name>]`: authenticate as `<actor>` and perform the one resulting transition, recording `--cost`/`--model` as a `Gtd-Cost:` trailer. */
365231
+ const runStepCommand = (argv, json, write, cost, model) => gen(function* () {
365105
365232
  const args = commandArgs(argv);
365106
365233
  if (args.length === 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd step: missing actor argument"));
365107
365234
  if (args.length > 1) return yield* fail$2(/* @__PURE__ */ new Error(`gtd step: too many arguments — expected one actor, got: ${args.join(", ")}`));
365108
- const result = yield* stepAsActor(args[0]);
365235
+ const result = yield* stepAsActor(args[0], cost, model);
365109
365236
  reportStepResult(result, json, write);
365110
365237
  });
365238
+ /**
365239
+ * The self-validation instruction gtd APPENDS to a `prompt` rest that declares
365240
+ * both `file:` and `mode:` — i.e. a state whose actor hands over a steering
365241
+ * file `gtd validate` formats and checks. Appended ONLY to plain `gtd next`
365242
+ * output (for a human or a simple driver who reads the prompt and hands it to
365243
+ * an agent, so the agent self-validates); withheld from `gtd next --json`,
365244
+ * where the driving loop instead runs `gtd validate` after the turn and
365245
+ * re-prompts on findings (see `bin/gtd-loop` / `skills/loop/SKILL.md`). This is
365246
+ * advisory: `gtd step` runs the same format-and-validate gate and REFUSES a
365247
+ * turn whose steering file is invalid (see `stepAsActor`), so a malformed file
365248
+ * is never captured whether or not this instruction was followed.
365249
+ */
365250
+ const selfValidateInstruction = (file) => `\nBefore finishing your turn, run \`gtd validate\` — it formats and checks ${file} — and fix every violation it reports until it exits cleanly. Do not finish while it still reports violations.\n`;
365251
+ /** True when a rendered rest is a `prompt` turn that hands over a validatable steering file (`file:`+`mode:` both declared). */
365252
+ const emitsValidatablePrompt = (rendered) => rendered.kind === "prompt" && rendered.file !== void 0 && rendered.mode !== void 0;
365111
365253
  /** `gtd next [--json]`: pure emitter of the resolved rest's rendered content (no mutation). */
365254
+ /** `gtd next --json`'s single-line object — omitting each optional key (never `null`-valued) when its source is unset, exactly like `gtd status --json`. */
365255
+ const nextJsonOutput = (rendered) => JSON.stringify({
365256
+ state: rendered.state,
365257
+ actor: rendered.actor,
365258
+ kind: rendered.kind,
365259
+ content: rendered.content,
365260
+ ...rendered.model !== void 0 ? { model: rendered.model } : {},
365261
+ ...rendered.memory !== void 0 ? { memory: rendered.memory } : {},
365262
+ ...rendered.file !== void 0 ? { file: rendered.file } : {},
365263
+ ...rendered.mode !== void 0 ? { mode: rendered.mode } : {},
365264
+ ...rendered.edges.length > 0 ? { edges: rendered.edges } : {}
365265
+ }) + "\n";
365266
+ /** `gtd next`'s plain-text output: the rendered content (newline-terminated), plus the self-validation instruction when the rest is a validatable prompt (see `emitsValidatablePrompt`). */
365267
+ const nextPlainOutput = (rendered) => {
365268
+ const base = rendered.content.endsWith("\n") ? rendered.content : rendered.content + "\n";
365269
+ return emitsValidatablePrompt(rendered) ? base + selfValidateInstruction(rendered.file) : base;
365270
+ };
365112
365271
  const runNextCommand = (json, write) => gen(function* () {
365113
365272
  const git = yield* GitService;
365114
365273
  const { rest, context } = yield* resolveRestContext(git);
365115
365274
  const rendered = yield* renderRest(rest, context);
365116
- if (json) write(JSON.stringify({
365117
- state: rendered.state,
365118
- actor: rendered.actor,
365119
- kind: rendered.kind,
365120
- content: rendered.content,
365121
- ...rendered.model !== void 0 ? { model: rendered.model } : {},
365122
- ...rendered.memory !== void 0 ? { memory: rendered.memory } : {},
365123
- ...rendered.file !== void 0 ? { file: rendered.file } : {},
365124
- ...rendered.mode !== void 0 ? { mode: rendered.mode } : {},
365125
- ...rendered.edges.length > 0 ? { edges: rendered.edges } : {}
365126
- }) + "\n");
365127
- else write(rendered.content.endsWith("\n") ? rendered.content : rendered.content + "\n");
365275
+ write(json ? nextJsonOutput(rendered) : nextPlainOutput(rendered));
365276
+ });
365277
+ /** The parser findings for a steering file's content, dispatched on its `mode:`. */
365278
+ const validationErrorsFor = (mode, content) => mode === "qa" ? parseOpenQuestions(content).errors : parseReviewDoc(content).errors;
365279
+ const MARKDOWN_STEERING_RE = /\.(?:md|markdown)$/i;
365280
+ /**
365281
+ * Format the resolved rest's steering file in place, then validate its
365282
+ * formatted contents. When the state declares both `file:` and `mode:` and
365283
+ * that file exists in the working tree, it is formatted (markdown only) and
365284
+ * parsed against the `mode:` format. `present` is false — and nothing is
365285
+ * formatted or validated — when the state declares no steering file, or the
365286
+ * file is absent (e.g. `building` deleted `.gtd/TODO.md`, or a human deleted
365287
+ * `.gtd/REVIEW.md` to approve), so those flows pass cleanly. Shared by
365288
+ * `gtd validate` and the `gtd step` capture gate, so both format and check the
365289
+ * SAME way — an agent's fresh draft and a human's edit are treated alike.
365290
+ */
365291
+ const formatAndCheckSteeringFile = (worktreeRead, stateDef, context) => gen(function* () {
365292
+ const file = yield* renderFile(stateDef, context);
365293
+ const mode = stateDef.mode;
365294
+ if (file === void 0 || mode === void 0) return {
365295
+ file,
365296
+ present: false,
365297
+ errors: []
365298
+ };
365299
+ if (!(yield* (yield* FileSystem).exists(file))) return {
365300
+ file,
365301
+ present: false,
365302
+ errors: []
365303
+ };
365304
+ if (MARKDOWN_STEERING_RE.test(file)) yield* formatFile(file);
365305
+ return {
365306
+ file,
365307
+ present: true,
365308
+ errors: validationErrorsFor(mode, worktreeRead(file))
365309
+ };
365310
+ });
365311
+ /**
365312
+ * The `gtd step` capture gate: format + validate the rest state's steering file
365313
+ * (see `formatAndCheckSteeringFile`) and FAIL with the findings when it is
365314
+ * invalid, so a malformed steering file — an agent's draft or a human's edit —
365315
+ * is never committed. A no-op when there is nothing to validate.
365316
+ */
365317
+ const enforceSteeringGate = (worktreeRead, invoker, rest, context, kind) => gen(function* () {
365318
+ if (kind !== "commit") return;
365319
+ const gate = yield* formatAndCheckSteeringFile(worktreeRead, rest.stateDef, context);
365320
+ if (gate.errors.length > 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd step ${invoker}: ${gate.file} is not valid at "${rest.state}" — fix these before stepping:\n${gate.errors.map((e) => ` - ${e}`).join("\n")}`));
365321
+ });
365322
+ /**
365323
+ * `gtd validate [--json]`: format (in place) then validate the steering file
365324
+ * the resolved rest declares (`file:` rendered, `mode:` selecting the parser),
365325
+ * over its WORKING-TREE contents. A state with no `file:`/`mode:`, or an absent
365326
+ * file, has nothing to validate (exit 0). A clean parse exits 0; violations
365327
+ * FAIL the Effect with the findings (one per line), so the process exits
365328
+ * non-zero — the signal a producing agent (or the driver) loops on until the
365329
+ * file is valid. Reuses the canonical `OpenQuestions`/`ReviewDoc` parsers (the
365330
+ * same the LSP publishes), so there is one source of truth per format and no
365331
+ * bash port.
365332
+ */
365333
+ const runValidateCommand = (argv, json, write) => gen(function* () {
365334
+ yield* rejectExtraArgs("validate", argv);
365335
+ const git = yield* GitService;
365336
+ const worktree = yield* WorktreeReader;
365337
+ const { rest, context } = yield* resolveRestContext(git);
365338
+ const { file, present, errors } = yield* formatAndCheckSteeringFile(worktree.read, rest.stateDef, context);
365339
+ if (!present) {
365340
+ if (json) write(JSON.stringify({
365341
+ state: rest.state,
365342
+ valid: true,
365343
+ errors: []
365344
+ }) + "\n");
365345
+ else write(`nothing to validate at "${rest.state}"\n`);
365346
+ return;
365347
+ }
365348
+ if (errors.length === 0) {
365349
+ if (json) write(JSON.stringify({
365350
+ state: rest.state,
365351
+ file,
365352
+ mode: rest.stateDef.mode,
365353
+ valid: true,
365354
+ errors: []
365355
+ }) + "\n");
365356
+ else write(`${file}: valid\n`);
365357
+ return;
365358
+ }
365359
+ return yield* fail$2(/* @__PURE__ */ new Error(`${file} is not valid:\n${errors.map((e) => ` - ${e}`).join("\n")}`));
365128
365360
  });
365129
365361
  /**
365130
365362
  * `gtd run`: the built-in driver for a `script`-content rest. Renders the
@@ -365164,8 +365396,21 @@ const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
365164
365396
  pattern: matchedRow?.[0] ?? null
365165
365397
  };
365166
365398
  });
365167
- /** `gtd status --json`'s emission — `{state, actor, changes, model?, memory?, file?, mode?}`. */
365168
- const writeStatusJson = (write, rest, statusChanges, model, memory, file) => {
365399
+ /**
365400
+ * True when the per-model breakdown adds information beyond the `Cost:` total:
365401
+ * more than one model, or a single model that carries an actual `--model` tag
365402
+ * (a lone `unspecified` bucket just restates the total, so it's suppressed).
365403
+ */
365404
+ const breakdownIsInformative = (byModel) => byModel.length > 1 || byModel.length === 1 && byModel[0].model !== "unspecified";
365405
+ /** The plain-text `Cost:` line(s) for `gtd status` — empty when nothing recorded, plus an indented per-model split when informative. */
365406
+ const costStatusLines = (cost, byModel) => {
365407
+ if (cost <= 0) return [];
365408
+ const lines = [`Cost: ${cost}`];
365409
+ if (breakdownIsInformative(byModel)) for (const m of byModel) lines.push(` ${m.model}: ${m.cost}`);
365410
+ return lines;
365411
+ };
365412
+ /** `gtd status --json`'s emission — `{state, actor, changes, model?, memory?, file?, mode?, cost?, costByModel?, edges?}`. */
365413
+ const writeStatusJson = (write, rest, statusChanges, model, memory, file, cost, costByModel) => {
365169
365414
  const edges = toTemplateEdges(rest.stateDef.on);
365170
365415
  write(JSON.stringify({
365171
365416
  state: rest.state,
@@ -365175,16 +365420,19 @@ const writeStatusJson = (write, rest, statusChanges, model, memory, file) => {
365175
365420
  ...memory !== void 0 ? { memory } : {},
365176
365421
  ...file !== void 0 ? { file } : {},
365177
365422
  ...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {},
365423
+ ...cost > 0 ? { cost } : {},
365424
+ ...cost > 0 ? { costByModel } : {},
365178
365425
  ...edges.length > 0 ? { edges } : {}
365179
365426
  }) + "\n");
365180
365427
  };
365181
- /** `gtd status`'s plain-text emission — `State:`/`Awaits:`/`Model:`/`Memory:`/`File:`/`Mode:`/`Pending:` lines. */
365182
- const writeStatusPlain = (write, rest, statusChanges, model, memory, file) => {
365428
+ /** `gtd status`'s plain-text emission — `State:`/`Awaits:`/`Model:`/`Memory:`/`File:`/`Mode:`/`Cost:`/`Pending:` lines. */
365429
+ const writeStatusPlain = (write, rest, statusChanges, model, memory, file, cost, costByModel) => {
365183
365430
  const lines = [`State: ${rest.state}`, `Awaits: ${rest.actor}`];
365184
365431
  if (model !== void 0) lines.push(`Model: ${model}`);
365185
365432
  if (memory !== void 0) lines.push(`Memory: ${memory}`);
365186
365433
  if (file !== void 0) lines.push(`File: ${file}`);
365187
365434
  if (rest.stateDef.mode !== void 0) lines.push(`Mode: ${rest.stateDef.mode}`);
365435
+ lines.push(...costStatusLines(cost, costByModel));
365188
365436
  if (statusChanges.length === 0) lines.push("Pending: (clean)");
365189
365437
  else {
365190
365438
  lines.push("Pending:");
@@ -365202,8 +365450,8 @@ const runStatusCommand = (argv, json, write) => gen(function* () {
365202
365450
  const memory = yield* renderMemory(rest.stateDef, context);
365203
365451
  const file = yield* renderFile(rest.stateDef, context);
365204
365452
  const statusChanges = computeStatusChanges(rest.stateDef.on ?? [], changes);
365205
- if (json) writeStatusJson(write, rest, statusChanges, model, memory, file);
365206
- else writeStatusPlain(write, rest, statusChanges, model, memory, file);
365453
+ if (json) writeStatusJson(write, rest, statusChanges, model, memory, file, context.processCost, context.processCostByModel);
365454
+ else writeStatusPlain(write, rest, statusChanges, model, memory, file, context.processCost, context.processCostByModel);
365207
365455
  });
365208
365456
  /**
365209
365457
  * `gtd mermaid`: pure emitter of the active workflow's SHAPE (not the
@@ -365225,6 +365473,7 @@ const KNOWN_SUBCOMMANDS = [
365225
365473
  "next",
365226
365474
  "run",
365227
365475
  "status",
365476
+ "validate",
365228
365477
  "mermaid"
365229
365478
  ];
365230
365479
  /**
@@ -365269,12 +365518,13 @@ const assertRunningFromRepoRoot = (git, fs) => gen(function* () {
365269
365518
  if ((yield* fs.realPath(topLevel)) !== (yield* fs.realPath(process.cwd()))) return yield* fail$2(/* @__PURE__ */ new Error(`gtd must be run from the repository root (${topLevel}); the current directory is ${process.cwd()}`));
365270
365519
  });
365271
365520
  /** Dispatches to the named `run*Command` handler for every known subcommand. */
365272
- const dispatchKnownSubcommand = (sub, argv, json, write) => {
365521
+ const dispatchKnownSubcommand = (sub, argv, json, write, cost, model) => {
365273
365522
  switch (sub) {
365274
- case "step": return runStepCommand(argv, json, write);
365523
+ case "step": return runStepCommand(argv, json, write, cost, model);
365275
365524
  case "next": return runNextCommand(json, write);
365276
365525
  case "run": return runRunCommand(argv, json, write);
365277
365526
  case "status": return runStatusCommand(argv, json, write);
365527
+ case "validate": return runValidateCommand(argv, json, write);
365278
365528
  case "mermaid": return runMermaidCommand(argv, json, write);
365279
365529
  }
365280
365530
  };
@@ -365299,8 +365549,9 @@ function makeProgram(opts = {}) {
365299
365549
  const positional = argv.slice(2).find((a) => !a.startsWith("--"));
365300
365550
  return gen(function* () {
365301
365551
  if (runVersionOrHelp(argv, write)) return;
365302
- const unknownOption = argv.slice(2).find((a) => a.startsWith("--") && a !== "--json");
365552
+ const unknownOption = argv.slice(2).find((a) => a.startsWith("--") && a !== "--json" && a !== "--cost" && a !== "--model" && !a.startsWith(COST_FLAG) && !a.startsWith(MODEL_FLAG));
365303
365553
  if (unknownOption !== void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd: unknown option '${unknownOption}' — see \`gtd --help\``));
365554
+ const { cost, model } = yield* parseStepFlags(argv, positional);
365304
365555
  if (positional === "format") return yield* runFormatCommand(argv, json);
365305
365556
  if (positional === "lsp") return yield* runLspCommand(argv, json);
365306
365557
  const sub = yield* requireKnownSubcommand(positional, json, write);
@@ -365309,7 +365560,7 @@ function makeProgram(opts = {}) {
365309
365560
  yield* assertRunningFromRepoRoot(git, fs);
365310
365561
  yield* closeReviewWindow;
365311
365562
  yield* (yield* ConfigInit).ensure;
365312
- const outcome = yield* either(dispatchKnownSubcommand(sub, argv, json, write));
365563
+ const outcome = yield* either(dispatchKnownSubcommand(sub, argv, json, write, cost, model));
365313
365564
  const rearm = yield* either(openReviewWindow);
365314
365565
  if (isLeft(outcome)) return yield* fail$2(outcome.left);
365315
365566
  if (isLeft(rearm)) return yield* fail$2(rearm.left);