@pmelab/gtd 3.5.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.
- package/bin/gtd-loop +58 -25
- package/dist/gtd.bundle.mjs +138 -26
- package/docs/cli.md +45 -5
- package/docs/design/state-file-association.md +9 -8
- package/docs/design/state-machine-validation.md +2 -2
- package/docs/design/steering-file-loops.md +11 -0
- package/docs/design/steering-file-validation-command.md +386 -0
- package/docs/development.md +8 -6
- package/docs/upgrading.md +12 -9
- package/package.json +1 -1
package/bin/gtd-loop
CHANGED
|
@@ -7,9 +7,10 @@ set -euo pipefail
|
|
|
7
7
|
# - "script" (a check rest) -> `gtd run` executes it and steps the check
|
|
8
8
|
# actor in one command; zero new commits = green with nothing owed (the
|
|
9
9
|
# terminal signal at idle).
|
|
10
|
-
# - "prompt" (an agent rest) -> feed `content` to the agent,
|
|
11
|
-
# `gtd
|
|
12
|
-
#
|
|
10
|
+
# - "prompt" (an agent rest) -> feed `content` to the agent, run the
|
|
11
|
+
# self-validation gate (`gtd validate`, re-prompting on findings — see the
|
|
12
|
+
# gate's own comment below), then `gtd step <actor>` ourselves — the
|
|
13
|
+
# harness owns ending the turn, not the prompt text.
|
|
13
14
|
# `.model` (optional, opaque) is exported as $GTD_LOOP_MODEL for the agent
|
|
14
15
|
# adapter invoked at a "prompt" rest; the default adapter appends `--model
|
|
15
16
|
# "$GTD_LOOP_MODEL"` only when it's non-empty.
|
|
@@ -34,6 +35,34 @@ prev_content=""
|
|
|
34
35
|
# across iterations and across restarts. In the git dir, never the work tree.
|
|
35
36
|
memory_marker="$(git rev-parse --git-dir 2>/dev/null || echo .git)/gtd-loop-memory"
|
|
36
37
|
|
|
38
|
+
# Swappable agent adapter: GTD_LOOP_AGENT_CMD lets any coding agent CLI stand
|
|
39
|
+
# in for the default, receiving the prompt via $GTD_LOOP_PROMPT, the state's
|
|
40
|
+
# opaque model hint (if any) via $GTD_LOOP_MODEL, and the memory scope via
|
|
41
|
+
# $GTD_LOOP_MEMORY / $GTD_LOOP_SESSION_ID / $GTD_LOOP_MEMORY_RESUME ("1" when
|
|
42
|
+
# this turn should continue the prior session). An adapter that ignores any of
|
|
43
|
+
# these keeps working — they're exported alongside $GTD_LOOP_PROMPT, never
|
|
44
|
+
# required. The default adapter maps the memory signal onto claude's own
|
|
45
|
+
# session flags: --resume to continue the scope's session, --session-id to pin
|
|
46
|
+
# a fresh one so the next same-scope turn can resume it. $1 is the prompt, $2
|
|
47
|
+
# the resume flag; the memory scope ($memory/$session_id) is read from the
|
|
48
|
+
# enclosing iteration, so a same-turn re-prompt (the validate gate) resumes the
|
|
49
|
+
# same session.
|
|
50
|
+
run_agent_turn() {
|
|
51
|
+
local prompt="$1" do_resume="$2" cmd="${GTD_LOOP_AGENT_CMD:-}"
|
|
52
|
+
if [[ -z "$cmd" ]]; then
|
|
53
|
+
cmd='claude -p "$GTD_LOOP_PROMPT"'
|
|
54
|
+
[[ -n "$model" ]] && cmd="$cmd"' --model "$GTD_LOOP_MODEL"'
|
|
55
|
+
if [[ "$do_resume" == "1" ]]; then
|
|
56
|
+
cmd="$cmd"' --resume "$GTD_LOOP_SESSION_ID"'
|
|
57
|
+
elif [[ -n "$session_id" ]]; then
|
|
58
|
+
cmd="$cmd"' --session-id "$GTD_LOOP_SESSION_ID"'
|
|
59
|
+
fi
|
|
60
|
+
cmd="$cmd"' --dangerously-skip-permissions'
|
|
61
|
+
fi
|
|
62
|
+
GTD_LOOP_PROMPT="$prompt" GTD_LOOP_MODEL="$model" GTD_LOOP_MEMORY="$memory" \
|
|
63
|
+
GTD_LOOP_SESSION_ID="$session_id" GTD_LOOP_MEMORY_RESUME="$do_resume" bash -c "$cmd"
|
|
64
|
+
}
|
|
65
|
+
|
|
37
66
|
while true; do
|
|
38
67
|
if ! next_json="$(gtd next --json 2>&1)"; then
|
|
39
68
|
echo "gtd-loop: could not determine the next step:" >&2
|
|
@@ -104,28 +133,32 @@ while true; do
|
|
|
104
133
|
rm -f "$memory_marker"
|
|
105
134
|
fi
|
|
106
135
|
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
136
|
+
# Run the agent turn (see run_agent_turn's own comment for the adapter
|
|
137
|
+
# contract). Then the self-validation gate: a producing state may declare a
|
|
138
|
+
# `file:`/`mode:` pair (e.g. the default workflow's grilling/reviewing),
|
|
139
|
+
# making its output's format checkable. `gtd validate` is a no-op (exit 0)
|
|
140
|
+
# when the resolved rest has nothing to validate, so run it after EVERY agent
|
|
141
|
+
# turn; on findings, re-prompt the SAME agent session to fix them (up to a
|
|
142
|
+
# cap) before stepping — the plain-`gtd next` self-validation instruction's
|
|
143
|
+
# driver-side equivalent, so the next rest is only ever handed a well-formed
|
|
144
|
+
# file. gtd itself never validates at step time — this is the loop's job.
|
|
145
|
+
run_agent_turn "$content" "$resume"
|
|
146
|
+
|
|
147
|
+
fix_attempt=0
|
|
148
|
+
while ! validate_out="$(gtd validate 2>&1)"; do
|
|
149
|
+
fix_attempt=$((fix_attempt + 1))
|
|
150
|
+
if ((fix_attempt > 3)); then
|
|
151
|
+
echo "gtd-loop: '$state' still fails \`gtd validate\` after 3 fix attempts:" >&2
|
|
152
|
+
echo "$validate_out" >&2
|
|
153
|
+
echo "Stopping rather than stepping with a malformed steering file." >&2
|
|
154
|
+
exit 1
|
|
125
155
|
fi
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
156
|
+
# Resume the same session when there is one (the agent just wrote the file);
|
|
157
|
+
# otherwise a fresh invocation still gets the findings via the prompt.
|
|
158
|
+
fix_resume=0
|
|
159
|
+
[[ -n "$session_id" ]] && fix_resume=1
|
|
160
|
+
run_agent_turn "$content"$'\n\n'"Your last turn does not pass \`gtd validate\`. Fix these format violations, then finish:"$'\n'"$validate_out" "$fix_resume"
|
|
161
|
+
done
|
|
162
|
+
|
|
130
163
|
gtd step "$actor" >/dev/null
|
|
131
164
|
done
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -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;
|
|
@@ -364396,8 +364396,8 @@ const parseQuestionBlock = (block) => {
|
|
|
364396
364396
|
* always returns a result, never throws. `errors` is non-empty exactly when
|
|
364397
364397
|
* the document violates the required structure (a `###` question under
|
|
364398
364398
|
* `## Open Questions` with no recognized response line) — the caller decides
|
|
364399
|
-
* what to do with that (
|
|
364400
|
-
*
|
|
364399
|
+
* what to do with that (`gtd validate` exits non-zero with them; the LSP
|
|
364400
|
+
* publishes them as diagnostics).
|
|
364401
364401
|
*/
|
|
364402
364402
|
const parseOpenQuestions = (content) => {
|
|
364403
364403
|
const lines = content.split(/\r?\n/);
|
|
@@ -364517,9 +364517,8 @@ const parseChangesets = (lines) => {
|
|
|
364517
364517
|
* Parses the review structure out of `content` (the raw text of
|
|
364518
364518
|
* `.gtd/REVIEW.md`). Total and side-effect-free: always returns a result,
|
|
364519
364519
|
* never throws. `errors` is non-empty exactly when the document violates the
|
|
364520
|
-
* required structure — the caller decides what to do with that (
|
|
364521
|
-
*
|
|
364522
|
-
* 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).
|
|
364523
364522
|
*/
|
|
364524
364523
|
const parseReviewDoc = (content) => {
|
|
364525
364524
|
const lines = content.split(/\r?\n/);
|
|
@@ -364544,10 +364543,10 @@ const parseReviewDoc = (content) => {
|
|
|
364544
364543
|
* LSP server for `.gtd/` steering files — document symbols for a `qa`-mode
|
|
364545
364544
|
* file's open questions and a `review`-mode file's review chunks/hunks, code
|
|
364546
364545
|
* actions to check/uncheck a hunk or a whole chunk, and diagnostics publishing
|
|
364547
|
-
* the same parsers' `errors` the
|
|
364548
|
-
*
|
|
364549
|
-
*
|
|
364550
|
-
*
|
|
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).
|
|
364551
364550
|
*
|
|
364552
364551
|
* CONFIG-DRIVEN (see `docs/design/state-file-association.md` §3): the server
|
|
364553
364552
|
* locates the active gtd config the SAME way the CLI does (`ConfigService`'s
|
|
@@ -364613,7 +364612,7 @@ const questionSymbols = (content) => {
|
|
|
364613
364612
|
};
|
|
364614
364613
|
});
|
|
364615
364614
|
};
|
|
364616
|
-
/** Diagnostics for `.gtd/TODO.md` — the same findings
|
|
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. */
|
|
364617
364616
|
const questionDiagnostics = (content) => {
|
|
364618
364617
|
const { errors } = parseOpenQuestions(content);
|
|
364619
364618
|
const lines = content.split(/\r?\n/);
|
|
@@ -364653,7 +364652,7 @@ const reviewSymbols = (content) => {
|
|
|
364653
364652
|
};
|
|
364654
364653
|
});
|
|
364655
364654
|
};
|
|
364656
|
-
/** Diagnostics for `.gtd/REVIEW.md` — the same findings
|
|
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. */
|
|
364657
364656
|
const reviewDiagnostics = (content) => {
|
|
364658
364657
|
const { errors } = parseReviewDoc(content);
|
|
364659
364658
|
const lines = content.split(/\r?\n/);
|
|
@@ -364982,7 +364981,7 @@ const startLspServer = () => gen(function* () {
|
|
|
364982
364981
|
const escapeLabel = (text) => text.replace(/"/g, "'").replace(/\r?\n/g, " ");
|
|
364983
364982
|
/**
|
|
364984
364983
|
* A Mermaid-safe node id for one state name: every non-word character folds
|
|
364985
|
-
* to `_` (state names may carry hyphens — `
|
|
364984
|
+
* to `_` (state names may carry hyphens — `grilling-answer`,
|
|
364986
364985
|
* `review-deciding` — which Mermaid's bare identifier grammar doesn't
|
|
364987
364986
|
* guarantee), and a leading digit gets an `s_` prefix (Mermaid ids can't
|
|
364988
364987
|
* start with one). The exact original name still reaches the diagram via
|
|
@@ -365048,6 +365047,10 @@ Commands:
|
|
|
365048
365047
|
actor (the built-in script driver)
|
|
365049
365048
|
status Print the resolved rest's state/actor and which declared
|
|
365050
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)
|
|
365051
365054
|
mermaid Print the active workflow's shape as Mermaid
|
|
365052
365055
|
stateDiagram-v2 source (no mutation)
|
|
365053
365056
|
format <file> Format a markdown file in place
|
|
@@ -365204,7 +365207,9 @@ const stepAsActor = (invoker, cost, model) => gen(function* () {
|
|
|
365204
365207
|
const executable = decision;
|
|
365205
365208
|
const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
|
|
365206
365209
|
const renderedState = decision.kind === "squash" ? decision.state : rest.state;
|
|
365207
|
-
const
|
|
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);
|
|
365208
365213
|
return {
|
|
365209
365214
|
state: rest.state,
|
|
365210
365215
|
subject: outcome.kind === "noop" ? null : outcome.subject,
|
|
@@ -365230,23 +365235,128 @@ const runStepCommand = (argv, json, write, cost, model) => gen(function* () {
|
|
|
365230
365235
|
const result = yield* stepAsActor(args[0], cost, model);
|
|
365231
365236
|
reportStepResult(result, json, write);
|
|
365232
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;
|
|
365233
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
|
+
};
|
|
365234
365271
|
const runNextCommand = (json, write) => gen(function* () {
|
|
365235
365272
|
const git = yield* GitService;
|
|
365236
365273
|
const { rest, context } = yield* resolveRestContext(git);
|
|
365237
365274
|
const rendered = yield* renderRest(rest, context);
|
|
365238
|
-
|
|
365239
|
-
|
|
365240
|
-
|
|
365241
|
-
|
|
365242
|
-
|
|
365243
|
-
|
|
365244
|
-
|
|
365245
|
-
|
|
365246
|
-
|
|
365247
|
-
|
|
365248
|
-
|
|
365249
|
-
|
|
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")}`));
|
|
365250
365360
|
});
|
|
365251
365361
|
/**
|
|
365252
365362
|
* `gtd run`: the built-in driver for a `script`-content rest. Renders the
|
|
@@ -365363,6 +365473,7 @@ const KNOWN_SUBCOMMANDS = [
|
|
|
365363
365473
|
"next",
|
|
365364
365474
|
"run",
|
|
365365
365475
|
"status",
|
|
365476
|
+
"validate",
|
|
365366
365477
|
"mermaid"
|
|
365367
365478
|
];
|
|
365368
365479
|
/**
|
|
@@ -365413,6 +365524,7 @@ const dispatchKnownSubcommand = (sub, argv, json, write, cost, model) => {
|
|
|
365413
365524
|
case "next": return runNextCommand(json, write);
|
|
365414
365525
|
case "run": return runRunCommand(argv, json, write);
|
|
365415
365526
|
case "status": return runStatusCommand(argv, json, write);
|
|
365527
|
+
case "validate": return runValidateCommand(argv, json, write);
|
|
365416
365528
|
case "mermaid": return runMermaidCommand(argv, json, write);
|
|
365417
365529
|
}
|
|
365418
365530
|
};
|
package/docs/cli.md
CHANGED
|
@@ -16,6 +16,10 @@ Commands:
|
|
|
16
16
|
actor (the built-in script driver)
|
|
17
17
|
status Print the resolved rest's state/actor and which declared
|
|
18
18
|
pattern (if any) each pending change matches (no mutation)
|
|
19
|
+
validate Validate the steering file the resolved rest declares
|
|
20
|
+
(its file:/mode:) against that mode's format; exits
|
|
21
|
+
non-zero with findings when the file violates it (no
|
|
22
|
+
mutation)
|
|
19
23
|
mermaid Print the active workflow's shape as Mermaid
|
|
20
24
|
stateDiagram-v2 source (no mutation)
|
|
21
25
|
format <file> Format a markdown file in place
|
|
@@ -226,6 +230,43 @@ when the state has no `on` (all omitted entirely, never `null`, otherwise):
|
|
|
226
230
|
|
|
227
231
|
`gtd status` takes no arguments — extra positional args are rejected.
|
|
228
232
|
|
|
233
|
+
## `gtd validate [--json]`
|
|
234
|
+
|
|
235
|
+
Format **and** validate the steering file the resolved rest declares. It
|
|
236
|
+
resolves the current state exactly like `gtd status`, renders that state's
|
|
237
|
+
`file:`, **formats that file in place** (the same markdown formatter as
|
|
238
|
+
`gtd format`), then runs the parser its `mode:` selects over the formatted
|
|
239
|
+
contents — `qa` → `src/OpenQuestions.ts`, `review` → `src/ReviewDoc.ts` (the
|
|
240
|
+
same pure parsers the LSP publishes as diagnostics, so there is one source of
|
|
241
|
+
truth per format).
|
|
242
|
+
|
|
243
|
+
- A well-formed file exits `0` (`<file>: valid`, or `{"valid":true, ...}` with
|
|
244
|
+
`--json`) — and is left formatted.
|
|
245
|
+
- Violations exit **non-zero** with the findings, one per line — the signal a
|
|
246
|
+
producing agent, or the driving loop, loops on until the file is well-formed.
|
|
247
|
+
- A state that declares no `file:`/`mode:`, or whose file is **absent** (e.g.
|
|
248
|
+
`building` deleted `.gtd/TODO.md`, or a human deleted `.gtd/REVIEW.md` to
|
|
249
|
+
approve), has nothing to validate and exits `0`
|
|
250
|
+
(`nothing to validate at "<state>"`). Nothing is formatted in that case.
|
|
251
|
+
|
|
252
|
+
This is how the bundled default keeps its steering files tidy and well-formed
|
|
253
|
+
without an in-machine validation state — and the check runs whoever last touched
|
|
254
|
+
the file, agent or human:
|
|
255
|
+
|
|
256
|
+
- **The producing agent self-validates.** Plain `gtd next` appends a "run
|
|
257
|
+
`gtd validate` and fix all violations" instruction to a `prompt` rest that
|
|
258
|
+
declares `file:`+`mode:`; `gtd next --json` withholds it and the driving loop
|
|
259
|
+
runs `gtd validate` after the turn instead (see `bin/gtd-loop` /
|
|
260
|
+
[STATES.md §12](../STATES.md#12-steering-file-validation-gtd-validate)).
|
|
261
|
+
- **`gtd step` enforces the same gate.** Capturing a turn out of a state that
|
|
262
|
+
declares `file:`+`mode:` formats that file in place and validates it first,
|
|
263
|
+
and **refuses the step** (committing nothing) when it is invalid — so a
|
|
264
|
+
human's edit at a gate (answering at `grilling-answer`, reviewing at
|
|
265
|
+
`await-review`) is formatted and checked exactly like an agent's draft, and a
|
|
266
|
+
malformed steering file is never committed.
|
|
267
|
+
|
|
268
|
+
`gtd validate` takes no arguments.
|
|
269
|
+
|
|
229
270
|
## `gtd mermaid`
|
|
230
271
|
|
|
231
272
|
Pure emitter of the active workflow's **shape** — not the resolved rest — as
|
|
@@ -260,7 +301,7 @@ Mermaid-aware renderer (GitHub, GitLab, VS Code, Obsidian, the
|
|
|
260
301
|
|
|
261
302
|
State names are aliased to Mermaid-safe identifiers (non-word characters fold to
|
|
262
303
|
`_`; a digit-led name gets an `s_` prefix) via a `state "<name>" as <alias>`
|
|
263
|
-
declaration up front, so a hyphenated name like `
|
|
304
|
+
declaration up front, so a hyphenated name like `grilling-answer` still displays
|
|
264
305
|
with its exact declared spelling. Rejects `--json` (exit 1,
|
|
265
306
|
`gtd mermaid does not accept --json`) — there is no structured shape to emit
|
|
266
307
|
beyond the Mermaid source itself — and takes no arguments (extra positional args
|
|
@@ -288,10 +329,9 @@ Errors (all exit 1, message on stderr):
|
|
|
288
329
|
Starts an LSP server over stdio for `.gtd/` steering files — document symbols
|
|
289
330
|
for a `qa`-mode file's open questions and a `review`-mode file's review
|
|
290
331
|
chunks/hunks, code actions to check/uncheck a hunk or a whole chunk, and
|
|
291
|
-
diagnostics publishing the same parser findings
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
[STATES.md §10](../STATES.md#10-the-bundled-default-workflow)).
|
|
332
|
+
diagnostics publishing the same parser findings `gtd validate` reports (see
|
|
333
|
+
`src/OpenQuestions.ts` / `src/ReviewDoc.ts` and
|
|
334
|
+
[STATES.md §12](../STATES.md#12-steering-file-validation-gtd-validate)).
|
|
295
335
|
|
|
296
336
|
**Config-driven** (see
|
|
297
337
|
[docs/design/state-file-association.md](design/state-file-association.md)): the
|
|
@@ -45,14 +45,14 @@ vars:
|
|
|
45
45
|
feedbackFile: .gtd/FEEDBACK.md
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
Per-state mapping (
|
|
48
|
+
Per-state mapping (10 states):
|
|
49
49
|
|
|
50
|
-
| States
|
|
51
|
-
|
|
|
52
|
-
| `grilling`, `
|
|
53
|
-
| `reviewing`, `
|
|
54
|
-
| `fixing`, `escalate`
|
|
55
|
-
| `idle`, `checking`
|
|
50
|
+
| States | `file:` | `mode:` |
|
|
51
|
+
| ---------------------------------------------- | ----------------------------- | ------------------------- |
|
|
52
|
+
| `grilling`, `grilling-answer`, `building` | `<%= it.vars.todoFile %>` | `qa` |
|
|
53
|
+
| `reviewing`, `await-review`, `review-deciding` | `<%= it.vars.reviewFile %>` | `review` |
|
|
54
|
+
| `fixing`, `escalate` | `<%= it.vars.feedbackFile %>` | — (plain text, no format) |
|
|
55
|
+
| `idle`, `checking` | — | — |
|
|
56
56
|
|
|
57
57
|
Prompts and scripts that already name these files switch to the vars
|
|
58
58
|
(`<%~ it.vars.todoFile %>` etc.) so each filename has ONE source of truth in
|
|
@@ -109,4 +109,5 @@ The advanced example (`docs/examples/advanced-workflow.md`) gains the same
|
|
|
109
109
|
- Eta-rendering of `on` pattern keys (the rename-switch story) — future work,
|
|
110
110
|
noted in §2.
|
|
111
111
|
- New LSP modes beyond `qa`/`review` — the vocabulary can grow with new formats;
|
|
112
|
-
adding one means: parser (spec),
|
|
112
|
+
adding one means: parser (spec), a `gtd validate` dispatch arm for the mode,
|
|
113
|
+
LSP dispatch arm, docs.
|
|
@@ -51,7 +51,7 @@ Each of these passes today's validation:
|
|
|
51
51
|
`retry: {max: 3, otherwise: escalate}`; delete that key and the machine
|
|
52
52
|
validates clean but can ping-pong forever with no human in the loop.
|
|
53
53
|
5. **Shadowed `on` row** — `on` is first-match-wins, so
|
|
54
|
-
`{"* **": a, "A .gtd/
|
|
54
|
+
`{"* **": a, "A .gtd/FEEDBACK.md": b}` never routes to `b`: the catch-all
|
|
55
55
|
subsumes the specific row. Dead rows are silent today.
|
|
56
56
|
6. **`retry.otherwise` cycle / self-reference** — `applyRetry` terminates at
|
|
57
57
|
runtime via its `visited` guard, but the guard's "accept the repeated target
|
|
@@ -400,7 +400,7 @@ the definition) — they fit `PatternMachine.step`'s purity discipline.
|
|
|
400
400
|
- **Self-explaining refusals.** A no-match refusal already lists the state's
|
|
401
401
|
declared patterns; for a user debugging their own machine, also list the
|
|
402
402
|
pending changes that failed to match (status + path, first N). "Your tree has
|
|
403
|
-
`M src/x.ts` but this state only matches `A .gtd/
|
|
403
|
+
`M src/x.ts` but this state only matches `A .gtd/FEEDBACK.md`" turns the most
|
|
404
404
|
common custom-workflow dead end from a mystery into a diff-vs-pattern
|
|
405
405
|
diagnosis the user can act on immediately.
|
|
406
406
|
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Plan: steering-file validation loops in the default workflow
|
|
2
2
|
|
|
3
|
+
> SUPERSEDED (§2–§4): the in-machine validation-loop states this plan added —
|
|
4
|
+
> `todo-validating` and `review-validating` — and their `.gtd/FORMAT.md`
|
|
5
|
+
> steering file have all been DELETED. Deterministic format-checking now lives
|
|
6
|
+
> in the `gtd validate` command (which runs the same pure `parseOpenQuestions` /
|
|
7
|
+
> `parseReviewDoc` parsers the state's `mode:` selects) plus producing-agent
|
|
8
|
+
> self-validation, rather than in bash-port `check`/`script` states. The default
|
|
9
|
+
> workflow is now 10 states, not 12. See
|
|
10
|
+
> [docs/design/steering-file-validation-command.md](steering-file-validation-command.md)
|
|
11
|
+
> and [STATES.md §12](../STATES.md). §1 (the file formats) and §5 (the LSP
|
|
12
|
+
> resurrection) below remain valid and current.
|
|
13
|
+
|
|
3
14
|
> Status: §2–§4 (the two validation loops in the default workflow) AND §5 (the
|
|
4
15
|
> LSP resurrection) both LANDED 2026-07-22. Goal: the bundled default workflow
|
|
5
16
|
> completely maps the functionality the deleted v2 LSP server (`src/Lsp.ts`,
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Replacing the steering-file validation _states_ with a `gtd validate` command
|
|
2
|
+
|
|
3
|
+
> Status: IMPLEMENTED. The authoritative reference is now
|
|
4
|
+
> [STATES.md §12](../../STATES.md) (the model) and §10 (the 10-state bundled
|
|
5
|
+
> default); this doc is the design record. Supersedes the two validation-loop
|
|
6
|
+
> states in [steering-file-loops.md](steering-file-loops.md) §2–§4
|
|
7
|
+
> (`todo-validating`, `review-validating`); everything else that doc describes
|
|
8
|
+
> (the file formats §1, the LSP resurrection §5) stays.
|
|
9
|
+
>
|
|
10
|
+
> **As built — two decisions differ from the first draft of this plan:**
|
|
11
|
+
>
|
|
12
|
+
> 1. **No new state property.** A state is treated as "must hand over a valid
|
|
13
|
+
> file" purely by declaring both `file:` and `mode:` — there is no
|
|
14
|
+
> `validate:` flag and no `PatternMachine`/`PatternConfig`/`schema.json`
|
|
15
|
+
> change. This is the "Alternative considered" from §2.3, chosen for zero
|
|
16
|
+
> engine surface.
|
|
17
|
+
> 2. **`gtd next --json` is unchanged.** It emits no new field (it already
|
|
18
|
+
> carries `file`/`mode`); the driver simply runs `gtd validate` after every
|
|
19
|
+
> agent turn (a no-op, exit 0, when the state has nothing to validate). Only
|
|
20
|
+
> plain `gtd next` changed — it appends the self-validation instruction to a
|
|
21
|
+
> `prompt` rest that declares `file:`+`mode:`.
|
|
22
|
+
>
|
|
23
|
+
> **Follow-up refinement (also built):**
|
|
24
|
+
>
|
|
25
|
+
> 3. **`gtd validate` also formats**, and the check runs after a human edits
|
|
26
|
+
> too. `gtd validate` now formats the steering file in place (the
|
|
27
|
+
> `gtd format` markdown formatter) before parsing it. And `gtd step` runs the
|
|
28
|
+
> SAME format-and-validate as a capture gate (`enforceSteeringGate` in
|
|
29
|
+
> `src/program.ts`): committing a turn out of a state that declares
|
|
30
|
+
> `file:`+`mode:` formats+validates that file and REFUSES the step when it is
|
|
31
|
+
> invalid. That makes the check apply to a human's edit at a gate
|
|
32
|
+
> (`grilling-answer`, `await-review`) exactly as to an agent's draft, and a
|
|
33
|
+
> malformed steering file is never committed. An absent file (a deletion —
|
|
34
|
+
> `building`'s TODO.md, an `await-review` delete-to-approve) is a no-op, so
|
|
35
|
+
> those flows still pass. `gtd validate`'s earlier "missing file read as
|
|
36
|
+
> empty" behavior is dropped in favor of "absent → nothing to validate".
|
|
37
|
+
>
|
|
38
|
+
> The sections below are the original plan; read §2.1–§3.2 through these
|
|
39
|
+
> decisions (esp.: validate formats and is enforced at `gtd step`, and an absent
|
|
40
|
+
> file is a no-op rather than parsed-as-empty).
|
|
41
|
+
|
|
42
|
+
## 1. Motivation
|
|
43
|
+
|
|
44
|
+
The bundled default workflow (`src/workflows/default.yaml`) currently carries
|
|
45
|
+
two dedicated `check` states whose only job is to parse a steering file and
|
|
46
|
+
bounce a malformed draft back to its authoring agent via a `.gtd/FORMAT.md`
|
|
47
|
+
findings file:
|
|
48
|
+
|
|
49
|
+
- `todo-validating` — validates `.gtd/TODO.md`'s open-questions format after
|
|
50
|
+
`grilling`, before the human answers at `grilling-answer`.
|
|
51
|
+
- `review-validating` — validates `.gtd/REVIEW.md`'s checkbox format after
|
|
52
|
+
`reviewing`, before the human reviews at `await-review`.
|
|
53
|
+
|
|
54
|
+
This is noise in the state machine:
|
|
55
|
+
|
|
56
|
+
1. **2 of 12 states** exist only to re-derive a format check.
|
|
57
|
+
2. **A hand-synced dual implementation.** Each validator is a `grep/awk` port of
|
|
58
|
+
`src/OpenQuestions.ts` / `src/ReviewDoc.ts` — an explicit "executable spec ↔
|
|
59
|
+
bash validator" contract kept in sync _by hand_, with no shared code path. A
|
|
60
|
+
whole class of drift bugs.
|
|
61
|
+
3. **A third steering file** (`.gtd/FORMAT.md`) with its own lifecycle and
|
|
62
|
+
hygiene rules, plus "never touch FORMAT.md" / "fix FORMAT.md first" clauses
|
|
63
|
+
bolted onto the `grilling` and `reviewing` prompts.
|
|
64
|
+
4. **Two extra commits on every happy-path cycle** (`gtd(check): …` turns that
|
|
65
|
+
only say "the format was fine").
|
|
66
|
+
|
|
67
|
+
The core realization: the validation is redundant with tooling we already have.
|
|
68
|
+
`src/OpenQuestions.ts` / `src/ReviewDoc.ts` are pure parsers, and `src/Lsp.ts`
|
|
69
|
+
already publishes their `errors` as live editor diagnostics. What was missing
|
|
70
|
+
was a way for the **producing agent** to run the same canonical check itself,
|
|
71
|
+
mid-turn, so the human is only ever handed a _validated_ file.
|
|
72
|
+
|
|
73
|
+
## 2. The design
|
|
74
|
+
|
|
75
|
+
Validation stops being a **state** and becomes a **tool the producing agent uses
|
|
76
|
+
before it finishes its turn** (or that the driver runs on the agent's behalf).
|
|
77
|
+
No bash port, no `.gtd/FORMAT.md`, no extra states. The pure engine
|
|
78
|
+
(`src/PatternMachine.ts`) never validates a steering file — as today, that stays
|
|
79
|
+
out of scope for the engine (steering-file-loops.md §6). The only engine-shaped
|
|
80
|
+
addition is one declarative state property that marks _which_ states must hand
|
|
81
|
+
over a valid file (see §2.3).
|
|
82
|
+
|
|
83
|
+
Three parts:
|
|
84
|
+
|
|
85
|
+
### 2.1 A `gtd validate` command (current-state-scoped, canonical parsers)
|
|
86
|
+
|
|
87
|
+
A new subcommand that resolves the current state exactly like `gtd status`,
|
|
88
|
+
renders that state's `file:`, reads its working-tree contents, and runs the
|
|
89
|
+
parser its `mode:` selects:
|
|
90
|
+
|
|
91
|
+
- `mode: qa` → `parseOpenQuestions(content).errors`
|
|
92
|
+
- `mode: review` → `parseReviewDoc(content).errors`
|
|
93
|
+
|
|
94
|
+
Behavior:
|
|
95
|
+
|
|
96
|
+
- **Nothing to validate** — the resolved state declares no `file:` **or** no
|
|
97
|
+
`mode:` → exit 0, report "nothing to validate at \"\<state\>\"".
|
|
98
|
+
- **Valid** — the parser returns no `errors` → exit 0, report "\<file\>: valid".
|
|
99
|
+
- **Invalid** — the parser returns `errors` → **fail the Effect** with the
|
|
100
|
+
findings (one per line, each prefixed with the rendered file path), which
|
|
101
|
+
makes `main.ts` print `gtd: …` to stderr and exit non-zero. This non-zero exit
|
|
102
|
+
is the whole point: the producing agent (or the driver) loops
|
|
103
|
+
`write → gtd validate → fix` until it exits 0.
|
|
104
|
+
- A **missing** `file:` on disk is read as empty content (catch the read error,
|
|
105
|
+
treat as `""`), NOT as a hard error. For `qa` an empty/absent file is
|
|
106
|
+
trivially valid (no `## Open Questions` section); for `review` an empty file
|
|
107
|
+
fails the parser (no header, no chunks) — both are the correct outcomes.
|
|
108
|
+
Keeping validate purely a function of the parser over the file's content is
|
|
109
|
+
the crisp contract: **`gtd validate` fails ⟺ the file exists and violates its
|
|
110
|
+
format.**
|
|
111
|
+
|
|
112
|
+
`--json` shape: on success
|
|
113
|
+
`{ "state", "file"?, "mode"?, "valid": true, "errors": [] }`; on violations, the
|
|
114
|
+
existing error envelope (`{ "state": "error", "prompt": "<findings>" }`) via
|
|
115
|
+
`makeProgram`'s `--json` `catchAll` — consistent with how step refusals surface
|
|
116
|
+
under `--json`.
|
|
117
|
+
|
|
118
|
+
`gtd validate` takes no positional args (`rejectExtraArgs("validate", argv)`),
|
|
119
|
+
accepts `--json`, and — like every state command — goes through the repo-root
|
|
120
|
+
guard, auto-init, and the review-window close-first/re-arm-last bracketing.
|
|
121
|
+
|
|
122
|
+
### 2.2 Conditional self-validation instruction on `gtd next` (THE key rule)
|
|
123
|
+
|
|
124
|
+
The instruction that tells the agent to validate its own output is **not** a
|
|
125
|
+
static line in the YAML prompt. gtd appends it **only in plain (non-JSON)
|
|
126
|
+
`gtd next` output**; under `--json` it is withheld and the _driver_ owns the
|
|
127
|
+
validate-and-retry loop — mirroring how the driver already owns turn
|
|
128
|
+
orchestration.
|
|
129
|
+
|
|
130
|
+
- **`gtd next` (plain text):** when the resolved rest is a state that must hand
|
|
131
|
+
over a valid file (see §2.3), append a standard block to the rendered prompt,
|
|
132
|
+
referencing the rendered `file:`, e.g.:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
<rendered prompt…>
|
|
136
|
+
|
|
137
|
+
Before finishing your turn, run `gtd validate` and fix every violation it
|
|
138
|
+
reports in <file> until it exits cleanly. Do not finish while it still
|
|
139
|
+
reports violations.
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This is for a human (or a simple driver) who reads `gtd next` and hands the
|
|
143
|
+
text to an agent: the agent self-validates.
|
|
144
|
+
|
|
145
|
+
- **`gtd next --json`:** do **NOT** append the block. The `content` field stays
|
|
146
|
+
the clean rendered prompt. Instead emit the marker field `"validate": true`
|
|
147
|
+
(omitted when false — same discipline as `model`/`memory`/`file`/`mode`). The
|
|
148
|
+
driver reads this and, after the agent's turn but **before** `gtd step`, runs
|
|
149
|
+
`gtd validate`; on a non-zero exit it re-invokes the agent with the findings
|
|
150
|
+
and loops until `gtd validate` passes (subject to a cap — §2.5), then steps.
|
|
151
|
+
This is the JSON-mode equivalent of the appended self-instruction.
|
|
152
|
+
|
|
153
|
+
The appended text is gtd-owned (a constant in `program.ts`); it references the
|
|
154
|
+
already-rendered `rest.file`. It is emitted only by `runNextCommand`'s
|
|
155
|
+
plain-text branch — `gtd run` drives _script_ states (the `check` actor), never
|
|
156
|
+
prompt states, so it is unaffected.
|
|
157
|
+
|
|
158
|
+
### 2.3 A `validate: true` state property (the marker)
|
|
159
|
+
|
|
160
|
+
Add one optional boolean state property, `validate: true`, meaning: _the actor
|
|
161
|
+
at this state must hand over a `file:` that passes `gtd validate` (per its
|
|
162
|
+
`mode:`) before the turn is considered done._ Set it on `grilling` and
|
|
163
|
+
`reviewing` in the bundled default.
|
|
164
|
+
|
|
165
|
+
Why a dedicated property rather than keying off `mode:` presence: `building`
|
|
166
|
+
also declares `mode: qa` but its job is to **delete** `.gtd/TODO.md`, not
|
|
167
|
+
produce it — keying self-validation on `mode:` would wrongly target `building`.
|
|
168
|
+
`validate: true` names exactly the producing states.
|
|
169
|
+
|
|
170
|
+
Semantics / validation (in `validateDefinition`):
|
|
171
|
+
|
|
172
|
+
- `validate: true` **requires** both `file:` and `mode:` on the same state
|
|
173
|
+
(there is nothing to validate without a file and a format).
|
|
174
|
+
- **Forbidden on a commit state** (never at rest — same rule as
|
|
175
|
+
`reviewWindow`/`reviewBase`).
|
|
176
|
+
- Engine never branches on it at step time; it is edge/emission-only data, read
|
|
177
|
+
by `renderRest`/`gtd next` and surfaced in `--json`.
|
|
178
|
+
|
|
179
|
+
> **Alternative considered (no new property, lower engine risk):** skip
|
|
180
|
+
> `validate: true`; in plain `gtd next` append the block to any `prompt` state
|
|
181
|
+
> declaring `file:`+`mode:` (accepting a harmless no-op instruction on
|
|
182
|
+
> `building`, whose deleted TODO validates as empty-valid), and have the driver
|
|
183
|
+
> run `gtd validate` after _every_ agent turn (a no-op exit 0 whenever there is
|
|
184
|
+
> nothing to validate). This removes all `PatternMachine`/`PatternConfig`/schema
|
|
185
|
+
> changes. It is viable but less self-documenting (no `validate:true` in
|
|
186
|
+
> `--json` or `gtd status`, and the `building` prompt carries an odd line).
|
|
187
|
+
> **Recommended: the explicit property.** If the implementer prefers to avoid
|
|
188
|
+
> engine changes, the alternative is acceptable — pick one and keep it
|
|
189
|
+
> consistent across code, docs, and tests.
|
|
190
|
+
|
|
191
|
+
## 3. Concrete change list
|
|
192
|
+
|
|
193
|
+
### 3.1 New command — `src/program.ts`
|
|
194
|
+
|
|
195
|
+
- Import `parseOpenQuestions` (`./OpenQuestions.js`) and `parseReviewDoc`
|
|
196
|
+
(`./ReviewDoc.js`); `WorktreeReader` is already imported.
|
|
197
|
+
- Add `runValidateCommand(argv, json, write)`:
|
|
198
|
+
- `rejectExtraArgs("validate", argv)`.
|
|
199
|
+
- `git = yield* GitService`; `worktree = yield* WorktreeReader`.
|
|
200
|
+
- `{ rest, context } = yield* resolveRestContext(git)` (reuse the existing
|
|
201
|
+
helper — it builds the same context `gtd status`/`gtd next` use).
|
|
202
|
+
- `file = yield* renderFile(rest.stateDef, context)`;
|
|
203
|
+
`mode = rest.stateDef.mode`.
|
|
204
|
+
- No `file`/no `mode` → success "nothing to validate".
|
|
205
|
+
- Read `file` via `worktree.read`, catching a missing-file error into `""`.
|
|
206
|
+
- `errors = mode === "qa" ? parseOpenQuestions(content).errors : parseReviewDoc(content).errors`.
|
|
207
|
+
- Empty `errors` → success ("\<file\>: valid" / JSON `valid:true`).
|
|
208
|
+
- Non-empty →
|
|
209
|
+
`Effect.fail(new Error(<file> + " is not valid:\n" + errors.map(e => " - " + e).join("\n")))`.
|
|
210
|
+
- Add `"validate"` to `KNOWN_SUBCOMMANDS` and a `case "validate":` in
|
|
211
|
+
`dispatchKnownSubcommand`.
|
|
212
|
+
- Add the appended-instruction constant and wire it into `runNextCommand`'s
|
|
213
|
+
**plain** branch only (append when `rendered` is a prompt state with the
|
|
214
|
+
`validate` marker and a defined `rendered.file`); in the `--json` branch add
|
|
215
|
+
`...(rendered.validate ? { validate: true } : {})` to the emitted object and
|
|
216
|
+
do NOT touch `content`.
|
|
217
|
+
- Add the `validate` line to `HELP_TEXT`.
|
|
218
|
+
|
|
219
|
+
Exit-code note: failing the Effect is the mechanism for non-zero exit (see
|
|
220
|
+
`main.ts` `catchAll`). Findings land on stderr in plain mode, and under `prompt`
|
|
221
|
+
in the `--json` error envelope.
|
|
222
|
+
|
|
223
|
+
### 3.2 New state property — engine + compiler + emission
|
|
224
|
+
|
|
225
|
+
- `src/PatternMachine.ts`: add `validate?: boolean` to `StateDef`; in
|
|
226
|
+
`validateDefinition` add the two rules from §2.3 (requires `file`+`mode`;
|
|
227
|
+
forbidden on commit states).
|
|
228
|
+
- `src/PatternConfig.ts`: compile the `validate:` key (boolean; reject
|
|
229
|
+
non-boolean) in the per-state field compilers.
|
|
230
|
+
- `src/Edge.ts`: add `validate?: boolean` to `RenderedRest`; set it in
|
|
231
|
+
`renderRest` (`...(rest.stateDef.validate ? { validate: true } : {})`).
|
|
232
|
+
- `src/program.ts`: emit `validate` in `gtd next --json` (see §3.1) and,
|
|
233
|
+
optionally, in `gtd status --json` + a `Validate: yes` plain line for symmetry
|
|
234
|
+
with `model`/`memory`/`file`/`mode`.
|
|
235
|
+
- `schema.json`: add the `validate` boolean to the per-state schema.
|
|
236
|
+
|
|
237
|
+
### 3.3 `src/workflows/default.yaml`
|
|
238
|
+
|
|
239
|
+
- **Delete** the `todo-validating` and `review-validating` states.
|
|
240
|
+
- **Rewire**: `grilling.on: "* **": grilling-answer`;
|
|
241
|
+
`reviewing.on: "* **": await-review`.
|
|
242
|
+
- **Mark**: add `validate: true` to `grilling` and `reviewing` (they already
|
|
243
|
+
declare `file:`+`mode:`).
|
|
244
|
+
- **Prompts**: from `grilling` and `reviewing`, remove the `.gtd/FORMAT.md`
|
|
245
|
+
clauses ("If `.gtd/FORMAT.md` exists…", "Never touch `.gtd/FORMAT.md`…"). Do
|
|
246
|
+
NOT add a `gtd validate` line — gtd appends it in plain mode (§2.2). **Keep**
|
|
247
|
+
the embedded format spec (so drafts are born valid).
|
|
248
|
+
- **Header comment**: 12 → 10 states; drop the `todo-validating`/
|
|
249
|
+
`review-validating` cycle-order lines and rewrite Loop 1 / Loop 2 to describe
|
|
250
|
+
self-validation via `gtd validate`; remove `.gtd/FORMAT.md` from the hygiene
|
|
251
|
+
invariant (it no longer exists); update the "dual-implementation contract"
|
|
252
|
+
paragraph (there is no bash port anymore — `OpenQuestions.ts`/`ReviewDoc.ts`
|
|
253
|
+
are the single source of truth, consumed by the LSP and `gtd validate`).
|
|
254
|
+
|
|
255
|
+
Reachability after rewiring (assert mentally / via `validateDefinition`): all 10
|
|
256
|
+
states reachable —
|
|
257
|
+
`idle→grilling→grilling-answer→building→checking→(fixing| reviewing)`,
|
|
258
|
+
`fixing↔checking`, `fixing→escalate→checking`,
|
|
259
|
+
`reviewing→ await-review→(idle|review-deciding|grilling)`,
|
|
260
|
+
`review-deciding→(grilling|idle| await-review)`.
|
|
261
|
+
|
|
262
|
+
### 3.4 Driver contract — `skills/loop/SKILL.md` + `bin/gtd-loop`
|
|
263
|
+
|
|
264
|
+
Teach the reference driver the `--json` half of §2.2:
|
|
265
|
+
|
|
266
|
+
- After emitting an agent `prompt` turn whose `gtd next --json` carried
|
|
267
|
+
`"validate": true`, and after the agent has acted but **before**
|
|
268
|
+
`gtd step <actor>`, run `gtd validate`.
|
|
269
|
+
- On a non-zero exit, re-invoke the agent with a fix instruction that includes
|
|
270
|
+
the `gtd validate` findings, then run `gtd validate` again. Loop until it
|
|
271
|
+
passes.
|
|
272
|
+
- Cap the fix attempts (§2.5) and reuse the loop's existing no-progress / stall
|
|
273
|
+
handling to avoid spinning; surface a clear message on the cap.
|
|
274
|
+
- Then `gtd step <actor>` as today.
|
|
275
|
+
|
|
276
|
+
Document the same contract in `skills/loop/SKILL.md` (it is a genuine addition
|
|
277
|
+
to the driver protocol — `dispatch on kind` + the new validate gate).
|
|
278
|
+
|
|
279
|
+
### 3.5 Doc / comment sweep
|
|
280
|
+
|
|
281
|
+
- **`STATES.md`**: §1 property table — add the `validate` row. §10 — table 12 →
|
|
282
|
+
10 states (drop the two validator rows; update `grilling`/`reviewing` `on`;
|
|
283
|
+
note `validate: true` on `grilling`/`reviewing`); rewrite the Loop 1 / Loop 2
|
|
284
|
+
walkthrough; drop `.gtd/FORMAT.md` from the hygiene invariant.
|
|
285
|
+
- **`docs/cli.md`**: document `gtd validate` (and the `--json` shape).
|
|
286
|
+
- **`docs/configuration.md`**: document the `validate:` property; remove the
|
|
287
|
+
FORMAT-loop description; keep the `file:`/`mode:` known-limitation.
|
|
288
|
+
- **`docs/design/steering-file-loops.md`**: add a status banner at the top
|
|
289
|
+
pointing here and noting §2–§4's validator states were replaced by
|
|
290
|
+
`gtd validate` + the `validate:` marker (leave §1 formats and §5 LSP intact).
|
|
291
|
+
- **`docs/upgrading.md`**, **`docs/development.md`**,
|
|
292
|
+
**`docs/design/state-file-association.md`**,
|
|
293
|
+
**`docs/design/state-machine-validation.md`**: replace `todo-validating`/
|
|
294
|
+
`review-validating`/`.gtd/FORMAT.md` references with the `gtd validate` model.
|
|
295
|
+
- **`README.md`**: adjust the steering-file / editor-integration mention.
|
|
296
|
+
- **`AGENTS.md`**: update the "Changing the Workflow" e2e feature list and the
|
|
297
|
+
"Scripted Check Actor" / dual-implementation notes to reflect that the bundled
|
|
298
|
+
default no longer ships the two validator states (the `check` actor still
|
|
299
|
+
exists for `checking`/`review-deciding`).
|
|
300
|
+
- **`src/OpenQuestions.ts` / `src/ReviewDoc.ts`** docstrings: remove the
|
|
301
|
+
"executable spec ↔ bash validator contract" paragraph and the stale
|
|
302
|
+
`gtd questions` / `gtd changesets` command references; state they are now the
|
|
303
|
+
single source of truth consumed by the LSP (`src/Lsp.ts`) and `gtd validate`.
|
|
304
|
+
- **`src/Lsp.ts`** docstrings: the two "same findings the workflow's
|
|
305
|
+
`todo-validating`/`review-validating` script would write to `.gtd/FORMAT.md`"
|
|
306
|
+
comments → reference `gtd validate` instead.
|
|
307
|
+
|
|
308
|
+
### 3.6 Tests
|
|
309
|
+
|
|
310
|
+
- **`tests/integration/features/smoke.feature`**: happy path becomes
|
|
311
|
+
`idle → grilling → grilling-answer → building → checking` (drop the
|
|
312
|
+
`todo-validating` hop and its `gtd step check`).
|
|
313
|
+
- **`tests/integration/features/default-workflow.feature`**: major rewrite.
|
|
314
|
+
Remove the `.gtd/FORMAT.md` malformed-lap simulations from the main journey
|
|
315
|
+
and the two standalone "malformed … bounces back" scenarios (that machinery is
|
|
316
|
+
gone). New hops: `grilling → grilling-answer` and `reviewing → await-review`
|
|
317
|
+
directly. The `.gtd/FORMAT.md does not exist` hygiene assertion can stay
|
|
318
|
+
(trivially true); update the comment that attributed its cleanup to the
|
|
319
|
+
validators.
|
|
320
|
+
- **`tests/integration/features/mermaid.feature`**: drop the
|
|
321
|
+
`state "todo-validating" as todo_validating` assertion (no longer a default
|
|
322
|
+
state); keep the rest.
|
|
323
|
+
- **`src/Mermaid.test.ts`**: the hyphenated-name unit test uses
|
|
324
|
+
`"todo-validating"` as a _synthetic_ fixture name — fine to leave, it is not
|
|
325
|
+
the bundled default. The "renders the bundled default … covering every state"
|
|
326
|
+
test iterates actual states and needs no change.
|
|
327
|
+
- **New `tests/integration/features/validate.feature` (@inmem)** — cucumber
|
|
328
|
+
scenarios for the new feature (per `AGENTS.md`: one scenario per feature,
|
|
329
|
+
composable `Given` steps, real file content in the scenario text). `@inmem` is
|
|
330
|
+
fine: `gtd validate` reads the working tree and runs a pure parser — no script
|
|
331
|
+
execution. Cover, with HEAD placed at the relevant state via
|
|
332
|
+
`a commit "gtd(<actor>): <state>" that adds …`:
|
|
333
|
+
- `grilling` + a valid `.gtd/TODO.md` → `gtd validate` succeeds, stdout
|
|
334
|
+
"valid".
|
|
335
|
+
- `grilling` + a malformed `.gtd/TODO.md` (an `### ` question with no
|
|
336
|
+
`Suggested default:`/`Answer:` line) → `gtd validate` fails, stderr carries
|
|
337
|
+
the finding.
|
|
338
|
+
- `reviewing` + a malformed `.gtd/REVIEW.md` (missing `# Review:` header) →
|
|
339
|
+
fails with the finding; a valid one → succeeds.
|
|
340
|
+
- a state with a `file:` but no `mode:` (e.g. `fixing`) → succeeds with
|
|
341
|
+
"nothing to validate".
|
|
342
|
+
- `gtd next` at `grilling` (plain) **contains** the appended validate
|
|
343
|
+
instruction; `gtd next --json` at `grilling` **does not** contain it in
|
|
344
|
+
`content` and **does** emit `"validate":true`.
|
|
345
|
+
- **Optional (nice-to-have) `@live`**: a `gtd-loop`-style scenario proving the
|
|
346
|
+
driver runs `gtd validate` after a producing agent turn and re-runs the agent
|
|
347
|
+
on findings before stepping. If added, follow `gtd-loop.feature`'s stub-agent
|
|
348
|
+
pattern. May be deferred if the driver-contract change is landed with unit
|
|
349
|
+
coverage instead.
|
|
350
|
+
|
|
351
|
+
### 3.7 No change needed
|
|
352
|
+
|
|
353
|
+
- `stryker.config.json` — the parsers stay in the mutate list; nothing removed.
|
|
354
|
+
- `src/PatternTemplates.ts` — the append happens in `program.ts`, not in
|
|
355
|
+
template rendering.
|
|
356
|
+
|
|
357
|
+
## 4. Acceptance checklist
|
|
358
|
+
|
|
359
|
+
- [ ] `gtd validate` exists, is current-state-scoped, exits non-zero with
|
|
360
|
+
findings on a malformed file and 0 otherwise; `--json` shape as in §2.1.
|
|
361
|
+
- [ ] `gtd next` (plain) appends the self-validation instruction at
|
|
362
|
+
`grilling`/`reviewing`; `gtd next --json` withholds it and emits
|
|
363
|
+
`"validate": true`.
|
|
364
|
+
- [ ] `validate:` property compiles, validates (requires `file`+`mode`,
|
|
365
|
+
forbidden on commit states), and is emitted in `--json`.
|
|
366
|
+
- [ ] The bundled default is 10 states; `todo-validating`/`review-validating`/
|
|
367
|
+
`.gtd/FORMAT.md` are gone; an approved cycle still leaves `.gtd/` empty.
|
|
368
|
+
- [ ] The reference driver runs `gtd validate` after a `validate:true` agent
|
|
369
|
+
turn and loops the agent on findings before stepping (capped).
|
|
370
|
+
- [ ] `npm test` green; `npm run build` (single-file bundle) green; docs and
|
|
371
|
+
`AGENTS.md` updated. (Do NOT run `npm run test:mutation`.)
|
|
372
|
+
|
|
373
|
+
## 5. Notes for the implementer
|
|
374
|
+
|
|
375
|
+
- Keep CLI flags orthogonal (`AGENTS.md`): `gtd validate` accepts only `--json`;
|
|
376
|
+
reject unknown `--` options (the existing `unknownOption` guard already covers
|
|
377
|
+
this once `validate` is a known subcommand).
|
|
378
|
+
- `gtd validate` resolves the CURRENT state — during a producing agent's turn,
|
|
379
|
+
HEAD still points at the commit that entered `grilling`/`reviewing` and the
|
|
380
|
+
agent's file is uncommitted in the working tree, so resolution +
|
|
381
|
+
`worktree.read` see exactly the right thing. No `gtd step` has happened yet.
|
|
382
|
+
- The `file:`/`mode:` known-limitation (STATES.md §10, configuration.md) still
|
|
383
|
+
applies: `on` pattern keys are literal `.gtd/...` paths. `gtd validate` reads
|
|
384
|
+
the _rendered_ `file:`, so repointing `todoFile`/`reviewFile` via `vars:`
|
|
385
|
+
keeps validate correct even though the (now-removed) `on` FORMAT rows used to
|
|
386
|
+
desync.
|
package/docs/development.md
CHANGED
|
@@ -62,12 +62,14 @@ git/`.gtdrc` dependency at all and serves whatever content the editor hands it
|
|
|
62
62
|
over the LSP protocol, exactly like any other document.
|
|
63
63
|
|
|
64
64
|
`src/OpenQuestions.ts` and `src/ReviewDoc.ts` are the pure parsers behind both
|
|
65
|
-
the LSP and the
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
the LSP and the `gtd validate` command, which resolves the current state, reads
|
|
66
|
+
its `file:`, and runs the parser its `mode:` selects (`qa` →
|
|
67
|
+
`parseOpenQuestions`, `review` → `parseReviewDoc`) — ONE source of truth per
|
|
68
|
+
format, no bash port and no dual-implementation contract to keep in sync. See
|
|
69
|
+
[docs/design/steering-file-validation-command.md](design/steering-file-validation-command.md)
|
|
70
|
+
and [STATES.md §12](../STATES.md) for the command, and each module's own doc
|
|
71
|
+
comment for the format it defines. Their unit tests are that format's spec
|
|
72
|
+
tests.
|
|
71
73
|
|
|
72
74
|
## Mutation testing
|
|
73
75
|
|
package/docs/upgrading.md
CHANGED
|
@@ -61,15 +61,18 @@ upgrading.
|
|
|
61
61
|
checkbox-only REVIEW.md diffs, and doc-structure validation are gone as ENGINE
|
|
62
62
|
mechanisms — verdicts are now expressed purely by which file a turn writes or
|
|
63
63
|
deletes, matched by an ordinary `on` pattern. The bundled default workflow's
|
|
64
|
-
own
|
|
65
|
-
`
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
own checkbox-review verdict (`review-deciding`) is ordinary WORKFLOW data (an
|
|
65
|
+
`on` pattern), not an engine hook: `D REVIEW.md` = approve, `M REVIEW.md` =
|
|
66
|
+
route to the deterministic decider, in the bundled default. Deterministic
|
|
67
|
+
format-checking of the steering files is likewise no longer a pair of
|
|
68
|
+
in-machine `check`/`script` states — the old `todo-validating`/
|
|
69
|
+
`review-validating` states and their `.gtd/FORMAT.md` steering file are gone,
|
|
70
|
+
replaced by the `gtd validate` command plus producing-agent self-validation
|
|
71
|
+
(see
|
|
72
|
+
[docs/design/steering-file-validation-command.md](design/steering-file-validation-command.md)
|
|
73
|
+
and [STATES.md §12](../STATES.md)).
|
|
74
|
+
- **`gtd review <target>`.** Gone; the v3 command surface is `step` / `next` /
|
|
75
|
+
`run` / `status` / `format` / `validate` / `lsp` (see
|
|
73
76
|
[CLI reference](cli.md)).
|
|
74
77
|
- **Model tiers, the decision log.** No `models` config key, no `Gtd-Decisions`
|
|
75
78
|
trailer scan, no grilling/architecting "prior decisions" context assembled
|