@pmelab/gtd 3.3.0 → 3.5.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 CHANGED
@@ -13,8 +13,26 @@ set -euo pipefail
13
13
  # `.model` (optional, opaque) is exported as $GTD_LOOP_MODEL for the agent
14
14
  # adapter invoked at a "prompt" rest; the default adapter appends `--model
15
15
  # "$GTD_LOOP_MODEL"` only when it's non-empty.
16
+ #
17
+ # `.memory` (optional, opaque — see skills/loop/SKILL.md "Agent memory scope")
18
+ # is the agent-memory scope label. gtd never interprets it; the driver does:
19
+ # consecutive "prompt" turns emitting the SAME label continue ONE agent
20
+ # session (so the agent still remembers its previous turn), and a changed
21
+ # label — or the first turn, or an unset label — starts fresh. That is what
22
+ # lets a loop retain memory within itself (grilling ↔ answer, fixing ↔
23
+ # checking) and clear it at a phase boundary. The previous prompt turn's scope
24
+ # and its agent session id are persisted in the git dir (NOT the working tree,
25
+ # so `gtd status` and the pending diff never see them), so retention survives
26
+ # across restarts — a grilling loop that halts at its human gate every lap
27
+ # still resumes one planning session. The scope, session id, and a
28
+ # resume-or-fresh flag are exported as $GTD_LOOP_MEMORY / $GTD_LOOP_SESSION_ID
29
+ # / $GTD_LOOP_MEMORY_RESUME; the default adapter turns them into
30
+ # `--resume`/`--session-id`. An adapter that ignores them keeps working.
16
31
  prev_state=""
17
32
  prev_content=""
33
+ # Where the last "prompt" turn's `memory` scope + session id are remembered
34
+ # across iterations and across restarts. In the git dir, never the work tree.
35
+ memory_marker="$(git rev-parse --git-dir 2>/dev/null || echo .git)/gtd-loop-memory"
18
36
 
19
37
  while true; do
20
38
  if ! next_json="$(gtd next --json 2>&1)"; then
@@ -29,6 +47,7 @@ while true; do
29
47
  kind="$(jq -r .kind <<<"$next_json")"
30
48
  content="$(jq -r .content <<<"$next_json")"
31
49
  model="$(jq -r '.model // empty' <<<"$next_json")"
50
+ memory="$(jq -r '.memory // empty' <<<"$next_json")"
32
51
 
33
52
  if [[ "$kind" == "message" ]]; then
34
53
  echo "--- Your turn ($state) ---"
@@ -60,19 +79,53 @@ while true; do
60
79
  prev_state="$state"
61
80
  prev_content="$content"
62
81
 
82
+ # Agent-memory scope: compare this turn's `memory` label against the last
83
+ # prompt turn's (persisted in $memory_marker). Same non-empty label AND a
84
+ # known prior session -> resume that session; otherwise start fresh with a
85
+ # new session id. An unset label carries no constraint (harness default) and
86
+ # clears the marker so a later labelled turn can't accidentally match it.
87
+ prev_memory=""
88
+ prev_session=""
89
+ if [[ -f "$memory_marker" ]]; then
90
+ prev_memory="$(sed -n 1p "$memory_marker")"
91
+ prev_session="$(sed -n 2p "$memory_marker")"
92
+ fi
93
+ resume=0
94
+ session_id=""
95
+ if [[ -n "$memory" ]]; then
96
+ if [[ "$memory" == "$prev_memory" && -n "$prev_session" ]]; then
97
+ resume=1
98
+ session_id="$prev_session"
99
+ else
100
+ session_id="$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "")"
101
+ fi
102
+ printf '%s\n%s\n' "$memory" "$session_id" >"$memory_marker"
103
+ else
104
+ rm -f "$memory_marker"
105
+ fi
106
+
63
107
  # Swappable agent adapter: GTD_LOOP_AGENT_CMD lets any coding agent CLI
64
- # stand in for the default, receiving the prompt via $GTD_LOOP_PROMPT and
65
- # the state's opaque model hint (if any) via $GTD_LOOP_MODEL. An adapter
66
- # that ignores $GTD_LOOP_MODEL keeps working — it's exported alongside
67
- # $GTD_LOOP_PROMPT, never required.
108
+ # stand in for the default, receiving the prompt via $GTD_LOOP_PROMPT, the
109
+ # state's opaque model hint (if any) via $GTD_LOOP_MODEL, and the memory
110
+ # scope via $GTD_LOOP_MEMORY / $GTD_LOOP_SESSION_ID / $GTD_LOOP_MEMORY_RESUME
111
+ # ("1" when this turn should continue the prior session). An adapter that
112
+ # ignores any of these keeps working — they're exported alongside
113
+ # $GTD_LOOP_PROMPT, never required. The default adapter maps the memory
114
+ # signal onto claude's own session flags: --resume to continue the scope's
115
+ # session, --session-id to pin a fresh one so the next same-scope turn can
116
+ # resume it.
68
117
  cmd="${GTD_LOOP_AGENT_CMD:-}"
69
118
  if [[ -z "$cmd" ]]; then
70
- if [[ -n "$model" ]]; then
71
- cmd='claude -p "$GTD_LOOP_PROMPT" --model "$GTD_LOOP_MODEL" --dangerously-skip-permissions'
72
- else
73
- cmd='claude -p "$GTD_LOOP_PROMPT" --dangerously-skip-permissions'
119
+ cmd='claude -p "$GTD_LOOP_PROMPT"'
120
+ [[ -n "$model" ]] && cmd="$cmd"' --model "$GTD_LOOP_MODEL"'
121
+ if [[ "$resume" == "1" ]]; then
122
+ cmd="$cmd"' --resume "$GTD_LOOP_SESSION_ID"'
123
+ elif [[ -n "$session_id" ]]; then
124
+ cmd="$cmd"' --session-id "$GTD_LOOP_SESSION_ID"'
74
125
  fi
126
+ cmd="$cmd"' --dangerously-skip-permissions'
75
127
  fi
76
- GTD_LOOP_PROMPT="$content" GTD_LOOP_MODEL="$model" bash -c "$cmd"
128
+ GTD_LOOP_PROMPT="$content" GTD_LOOP_MODEL="$model" GTD_LOOP_MEMORY="$memory" \
129
+ GTD_LOOP_SESSION_ID="$session_id" GTD_LOOP_MEMORY_RESUME="$resume" bash -c "$cmd"
77
130
  gtd step "$actor" >/dev/null
78
131
  done
@@ -189384,6 +189384,13 @@ const validateModel = (name, state) => {
189384
189384
  if (isCommitState(state) && state.model !== void 0) errors.push(`state "${name}": a commit state cannot declare "model"`);
189385
189385
  return errors;
189386
189386
  };
189387
+ /** `memory`, when present, must be a non-empty string; forbidden on a commit state — same rule family as `model` (`validateModel`): a commit state is never at rest, so no agent turn there could carry or continue a memory scope. */
189388
+ const validateMemory = (name, state) => {
189389
+ const errors = [];
189390
+ if (state.memory !== void 0 && state.memory === "") errors.push(`state "${name}": "memory" must be a non-empty string`);
189391
+ if (isCommitState(state) && state.memory !== void 0) errors.push(`state "${name}": a commit state cannot declare "memory"`);
189392
+ return errors;
189393
+ };
189387
189394
  const STATE_MODES = /* @__PURE__ */ new Set(["qa", "review"]);
189388
189395
  /**
189389
189396
  * `file`, when present, must be a non-empty string; forbidden on a commit
@@ -189479,6 +189486,7 @@ const validateState = (def, name, names) => {
189479
189486
  ...validateOnEdges(name, state, names),
189480
189487
  ...validateRetry(name, state, names),
189481
189488
  ...validateModel(name, state),
189489
+ ...validateMemory(name, state),
189482
189490
  ...validateFile(name, state),
189483
189491
  ...validateMode(name, state),
189484
189492
  ...validateReviewWindow(name, state)
@@ -189495,7 +189503,9 @@ const validateState = (def, name, names) => {
189495
189503
  * states carry an `actor`; every `on` pattern parses and every `on` target
189496
189504
  * and `retry.otherwise` names a defined state; `retry.max` is a
189497
189505
  * non-negative integer; `model`, when present, is a non-empty string and is
189498
- * never declared on a commit state; `file`, when present, is a non-empty
189506
+ * never declared on a commit state; `memory`, when present, is a non-empty
189507
+ * string and is never declared on a commit state (same rule family as
189508
+ * `model`); `file`, when present, is a non-empty
189499
189509
  * string and is never declared on a commit state; `mode`, when present, is
189500
189510
  * one of the closed vocabulary (`qa`/`review`), requires a sibling `file`,
189501
189511
  * and is never declared on a commit state; `reviewWindow`/`reviewBase`, when
@@ -189543,6 +189553,7 @@ const validateDefinition = (def) => {
189543
189553
  * max: <number>
189544
189554
  * otherwise: <targetState>
189545
189555
  * model: <string> # optional, opaque harness hint — never on a commit state
189556
+ * memory: <string> # optional, opaque memory-scope label — never on a commit state
189546
189557
  * file: <string> # optional, an Eta template naming the state's steering file — never on a commit state
189547
189558
  * mode: qa | review # optional, requires "file" — never on a commit state
189548
189559
  * ```
@@ -189636,6 +189647,7 @@ const KNOWN_STATE_KEYS = /* @__PURE__ */ new Set([
189636
189647
  "initial",
189637
189648
  "retry",
189638
189649
  "model",
189650
+ "memory",
189639
189651
  "file",
189640
189652
  "mode",
189641
189653
  "reviewWindow",
@@ -189765,6 +189777,15 @@ const compileModel = (raw, name, errors) => {
189765
189777
  }
189766
189778
  return raw.model;
189767
189779
  };
189780
+ /** The `memory` field: an opaque memory-scope label (Eta template), or undefined (either absent or invalid — the type mismatch is its own error). Never interpreted or validated beyond "is it a string" — see `PatternMachine.StateDef.memory`. */
189781
+ const compileMemory = (raw, name, errors) => {
189782
+ if (raw.memory === void 0) return void 0;
189783
+ if (typeof raw.memory !== "string") {
189784
+ errors.push(`state "${name}": "memory" must be a string`);
189785
+ return;
189786
+ }
189787
+ return raw.memory;
189788
+ };
189768
189789
  /** The `file` field: an Eta template string naming the state's steering file, or undefined (either absent or invalid — the type mismatch is its own error). Vocabulary/shape rules (non-empty, forbidden on a commit state) are `validateDefinition`'s concern, not this compiler's — see `PatternMachine.StateDef.file`. */
189769
189790
  const compileFile = (raw, name, errors) => {
189770
189791
  if (raw.file === void 0) return void 0;
@@ -189821,6 +189842,7 @@ const assembleStateDef = (parts) => ({
189821
189842
  initial: parts.initial,
189822
189843
  retry: parts.retry,
189823
189844
  model: parts.model,
189845
+ memory: parts.memory,
189824
189846
  file: parts.file,
189825
189847
  mode: parts.mode,
189826
189848
  reviewWindow: parts.reviewWindow,
@@ -189843,6 +189865,7 @@ const compileState = (name, raw, configDir, errors) => {
189843
189865
  initial: compileInitial(raw, name, errors),
189844
189866
  retry: compileRetry(raw.retry, name, errors),
189845
189867
  model: compileModel(raw, name, errors),
189868
+ memory: compileMemory(raw, name, errors),
189846
189869
  file: compileFile(raw, name, errors),
189847
189870
  mode: compileMode(raw, name, errors),
189848
189871
  reviewWindow: compileBooleanFlag(raw, "reviewWindow", name, errors),
@@ -189896,7 +189919,7 @@ const compileWorkflowConfig = (raw, configDir) => {
189896
189919
  * default.yaml's content values start with `./`/`../`, so
189897
189920
  * `compileWorkflowConfig` never resolves a file reference against it.
189898
189921
  */
189899
- const compiled = compileWorkflowConfig((0, import_dist$1.parse)("# The bundled default gtd workflow (v3 \"pattern machine\" — see\n# docs/design/pattern-machine-plan.md, and\n# docs/design/steering-file-loops.md for the two validation loops below).\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/default.ts does the compiling).\n#\n# Every content string below is INLINE (no `./`-relative file references):\n# the bundled default ships inside a single-file build\n# (`dist/gtd.bundle.mjs`), so its content must already be resolved at BUILD\n# time rather than read from disk at RUNTIME — the same reason\n# `src/prompts/*.md` are imported through tsdown's `.md`-as-text loader\n# instead of read via `fs` at runtime. `default.yaml` itself is imported the\n# same way (tsdown/vitest both treat `.yaml` as text — see\n# tsdown.config.ts / tests/vitest.rawMd.ts), so this file never needs a\n# sibling prompts/ directory to exist on disk once installed.\n#\n# States, in cycle order — 12 states, no squash:\n# idle (initial, human, message)\n# -> grilling (agent, prompt): reads .gtd/TODO.md's sketch (or a prior\n# lap's feedback), explores the codebase, and develops it into a concrete\n# implementation plan, stating any open question it can't settle itself\n# under a `## Open Questions` heading (see \"Loop 1\" below) rather than\n# guessing silently.\n# grilling -> todo-validating (check, script): mechanics-only parser port\n# of the open-questions format (see src/OpenQuestions.ts's module doc for\n# the executable spec this is a bash rendition of). Malformed ->\n# .gtd/FORMAT.md -> back to grilling; valid -> grilling-answer.\n# todo-validating -> grilling-answer (human, message): tick off suggested\n# defaults by replacing them with `Answer:` lines; a clean step accepts\n# every remaining default -> building; any edit loops back to grilling.\n# grilling-answer -> building (agent, prompt): implements the plan in\n# .gtd/TODO.md directly (no task decomposition, no per-task queue) and\n# deletes .gtd/TODO.md once the work is complete and verified.\n# building -> checking (check, script): runs the test command inline;\n# A/M .gtd/FEEDBACK.md -> fixing (capped, redirects to escalate); D\n# .gtd/FEEDBACK.md (a green run that just cleaned up prior feedback) or C\n# (green, nothing to clean) -> reviewing.\n# fixing (agent, prompt) <-> checking\n# escalate (human, message): manual intervention, then back to checking\n# checking (green) -> reviewing (agent, prompt): writes .gtd/REVIEW.md in\n# the checkbox review format (see \"Loop 2\" below), grouping the cycle's\n# full diff into reviewable chunks.\n# reviewing -> review-validating (check, script): mechanics-only parser\n# port of the review format (see src/ReviewDoc.ts's module doc for the\n# executable spec). Malformed -> .gtd/FORMAT.md -> back to reviewing;\n# valid -> await-review.\n# review-validating -> await-review (human, message): tick a pointer's `- [\n# ]` to `- [x]` to approve that item; deleting .gtd/REVIEW.md outright is\n# the power-user approve-everything shortcut; anything else pending\n# (code edits with REVIEW.md untouched) is feedback straight to grilling.\n# await-review -> review-deciding (check, script), entered by any `M\n# .gtd/REVIEW.md` step: if no unticked `- [ ]` pointer remains, the cycle\n# is approved (rm .gtd/REVIEW.md -> idle); otherwise the still-unticked\n# pointers (with their notes) are extracted into a fresh .gtd/TODO.md and\n# .gtd/REVIEW.md is removed (-> grilling, feedback lap).\n#\n# The cycle ends at human approval, resting back at idle — there is no\n# squash state. The cycle's commits stay in history; whether/how to squash\n# them (amend, interactive rebase, a PR's squash-merge) is entirely the\n# human's business once they're done reviewing — gtd makes no assumption.\n# That squash-flavored finale (a `squashing` prompt state + a `done` commit\n# state) still exists as an ENGINE capability (see STATES.md §8) and is kept\n# as part of the fuller machine at docs/examples/advanced-workflow.md.\n#\n# A `gtd(<actor>): idle` commit — entering the workflow's own initial state —\n# is therefore a PROCESS BOUNDARY (see `computeProcessRun` in src/Edge.ts):\n# the next cycle's `retry` counts, `startCommit`, and diffs never reach back\n# across an approved cycle's own idle-entering commit.\n#\n# HYGIENE INVARIANT (asserted in e2e — see default-workflow.feature): an\n# approved cycle leaves `.gtd/` empty. FEEDBACK.md is cleaned up by a green\n# `checking` run; FORMAT.md by a valid validator run (either loop); REVIEW.md\n# by the review-deciding approval branch; TODO.md by `building`.\n#\n# Loop 1 — TODO.md open questions (src/OpenQuestions.ts is this format's\n# EXECUTABLE SPEC — its unit tests are the format's spec tests; the\n# `todo-validating`/`review-validating` bash scripts below are a pragmatic\n# grep/awk PORT of the exact same rules, not a full markdown parser. Keep\n# both in sync by hand — there is no shared code path between the two\n# implementations, by design: the engine stays git/filesystem/Effect-free,\n# and the LSP's parsers stay dependency-free).\n#\n# Loop 2 — REVIEW.md checkboxes: same dual-implementation contract, this time\n# against src/ReviewDoc.ts.\n#\n# `grilling` and `reviewing` both declare `model: smart` — an opaque harness\n# hint (see the comment at `grilling` below) for their heavier one-shot\n# planning/reviewing turns. Every other state is unset (harness default).\n#\n# `vars.testCommand` is this workflow's own declared default for `checking`'s\n# script (see it.vars in PatternTemplates.ts) — override it per-repo with a\n# top-level `.gtdrc` `vars: { testCommand: ... }` key, or per-invocation with\n# a `GTD_VAR_testCommand` environment variable (highest precedence).\n#\n# `vars.todoFile`/`reviewFile`/`feedbackFile` are this workflow's ONE source\n# of truth for the three steering-file paths every prompt/script below reads\n# or writes — each appears in templates as `<%~ it.vars.todoFile %>` etc\n# (see docs/design/state-file-association.md §2). Every state that rests on\n# one of these files also declares a `file:`/`mode:` pair (see STATES.md's\n# property table) so `gtd status`/`gtd next --json` and the LSP (`gtd lsp`)\n# know which file a human/editor should look at, and in what format\n# (`qa` for TODO.md's open-questions format, `review` for REVIEW.md's\n# checkbox format) — `fixing`/`escalate` declare `file:` alone (FEEDBACK.md\n# is plain text, no LSP format). KNOWN LIMITATION: `on` pattern keys below are\n# NOT Eta templates — they keep the LITERAL `.gtd/...` paths matching these\n# vars' DEFAULT values. Repointing `todoFile`/`reviewFile`/`feedbackFile` via\n# `.gtdrc`'s top-level `vars:` or a `GTD_VAR_` override changes what `file:`\n# renders to (and what a template reads/writes) WITHOUT changing what the\n# `on` patterns match against — desyncing the machine. See\n# docs/configuration.md's `file:`/`mode:` section for the full caveat.\n#\n# A fuller machine — two-phase Q&A planning, an architecture phase, task\n# decomposition, a deterministic per-task builder loop, agent-prepared\n# review, and the squash finale (`squashing` + `done`) — is preserved as a\n# copy-paste `.gtdrc` example at docs/examples/advanced-workflow.md.\n\nvars:\n testCommand: npm test\n todoFile: .gtd/TODO.md\n reviewFile: .gtd/REVIEW.md\n feedbackFile: .gtd/FEEDBACK.md\n\nstates:\n idle:\n actor: human\n initial: true\n # Every human-gate `message:` below closes with the SAME rendered block —\n # a \"What each change does next\" list built from `it.edges`, the resting\n # state's own `on` edges (see PatternTemplates.TemplateContext.edges). Each\n # edge's human-readable route lives in its `describe:` (the single source\n # of truth), so the list can never drift from the routing it documents.\n # The block skips edges without a `describe`, and each `<%~ ... \"\\n\" %>`\n # keeps the bullet's own newline (Eta's autoTrim strips the template's own\n # line breaks around `<% %>` tags, not text inside an interpolation).\n message: |\n No active gtd cycle.\n\n To start one, write `.gtd/TODO.md` with a short sketch of what you want\n built — a few sentences is enough.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: grilling\n describe: >-\n Save your sketch to `.gtd/TODO.md` (any working-tree change) to start\n the cycle — the agent reads it, explores the codebase, and develops it\n into a concrete implementation plan (**grilling**).\n\n grilling:\n actor: agent\n file: <%= it.vars.todoFile %>\n mode: qa\n # `model` is an opaque harness hint — gtd never interprets this string, it\n # only passes it through to `gtd next --json`/`gtd status --json` so the\n # driving loop can map it onto whatever models its agent harness\n # provides. \"smart\" here just names the harness-chosen tier for the\n # heavier planning turn; states without a `model` use the harness's\n # default.\n model: smart\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.todoFile %>`, which this prompt tells you to develop. Never\n touch `.gtd/FORMAT.md` yourself if it exists — a validator writes it,\n and only the validator removes it once you've fixed the underlying\n problem.\n\n If `.gtd/FORMAT.md` exists, it holds format findings from your last\n draft of `<%= it.vars.todoFile %>` — fix those findings first, then\n continue below.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build (or\n feedback from a prior review lap). Read it, explore the codebase, and\n develop it into a concrete implementation plan.\n\n For every question you cannot settle yourself, add it under a\n `## Open Questions` heading, one `### <question>` sub-heading each,\n whose body's FIRST non-blank line is exactly\n `Suggested default: <your answer>` — the human can accept it as-is or\n override it. Omit the section entirely once there are no open\n questions. If `<%= it.vars.todoFile %>` already carries\n `## Open Questions` entries from a previous lap, fold any\n `Answer: <text>` line into the plan; every remaining entry's body must\n still start with either `Suggested default: ...` or `Answer: ...` —\n never anything else.\n\n Leave `<%= it.vars.todoFile %>` uncommitted and finish your turn — do\n not commit, and do not run `gtd step agent` yourself; the harness does\n that.\n on:\n \"* **\": todo-validating\n\n todo-validating:\n actor: check\n file: <%= it.vars.todoFile %>\n mode: qa\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: a pragmatic grep/awk port of\n # src/OpenQuestions.ts's parser rules (the format's executable spec) —\n # free-form prose, plus an OPTIONAL \"## Open Questions\" section; every\n # \"### \" sub-heading directly under it is one question whose body's\n # first non-blank line must start with \"Suggested default: \" or\n # \"Answer: \". A violation is one finding line (file + line + rule) in\n # .gtd/FORMAT.md; a clean parse removes any stale .gtd/FORMAT.md\n # instead. What FORMAT.md's presence/absence MEANS (another drafting\n # round vs. proceeding to the human) is decided by this state's own\n # `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n awk '\n BEGIN { in_section = 0; have_question = 0; question = \"\"; qline = 0; body_seen = 0 }\n {\n trimmed = $0\n gsub(/^[ \\t]+|[ \\t]+$/, \"\", trimmed)\n\n level = 0\n if (match(trimmed, /^#+/)) level = RLENGTH\n\n if (in_section && level >= 1 && level <= 2) {\n if (have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n in_section = 0\n have_question = 0\n }\n\n if (trimmed == \"## Open Questions\") { in_section = 1; next }\n\n if (in_section && level == 3) {\n if (have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n question = trimmed\n sub(/^### */, \"\", question)\n qline = NR\n have_question = 1\n body_seen = 0\n next\n }\n\n if (in_section && have_question && !body_seen && trimmed != \"\") {\n if (trimmed ~ /^Suggested default: *.+/ || trimmed ~ /^Answer: *.+/) {\n body_seen = 1\n } else {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n body_seen = 1\n }\n }\n }\n END {\n if (in_section && have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n }\n ' <%~ it.vars.todoFile %> > .gtd/.format-findings\n\n if [ -s .gtd/.format-findings ]; then\n mv .gtd/.format-findings .gtd/FORMAT.md\n else\n rm -f .gtd/.format-findings\n rm -f .gtd/FORMAT.md\n fi\n on:\n \"A .gtd/FORMAT.md\": grilling\n \"M .gtd/FORMAT.md\": grilling\n \"D .gtd/FORMAT.md\": grilling-answer\n \"C\": grilling-answer\n\n grilling-answer:\n actor: human\n file: <%= it.vars.todoFile %>\n mode: qa\n message: |\n `<%= it.vars.todoFile %>` holds the plan under development, with any\n open questions under `## Open Questions` — each carrying a suggested\n default. Answer a question by editing its entry in place: replace its\n `Suggested default: ...` line with `Answer: ...`, or add further detail.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: building\n describe: >-\n Change nothing to accept every suggested default as-is and move on to\n implementation (**building**).\n \"* **\":\n to: grilling\n describe: >-\n Edit the plan — answer a question, add detail, or raise a new one — to\n send it back for another round of grilling (**grilling**).\n\n building:\n actor: agent\n file: <%= it.vars.todoFile %>\n mode: qa\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create or edit anything under `.gtd/` except deleting\n `<%= it.vars.todoFile %>`, which this prompt tells you to delete once\n its work is complete.\n\n The plan to implement is: <%~ it.read(it.vars.todoFile) %>\n\n Read that plan and implement it in full. Use TDD discipline: one test,\n then the implementation that passes it, then the next — never write\n all the tests first.\n\n Delete `<%= it.vars.todoFile %>` once the work it describes is complete\n and verified. Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": checking\n\n checking:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: run the test suite and record a\n # red run's output as .gtd/FEEDBACK.md. What that output MEANS (another\n # fix round vs escalation) is decided by the workflow's own `on`/`retry`\n # rules at capture time — never here.\n #\n # `it.vars.testCommand` defaults to \"npm test\" (this workflow's own\n # `vars:` above) — override it with a top-level `.gtdrc`\n # `vars: { testCommand: ... }` key, or a `GTD_VAR_testCommand`\n # environment variable (highest precedence).\n #\n # A green run also removes any `<%= it.vars.feedbackFile %>` left over\n # from an earlier red run — there is no squash to discard it for us\n # anymore, so the script cleans it up itself before moving on to\n # review.\n set +e\n mkdir -p .gtd\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output <%~ it.vars.feedbackFile %>\n else\n rm -f .gtd/.check-output\n printf 'npm test failed with exit code %s and produced no output.' \"$code\" > <%~ it.vars.feedbackFile %>\n fi\n else\n rm -f .gtd/.check-output\n rm -f <%~ it.vars.feedbackFile %>\n fi\n on:\n \"A .gtd/FEEDBACK.md\": fixing\n \"M .gtd/FEEDBACK.md\": fixing\n \"D .gtd/FEEDBACK.md\": reviewing\n \"C\": reviewing\n\n fixing:\n actor: agent\n file: <%= it.vars.feedbackFile %>\n retry:\n max: 3\n otherwise: escalate\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.feedbackFile %>`, which this prompt tells you to address.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix\n the code so the suite passes. Keep the change focused — do not\n refactor unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue; the\n machine picks the dispute up either way.\n\n Leave everything uncommitted and finish your turn — do not commit.\n on:\n \"* **\": checking\n\n escalate:\n actor: human\n file: <%= it.vars.feedbackFile %>\n message: |\n The agent could not get the check to pass after repeated attempts.\n `<%= it.vars.feedbackFile %>` holds the last failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: checking\n describe: >-\n Fix the failure yourself — edit the code and/or `.gtd/FEEDBACK.md` —\n to re-run the check (**checking**).\n\n reviewing:\n actor: agent\n file: <%= it.vars.reviewFile %>\n mode: review\n model: smart\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewFile %>`, which this prompt tells you to write.\n Never touch `.gtd/FORMAT.md` yourself if it exists — a validator writes\n it, and only the validator removes it once you've fixed the underlying\n problem.\n\n If `.gtd/FORMAT.md` exists, it holds format findings from your last\n draft of `<%= it.vars.reviewFile %>` — fix those findings first, then\n continue below.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the cycle's\n full diff (inlined below) in this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.startCommit %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation\n of what changed and why, then one\n `- [ ] ./path/to/file.ts#42 — note` file pointer per hunk (checkboxes\n are for the human to tick, not you; every path is `./`-relative, with\n an optional `#line` and an optional ` — note`).\n\n Leave `<%= it.vars.reviewFile %>` uncommitted and finish your turn.\n <% if (it.processDiff.trim()) { %>\n\n ## Full cycle diff\n\n ```diff\n <%~ it.processDiff %>\n ```\n <% } %>\n on:\n \"* **\": review-validating\n\n review-validating:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: a pragmatic grep/awk port of\n # src/ReviewDoc.ts's parser rules (the format's executable spec) — a\n # `# Review: <hash>` header as the first non-blank line, a\n # `<!-- base: <hash> -->` comment, and at least one `##` chunk with at\n # least one `- [ ]`/`- [x]` file pointer. A violation is one finding\n # line (file + line + rule) in .gtd/FORMAT.md; a clean parse removes any\n # stale .gtd/FORMAT.md instead. What FORMAT.md's presence/absence MEANS\n # (another drafting round vs. handing off to the human) is decided by\n # this state's own `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n awk '\n BEGIN {\n first_nonblank_seen = 0\n header_ok = 0\n base_ok = 0\n chunk_open = 0\n chunk_title = \"\"\n chunk_line = 0\n chunk_files = 0\n chunk_count = 0\n }\n {\n trimmed = $0\n gsub(/^[ \\t]+|[ \\t]+$/, \"\", trimmed)\n\n if (!first_nonblank_seen && trimmed != \"\") {\n first_nonblank_seen = 1\n if (trimmed ~ /^# Review: [^ ]+$/) {\n header_ok = 1\n } else {\n printf \"<%~ it.vars.reviewFile %>:%d: first non-blank line must be \\\"# Review: <hash>\\\"\\n\", NR\n }\n }\n\n if (trimmed ~ /^<!-- *base: *[^ ]+ *-->$/) base_ok = 1\n\n if (trimmed ~ /^## [^ ]/) {\n if (chunk_open && chunk_files == 0) {\n printf \"<%~ it.vars.reviewFile %>:%d: chunk \\\"%s\\\" has no file pointers\\n\", chunk_line, chunk_title\n }\n chunk_open = 1\n chunk_title = trimmed\n sub(/^## /, \"\", chunk_title)\n chunk_line = NR\n chunk_files = 0\n chunk_count++\n next\n }\n\n if (chunk_open) {\n if (trimmed ~ /^-[ \\t]*\\[[ xX]\\][ \\t]*\\.\\/[^ \\t]+/) {\n chunk_files++\n } else if (trimmed ~ /^-[ \\t]*\\[/) {\n printf \"<%~ it.vars.reviewFile %>:%d: file pointer does not match \\\"- [ ] ./path#line — note\\\"\\n\", NR\n }\n }\n }\n END {\n if (chunk_open && chunk_files == 0) {\n printf \"<%~ it.vars.reviewFile %>:%d: chunk \\\"%s\\\" has no file pointers\\n\", chunk_line, chunk_title\n }\n if (!header_ok) {\n print \"<%~ it.vars.reviewFile %>:1: missing or malformed \\\"# Review: <hash>\\\" header as first line\"\n }\n if (!base_ok) {\n print \"<%~ it.vars.reviewFile %>:1: missing \\\"<!-- base: <hash> -->\\\" comment\"\n }\n if (chunk_count == 0) {\n print \"<%~ it.vars.reviewFile %>:1: no \\\"##\\\" chunks found\"\n }\n }\n ' <%~ it.vars.reviewFile %> > .gtd/.format-findings\n\n if [ -s .gtd/.format-findings ]; then\n mv .gtd/.format-findings .gtd/FORMAT.md\n else\n rm -f .gtd/.format-findings\n rm -f .gtd/FORMAT.md\n fi\n on:\n \"A .gtd/FORMAT.md\": reviewing\n \"M .gtd/FORMAT.md\": reviewing\n \"D .gtd/FORMAT.md\": await-review\n \"C\": await-review\n\n await-review:\n actor: human\n file: <%= it.vars.reviewFile %>\n mode: review\n # While the cycle rests here for human review, gtd opens a \"review checkout\n # window\": HEAD and the index are temporarily rewound to this cycle's start\n # (the process boundary — no `reviewBase` state is declared, so the base\n # defaults to it) with the working tree untouched, so the whole cycle diff\n # surfaces as ordinary uncommitted changes in the editor's git integration.\n # The window closes automatically on the next invocation, once the cycle\n # rests anywhere else (see src/ReviewWindow.ts, STATES.md §11).\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the completed\n cycle — one `- [ ]` checkbox per reviewable item, grouped into chunks.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"D .gtd/REVIEW.md\":\n to: idle\n describe: >-\n Delete `.gtd/REVIEW.md` outright to approve the whole cycle at once\n and rest back at the start (**idle**).\n \"M .gtd/REVIEW.md\":\n to: review-deciding\n describe: >-\n Edit `.gtd/REVIEW.md` — tick a `- [ ]` box to `- [x]` to approve that\n item, or add notes — to hand it to a deterministic check\n (**review-deciding**) that closes the cycle out when every box is\n ticked, or extracts the still-unticked items into a fresh\n `.gtd/TODO.md` and loops back through **grilling** with that feedback.\n \"* **\":\n to: grilling\n describe: >-\n Change only code, leaving `.gtd/REVIEW.md` untouched, to send that as\n feedback straight back to **grilling**.\n\n review-deciding:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: if .gtd/REVIEW.md still has any\n # unticked \"- [ ]\" pointer, extract those pointers (with their notes)\n # into a fresh .gtd/TODO.md; either way, remove .gtd/REVIEW.md. What\n # the resulting diff MEANS (feedback lap vs. full approval) is decided\n # by this state's own `on` rules at capture time — never here. The\n # A/M .gtd/TODO.md row is declared BEFORE the D .gtd/REVIEW.md row (see\n # this workflow's `on` map below) so a feedback lap — which always also\n # deletes .gtd/REVIEW.md — is captured as feedback, not mistaken for an\n # approval.\n set +e\n mkdir -p .gtd\n if grep -qE '^[[:space:]]*-[[:space:]]*\\[[[:space:]]*\\]' <%~ it.vars.reviewFile %> 2>/dev/null; then\n {\n echo \"Feedback from review — address these before continuing:\"\n echo\n grep -E '^[[:space:]]*-[[:space:]]*\\[[[:space:]]*\\]' <%~ it.vars.reviewFile %>\n } > <%~ it.vars.todoFile %>\n fi\n rm -f <%~ it.vars.reviewFile %>\n on:\n \"A .gtd/TODO.md\": grilling\n \"M .gtd/TODO.md\": grilling\n \"D .gtd/REVIEW.md\": idle\n \"C\": await-review\n"), ".");
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"), ".");
189900
189923
  const defaultWorkflowDefinition = compiled.definition;
189901
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`). */
189902
189925
  const defaultWorkflowVars = compiled.vars;
@@ -190034,6 +190057,10 @@ const workflowJsonSchema = {
190034
190057
  type: "string",
190035
190058
  description: "Opaque harness hint passed through `gtd next --json`/`gtd status --json` (e.g. \"smart\"). Never interpreted by gtd. Forbidden on a commit state."
190036
190059
  },
190060
+ memory: {
190061
+ type: "string",
190062
+ description: "Opaque memory-scope label passed through `gtd next --json`/`gtd status --json` (e.g. \"plan\"). A memory-aware driver retains an agent's memory across consecutive agent turns sharing this label and starts fresh when it changes. Never interpreted by gtd. Forbidden on a commit state."
190063
+ },
190037
190064
  file: {
190038
190065
  type: "string",
190039
190066
  description: "The state's steering file: an Eta template naming the file a human/editor should look at while the machine rests here. Forbidden on a commit state."
@@ -191739,6 +191766,40 @@ const renderStateTemplate = (template, context) => eta.renderString(template, co
191739
191766
  */
191740
191767
  const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
191741
191768
  const subjectOf$1 = (message) => (message.split("\n")[0] ?? "").trim();
191769
+ const COST_TRAILER_PREFIX = "Gtd-Cost: ";
191770
+ const COST_TRAILER_RE = /^Gtd-Cost:[ \t]*([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+?))?[ \t]*$/gm;
191771
+ /** The bucket a cost with no `--model` tag is grouped under, kept distinct so a mixed history still totals correctly. */
191772
+ const UNATTRIBUTED_MODEL = "unspecified";
191773
+ /**
191774
+ * Append a `Gtd-Cost: <cost>[ <model>]` trailer (after a blank line) to a
191775
+ * commit `subject`, or return the subject unchanged when no cost was supplied.
191776
+ * The subject (first line) is never touched, so `parseStateSubject`/
191777
+ * `resolveState` still read `gtd(<actor>): <state>` back exactly as before.
191778
+ */
191779
+ const withCostTrailer = (subject, cost, model) => cost === void 0 ? subject : `${subject}\n\n${COST_TRAILER_PREFIX}${cost}${model !== void 0 ? ` ${model}` : ""}`;
191780
+ /** Every `Gtd-Cost:` entry found across the given commit messages (each entry's model defaults to `UNATTRIBUTED_MODEL`). */
191781
+ const parseCostTrailers = (messages) => {
191782
+ const entries = [];
191783
+ for (const message of messages) for (const match of message.matchAll(COST_TRAILER_RE)) {
191784
+ const model = match[2]?.trim();
191785
+ entries.push({
191786
+ cost: Number(match[1]),
191787
+ model: model !== void 0 && model !== "" ? model : UNATTRIBUTED_MODEL
191788
+ });
191789
+ }
191790
+ return entries;
191791
+ };
191792
+ /** The total token cost across the given entries (`0` when none). */
191793
+ const totalCostOf = (entries) => entries.reduce((sum, entry) => sum + entry.cost, 0);
191794
+ /** Per-model token totals, highest-cost first (ties broken by model name for a stable order). */
191795
+ const costByModel = (entries) => {
191796
+ const byModel = /* @__PURE__ */ new Map();
191797
+ for (const entry of entries) byModel.set(entry.model, (byModel.get(entry.model) ?? 0) + entry.cost);
191798
+ return [...byModel.entries()].map(([model, cost]) => ({
191799
+ model,
191800
+ cost
191801
+ })).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
191802
+ };
191742
191803
  /** Collapse an arbitrary git status letter to the pattern grammar's closed `A|M|D` set (mirrors the plan's decision 5 — only those three are meaningful statuses). */
191743
191804
  const normalizeStatus = (raw) => raw === "A" ? "A" : raw === "D" ? "D" : "M";
191744
191805
  /** Resolve HEAD's subject against the active workflow definition (the bundled default, or a compiled `.gtdrc` `workflow:` key). */
@@ -191783,7 +191844,8 @@ const computeProcessRun = (git, def) => gen(function* () {
191783
191844
  if (!(yield* git.hasCommits())) return {
191784
191845
  startHash: "",
191785
191846
  startParentHash: EMPTY_TREE$1,
191786
- trace: []
191847
+ trace: [],
191848
+ costEntries: []
191787
191849
  };
191788
191850
  const initialState = initialStateOf(def);
191789
191851
  const history = yield* git.commitHistory();
@@ -191794,12 +191856,15 @@ const computeProcessRun = (git, def) => gen(function* () {
191794
191856
  i--;
191795
191857
  }
191796
191858
  const startIdx = i + 1;
191797
- const trace = history.slice(startIdx).map((h) => parseStateSubject(subjectOf$1(h.message)).state);
191859
+ const processCommits = history.slice(startIdx);
191860
+ const trace = processCommits.map((h) => parseStateSubject(subjectOf$1(h.message)).state);
191861
+ const costEntries = parseCostTrailers(processCommits.map((h) => h.message));
191798
191862
  const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$1;
191799
191863
  return {
191800
191864
  startHash: startIdx < history.length ? history[startIdx].hash : history[history.length - 1].hash,
191801
191865
  startParentHash,
191802
- trace
191866
+ trace,
191867
+ costEntries
191803
191868
  };
191804
191869
  });
191805
191870
  /** The `GTD_VAR_`-prefix stripped, exact-case, from every matching entry — the highest-precedence `it.vars` layer. A `value === undefined` entry (a name declared-but-unset in the environment) is skipped, never coerced to the string `"undefined"`. */
@@ -191836,13 +191901,27 @@ const toTemplateEdges = (edges) => (edges ?? []).map(([pattern, target, describe
191836
191901
  pattern,
191837
191902
  target
191838
191903
  });
191839
- /** Build the `PatternTemplates.TemplateContext` for rendering `state`'s content at the resolved rest. `vars` is the already-merged three-layer map (see `resolveVars`); `edges` is the resting state's own `on` edges (see `toTemplateEdges`). */
191840
- const buildTemplateContext = (git, read, state, actor, run, vars, edges) => gen(function* () {
191904
+ /**
191905
+ * Build the `PatternTemplates.TemplateContext` for rendering `state`'s content
191906
+ * at the resolved rest. `vars` is the already-merged three-layer map (see
191907
+ * `resolveVars`); `edges` is the resting state's own `on` edges (see
191908
+ * `toTemplateEdges`). `currentCost`/`currentModel` are the in-flight step's own
191909
+ * `--cost`/`--model` (folded into the process's committed cost entries so a
191910
+ * `commit:` squash template sees the whole-process total AND per-model
191911
+ * breakdown including the squashing step) — `0`/absent for the pure emitters
191912
+ * (`gtd next`/`gtd status`), where no step is being performed.
191913
+ */
191914
+ const buildTemplateContext = (git, read, state, actor, run, vars, edges, currentCost = 0, currentModel) => gen(function* () {
191841
191915
  const hasCommits = yield* git.hasCommits();
191842
191916
  const currentCommit = hasCommits ? yield* git.resolveRef("HEAD") : "";
191843
191917
  const previousCommit = hasCommits ? yield* git.resolveRef("HEAD~1").pipe(catchAll(() => succeed$3(run.startParentHash))) : "";
191844
191918
  const processDiff = [yield* git.diffRef(run.startParentHash).pipe(catchAll(() => succeed$3(""))), yield* git.diffHead().pipe(catchAll(() => succeed$3("")))].filter((d) => d.trim().length > 0).join("\n\n");
191845
191919
  const lastDiff = run.trace.length > 0 ? yield* git.commitDiff(currentCommit).pipe(catchAll(() => succeed$3(""))) : "";
191920
+ const stepEntry = currentCost > 0 || currentModel !== void 0 ? [{
191921
+ cost: currentCost,
191922
+ model: currentModel ?? "unspecified"
191923
+ }] : [];
191924
+ const allCostEntries = [...run.costEntries, ...stepEntry];
191846
191925
  return {
191847
191926
  startCommit: run.startHash,
191848
191927
  currentCommit,
@@ -191851,6 +191930,8 @@ const buildTemplateContext = (git, read, state, actor, run, vars, edges) => gen(
191851
191930
  actor,
191852
191931
  processDiff,
191853
191932
  lastDiff,
191933
+ processCost: totalCostOf(allCostEntries),
191934
+ processCostByModel: costByModel(allCostEntries),
191854
191935
  read,
191855
191936
  vars,
191856
191937
  edges: toTemplateEdges(edges)
@@ -191871,6 +191952,18 @@ const renderModel = (stateDef, context) => try_({
191871
191952
  catch: (e) => e instanceof Error ? e : new Error(String(e))
191872
191953
  });
191873
191954
  /**
191955
+ * Render a state's declared `memory:` scope label (if any) through the SAME
191956
+ * template context as its content/`model` — see `renderModel`'s doc comment;
191957
+ * a plain label (e.g. `"plan"`) passes through unchanged, while
191958
+ * `memory: "<%= it.vars.planScope %>"` resolves against the merged `it.vars`.
191959
+ * The render-failure semantics are identical (propagates as a thrown/rejected
191960
+ * error, same call site as `gtd next`/`gtd status`).
191961
+ */
191962
+ const renderMemory = (stateDef, context) => try_({
191963
+ try: () => stateDef.memory !== void 0 ? renderStateTemplate(stateDef.memory, context) : void 0,
191964
+ catch: (e) => e instanceof Error ? e : new Error(String(e))
191965
+ });
191966
+ /**
191874
191967
  * Render a state's declared `file:` steering-file template (if any) through
191875
191968
  * the SAME template context as its content/`model` — see `renderModel`'s doc
191876
191969
  * comment; the render-failure semantics are identical (propagates as a
@@ -191880,7 +191973,7 @@ const renderFile = (stateDef, context) => try_({
191880
191973
  try: () => stateDef.file !== void 0 ? renderStateTemplate(stateDef.file, context) : void 0,
191881
191974
  catch: (e) => e instanceof Error ? e : new Error(String(e))
191882
191975
  });
191883
- /** Render the resolved rest's declared content (script/prompt/message — never `commit`, since `resolveRest` never rests at a commit state) plus its `model:`/`file:` hints, if declared (see `renderModel`/`renderFile`). `mode:` is a closed literal, never Eta-rendered — passed through verbatim. */
191976
+ /** Render the resolved rest's declared content (script/prompt/message — never `commit`, since `resolveRest` never rests at a commit state) plus its `model:`/`memory:`/`file:` hints, if declared (see `renderModel`/`renderMemory`/`renderFile`). `mode:` is a closed literal, never Eta-rendered — passed through verbatim. */
191884
191977
  const renderRest = (rest, context) => gen(function* () {
191885
191978
  const kind = contentKindOf(rest.stateDef);
191886
191979
  if (kind === void 0) return yield* fail$2(/* @__PURE__ */ new Error(`state "${rest.state}" declares no content — invalid definition`));
@@ -191890,6 +191983,7 @@ const renderRest = (rest, context) => gen(function* () {
191890
191983
  catch: (e) => e instanceof Error ? e : new Error(String(e))
191891
191984
  });
191892
191985
  const model = yield* renderModel(rest.stateDef, context);
191986
+ const memory = yield* renderMemory(rest.stateDef, context);
191893
191987
  const file = yield* renderFile(rest.stateDef, context);
191894
191988
  return {
191895
191989
  state: rest.state,
@@ -191897,6 +191991,7 @@ const renderRest = (rest, context) => gen(function* () {
191897
191991
  kind,
191898
191992
  content,
191899
191993
  ...model !== void 0 ? { model } : {},
191994
+ ...memory !== void 0 ? { memory } : {},
191900
191995
  ...file !== void 0 ? { file } : {},
191901
191996
  ...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {},
191902
191997
  edges: toTemplateEdges(rest.stateDef.on)
@@ -191904,18 +191999,22 @@ const renderRest = (rest, context) => gen(function* () {
191904
191999
  });
191905
192000
  /**
191906
192001
  * Execute a `PatternMachine.step` decision: a `"commit"` decision stages and
191907
- * commits everything pending under the decided subject; a `"squash"`
191908
- * decision renders the commit-state template against the PENDING tree — a
191909
- * render failure REFUSES the step, touching nothing — then soft-resets to
191910
- * the process's start parent, writes ONE commit with the rendered message
191911
- * (via `commitAsIs`, so the still-uncommitted template file is excluded), and
191912
- * discards everything left pending (the template file included). A `"noop"`
191913
- * performs no IO.
191914
- */
191915
- const executeDecision = (git, run, decision, context) => gen(function* () {
192002
+ * commits everything pending under the decided subject, with an optional
192003
+ * `Gtd-Cost: <cost>[ <model>]` trailer (the invocation's `--cost`/`--model`,
192004
+ * persisted in the git log); a `"squash"` decision renders the commit-state
192005
+ * template against the PENDING tree — a render failure REFUSES the step,
192006
+ * touching nothing — then soft-resets to the process's start parent, writes
192007
+ * ONE commit with the rendered message (via `commitAsIs`, so the
192008
+ * still-uncommitted template file is excluded), and discards everything left
192009
+ * pending (the template file included). A squash records no trailer of its own
192010
+ * — the whole-process total and per-model breakdown reach the message through
192011
+ * `it.processCost`/`it.processCostByModel` in the rendered template. A
192012
+ * `"noop"` performs no IO.
192013
+ */
192014
+ const executeDecision = (git, run, decision, context, cost, model) => gen(function* () {
191916
192015
  switch (decision.kind) {
191917
192016
  case "commit":
191918
- yield* git.commitAllWithPrefix(decision.subject);
192017
+ yield* git.commitAllWithPrefix(withCostTrailer(decision.subject, cost, model));
191919
192018
  return {
191920
192019
  kind: "commit",
191921
192020
  subject: decision.subject
@@ -364686,6 +364785,8 @@ const buildFileModeMap = (def, vars, root) => {
364686
364785
  actor: "",
364687
364786
  processDiff: "",
364688
364787
  lastDiff: "",
364788
+ processCost: 0,
364789
+ processCostByModel: [],
364689
364790
  read: (path) => {
364690
364791
  throw new Error(`"file:" is not readable while building the path→mode map (path: ${path})`);
364691
364792
  },
@@ -364937,7 +365038,10 @@ const HELP_TEXT = `Usage: gtd [command] [options]
364937
365038
  Commands:
364938
365039
  step <actor> Authenticate as <actor>, match the resolved rest's
364939
365040
  declared patterns against the pending changes, and commit
364940
- (or squash) the one resulting transition
365041
+ (or squash) the one resulting transition. Pass
365042
+ --cost=<n> (optionally --model=<name>) to record the
365043
+ just-finished invocation's token cost and model on the
365044
+ turn commit (summed into it.processCost/processCostByModel)
364941
365045
  next Print the resolved rest's rendered script/prompt/message
364942
365046
  (no mutation)
364943
365047
  run Execute the resolved rest's emitted script, then step its
@@ -364951,6 +365055,8 @@ Commands:
364951
365055
 
364952
365056
  Options:
364953
365057
  --json Output structured JSON instead of plain text
365058
+ --cost=<n> (gtd step only) record the invocation's token cost
365059
+ --model=<name> (gtd step only, with --cost) tag that cost's model
364954
365060
  --version, -v Print version and exit
364955
365061
  --help, -h Print this help and exit
364956
365062
  `;
@@ -364966,6 +365072,58 @@ const markEnveloped = (error) => Object.assign(error, { [ENVELOPED]: true });
364966
365072
  const isEnveloped = (error) => typeof error === "object" && error !== null && error[ENVELOPED] === true;
364967
365073
  /** Non-flag positional arguments past the subcommand name (argv[3..]). */
364968
365074
  const commandArgs = (argv) => argv.slice(3).filter((a) => a.length > 0 && !a.startsWith("--"));
365075
+ const COST_FLAG = "--cost=";
365076
+ /**
365077
+ * Parse the optional `--cost=<n>` flag (only `gtd step` accepts it — see
365078
+ * `makeProgram`). `<n>` is the token cost of the invocation that produced the
365079
+ * pending changes, recorded as a `Gtd-Cost:` trailer on the turn commit. Must
365080
+ * be a non-negative finite number; a bare `--cost` (no `=`) or a non-numeric/
365081
+ * negative value is a usage error. Returns `undefined` when the flag is absent.
365082
+ */
365083
+ const parseCostFlag = (argv) => {
365084
+ if (argv.slice(2).includes("--cost")) return fail$2(/* @__PURE__ */ new Error("gtd: --cost requires a value — use --cost=<number>"));
365085
+ const flag = argv.slice(2).find((a) => a.startsWith(COST_FLAG));
365086
+ if (flag === void 0) return succeed$3(void 0);
365087
+ const raw = flag.slice(7);
365088
+ const n = Number(raw);
365089
+ if (raw.trim() === "" || !Number.isFinite(n) || n < 0) return fail$2(/* @__PURE__ */ new Error(`gtd: --cost must be a non-negative number — got "${raw}"`));
365090
+ return succeed$3(n);
365091
+ };
365092
+ const MODEL_FLAG = "--model=";
365093
+ /**
365094
+ * Parse the optional `--model=<name>` flag (only `gtd step`, and only
365095
+ * alongside `--cost` — see `makeProgram`). `<name>` tags the recorded cost
365096
+ * with the model the invocation ran on, appended to the `Gtd-Cost:` trailer
365097
+ * and grouped in `it.processCostByModel`. Must be non-empty and single-line
365098
+ * (it rides on one trailer line); a bare `--model` (no `=`) or an empty/
365099
+ * multiline value is a usage error. Returns `undefined` when the flag is absent.
365100
+ */
365101
+ const parseModelFlag = (argv) => {
365102
+ if (argv.slice(2).includes("--model")) return fail$2(/* @__PURE__ */ new Error("gtd: --model requires a value — use --model=<name>"));
365103
+ const flag = argv.slice(2).find((a) => a.startsWith(MODEL_FLAG));
365104
+ if (flag === void 0) return succeed$3(void 0);
365105
+ const raw = flag.slice(8);
365106
+ if (raw.trim() === "" || /[\r\n]/.test(raw)) return fail$2(/* @__PURE__ */ new Error("gtd: --model must be a non-empty, single-line value"));
365107
+ return succeed$3(raw);
365108
+ };
365109
+ /**
365110
+ * Parse and validate the `--cost`/`--model` step flags together. Both are
365111
+ * orthogonal to `--json` but only meaningful to `gtd step` — rejected on any
365112
+ * other command rather than silently ignored (same discipline as `--json` on
365113
+ * `format`) — and `--model` requires `--cost` (a model tag with no token count
365114
+ * records nothing to sum).
365115
+ */
365116
+ const parseStepFlags = (argv, positional) => gen(function* () {
365117
+ const cost = yield* parseCostFlag(argv);
365118
+ const model = yield* parseModelFlag(argv);
365119
+ if (cost !== void 0 && positional !== "step") return yield* fail$2(/* @__PURE__ */ new Error("gtd: --cost is only valid for `gtd step`"));
365120
+ if (model !== void 0 && positional !== "step") return yield* fail$2(/* @__PURE__ */ new Error("gtd: --model is only valid for `gtd step`"));
365121
+ if (model !== void 0 && cost === void 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd: --model requires --cost — it tags the recorded cost with the model that ran"));
365122
+ return {
365123
+ cost,
365124
+ model
365125
+ };
365126
+ });
364969
365127
  /** Rejects extra positional arguments for a subcommand that takes none (`status`, `run`). */
364970
365128
  const rejectExtraArgs = (command, argv) => {
364971
365129
  const args = commandArgs(argv);
@@ -365014,6 +365172,8 @@ const resolveRestContext = (git) => gen(function* () {
365014
365172
  context: yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars, rest.stateDef.on)
365015
365173
  };
365016
365174
  });
365175
+ /** The user-facing message for a `step` refusal — out-of-turn names the awaited actor, no-match names every declared pattern. */
365176
+ const formatStepRefusal = (invoker, refusal) => refusal.reason === "out-of-turn" ? `gtd step ${invoker}: out of turn — "${refusal.state}" awaits ${refusal.awaits}` : `gtd step ${invoker}: no declared pattern matches the pending changes at "${refusal.state}" — declared patterns: ${refusal.patterns.length > 0 ? refusal.patterns.join(", ") : "(none)"}`;
365017
365177
  /**
365018
365178
  * Authenticate `invoker` against the resolved rest and perform the one
365019
365179
  * resulting transition (commit or squash), shared by `gtd step` and
@@ -365022,7 +365182,7 @@ const resolveRestContext = (git) => gen(function* () {
365022
365182
  * `subject: null` rather than failing (exit zero, per the plan's "clean
365023
365183
  * no-op exits zero").
365024
365184
  */
365025
- const stepAsActor = (invoker) => gen(function* () {
365185
+ const stepAsActor = (invoker, cost, model) => gen(function* () {
365026
365186
  const git = yield* GitService;
365027
365187
  const config = yield* ConfigService;
365028
365188
  const worktree = yield* WorktreeReader;
@@ -365034,37 +365194,40 @@ const stepAsActor = (invoker) => gen(function* () {
365034
365194
  changes,
365035
365195
  processTrace: run.trace
365036
365196
  });
365037
- if (decision.kind === "refusal") {
365038
- const message = decision.reason === "out-of-turn" ? `gtd step ${invoker}: out of turn — "${decision.state}" awaits ${decision.awaits}` : `gtd step ${invoker}: no declared pattern matches the pending changes at "${decision.state}" — declared patterns: ${decision.patterns.length > 0 ? decision.patterns.join(", ") : "(none)"}`;
365039
- return yield* fail$2(new Error(message));
365040
- }
365197
+ if (decision.kind === "refusal") return yield* fail$2(new Error(formatStepRefusal(invoker, decision)));
365041
365198
  if (decision.kind === "noop") return {
365042
365199
  state: decision.state,
365043
- subject: null
365200
+ subject: null,
365201
+ cost: null,
365202
+ model: null
365044
365203
  };
365045
365204
  const executable = decision;
365046
365205
  const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
365047
365206
  const renderedState = decision.kind === "squash" ? decision.state : rest.state;
365048
- const outcome = yield* executeDecision(git, run, executable, yield* buildTemplateContext(git, worktree.read, renderedState, invoker, run, vars, rest.def.states[renderedState]?.on));
365207
+ const outcome = yield* executeDecision(git, run, executable, yield* buildTemplateContext(git, worktree.read, renderedState, invoker, run, vars, rest.def.states[renderedState]?.on, cost ?? 0, model), cost, model);
365049
365208
  return {
365050
365209
  state: rest.state,
365051
- subject: outcome.kind === "noop" ? null : outcome.subject
365210
+ subject: outcome.kind === "noop" ? null : outcome.subject,
365211
+ cost: cost ?? null,
365212
+ model: model ?? null
365052
365213
  };
365053
365214
  });
365054
365215
  /** Renders `stepAsActor`'s result the same way for both `gtd step` and `gtd run`. */
365055
365216
  const reportStepResult = (result, json, write) => {
365056
365217
  if (json) write(JSON.stringify({
365057
365218
  state: result.state,
365058
- subject: result.subject
365219
+ subject: result.subject,
365220
+ ...result.cost !== null ? { cost: result.cost } : {},
365221
+ ...result.model !== null ? { model: result.model } : {}
365059
365222
  }) + "\n");
365060
365223
  else write(result.subject !== null ? `committed: ${result.subject}\n` : `nothing to do at "${result.state}"\n`);
365061
365224
  };
365062
- /** `gtd step <actor>`: authenticate as `<actor>` and perform the one resulting transition. */
365063
- const runStepCommand = (argv, json, write) => gen(function* () {
365225
+ /** `gtd step <actor> [--cost=<n>] [--model=<name>]`: authenticate as `<actor>` and perform the one resulting transition, recording `--cost`/`--model` as a `Gtd-Cost:` trailer. */
365226
+ const runStepCommand = (argv, json, write, cost, model) => gen(function* () {
365064
365227
  const args = commandArgs(argv);
365065
365228
  if (args.length === 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd step: missing actor argument"));
365066
365229
  if (args.length > 1) return yield* fail$2(/* @__PURE__ */ new Error(`gtd step: too many arguments — expected one actor, got: ${args.join(", ")}`));
365067
- const result = yield* stepAsActor(args[0]);
365230
+ const result = yield* stepAsActor(args[0], cost, model);
365068
365231
  reportStepResult(result, json, write);
365069
365232
  });
365070
365233
  /** `gtd next [--json]`: pure emitter of the resolved rest's rendered content (no mutation). */
@@ -365078,6 +365241,7 @@ const runNextCommand = (json, write) => gen(function* () {
365078
365241
  kind: rendered.kind,
365079
365242
  content: rendered.content,
365080
365243
  ...rendered.model !== void 0 ? { model: rendered.model } : {},
365244
+ ...rendered.memory !== void 0 ? { memory: rendered.memory } : {},
365081
365245
  ...rendered.file !== void 0 ? { file: rendered.file } : {},
365082
365246
  ...rendered.mode !== void 0 ? { mode: rendered.mode } : {},
365083
365247
  ...rendered.edges.length > 0 ? { edges: rendered.edges } : {}
@@ -365122,25 +365286,43 @@ const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
365122
365286
  pattern: matchedRow?.[0] ?? null
365123
365287
  };
365124
365288
  });
365125
- /** `gtd status --json`'s emission — `{state, actor, changes, model?, file?, mode?}`. */
365126
- const writeStatusJson = (write, rest, statusChanges, model, file) => {
365289
+ /**
365290
+ * True when the per-model breakdown adds information beyond the `Cost:` total:
365291
+ * more than one model, or a single model that carries an actual `--model` tag
365292
+ * (a lone `unspecified` bucket just restates the total, so it's suppressed).
365293
+ */
365294
+ const breakdownIsInformative = (byModel) => byModel.length > 1 || byModel.length === 1 && byModel[0].model !== "unspecified";
365295
+ /** The plain-text `Cost:` line(s) for `gtd status` — empty when nothing recorded, plus an indented per-model split when informative. */
365296
+ const costStatusLines = (cost, byModel) => {
365297
+ if (cost <= 0) return [];
365298
+ const lines = [`Cost: ${cost}`];
365299
+ if (breakdownIsInformative(byModel)) for (const m of byModel) lines.push(` ${m.model}: ${m.cost}`);
365300
+ return lines;
365301
+ };
365302
+ /** `gtd status --json`'s emission — `{state, actor, changes, model?, memory?, file?, mode?, cost?, costByModel?, edges?}`. */
365303
+ const writeStatusJson = (write, rest, statusChanges, model, memory, file, cost, costByModel) => {
365127
365304
  const edges = toTemplateEdges(rest.stateDef.on);
365128
365305
  write(JSON.stringify({
365129
365306
  state: rest.state,
365130
365307
  actor: rest.actor,
365131
365308
  changes: statusChanges,
365132
365309
  ...model !== void 0 ? { model } : {},
365310
+ ...memory !== void 0 ? { memory } : {},
365133
365311
  ...file !== void 0 ? { file } : {},
365134
365312
  ...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {},
365313
+ ...cost > 0 ? { cost } : {},
365314
+ ...cost > 0 ? { costByModel } : {},
365135
365315
  ...edges.length > 0 ? { edges } : {}
365136
365316
  }) + "\n");
365137
365317
  };
365138
- /** `gtd status`'s plain-text emission — `State:`/`Awaits:`/`Model:`/`File:`/`Mode:`/`Pending:` lines. */
365139
- const writeStatusPlain = (write, rest, statusChanges, model, file) => {
365318
+ /** `gtd status`'s plain-text emission — `State:`/`Awaits:`/`Model:`/`Memory:`/`File:`/`Mode:`/`Cost:`/`Pending:` lines. */
365319
+ const writeStatusPlain = (write, rest, statusChanges, model, memory, file, cost, costByModel) => {
365140
365320
  const lines = [`State: ${rest.state}`, `Awaits: ${rest.actor}`];
365141
365321
  if (model !== void 0) lines.push(`Model: ${model}`);
365322
+ if (memory !== void 0) lines.push(`Memory: ${memory}`);
365142
365323
  if (file !== void 0) lines.push(`File: ${file}`);
365143
365324
  if (rest.stateDef.mode !== void 0) lines.push(`Mode: ${rest.stateDef.mode}`);
365325
+ lines.push(...costStatusLines(cost, costByModel));
365144
365326
  if (statusChanges.length === 0) lines.push("Pending: (clean)");
365145
365327
  else {
365146
365328
  lines.push("Pending:");
@@ -365155,10 +365337,11 @@ const runStatusCommand = (argv, json, write) => gen(function* () {
365155
365337
  const { rest, context } = yield* resolveRestContext(git);
365156
365338
  const changes = yield* pendingChanges(git);
365157
365339
  const model = yield* renderModel(rest.stateDef, context);
365340
+ const memory = yield* renderMemory(rest.stateDef, context);
365158
365341
  const file = yield* renderFile(rest.stateDef, context);
365159
365342
  const statusChanges = computeStatusChanges(rest.stateDef.on ?? [], changes);
365160
- if (json) writeStatusJson(write, rest, statusChanges, model, file);
365161
- else writeStatusPlain(write, rest, statusChanges, model, file);
365343
+ if (json) writeStatusJson(write, rest, statusChanges, model, memory, file, context.processCost, context.processCostByModel);
365344
+ else writeStatusPlain(write, rest, statusChanges, model, memory, file, context.processCost, context.processCostByModel);
365162
365345
  });
365163
365346
  /**
365164
365347
  * `gtd mermaid`: pure emitter of the active workflow's SHAPE (not the
@@ -365224,9 +365407,9 @@ const assertRunningFromRepoRoot = (git, fs) => gen(function* () {
365224
365407
  if ((yield* fs.realPath(topLevel)) !== (yield* fs.realPath(process.cwd()))) return yield* fail$2(/* @__PURE__ */ new Error(`gtd must be run from the repository root (${topLevel}); the current directory is ${process.cwd()}`));
365225
365408
  });
365226
365409
  /** Dispatches to the named `run*Command` handler for every known subcommand. */
365227
- const dispatchKnownSubcommand = (sub, argv, json, write) => {
365410
+ const dispatchKnownSubcommand = (sub, argv, json, write, cost, model) => {
365228
365411
  switch (sub) {
365229
- case "step": return runStepCommand(argv, json, write);
365412
+ case "step": return runStepCommand(argv, json, write, cost, model);
365230
365413
  case "next": return runNextCommand(json, write);
365231
365414
  case "run": return runRunCommand(argv, json, write);
365232
365415
  case "status": return runStatusCommand(argv, json, write);
@@ -365254,8 +365437,9 @@ function makeProgram(opts = {}) {
365254
365437
  const positional = argv.slice(2).find((a) => !a.startsWith("--"));
365255
365438
  return gen(function* () {
365256
365439
  if (runVersionOrHelp(argv, write)) return;
365257
- const unknownOption = argv.slice(2).find((a) => a.startsWith("--") && a !== "--json");
365440
+ const unknownOption = argv.slice(2).find((a) => a.startsWith("--") && a !== "--json" && a !== "--cost" && a !== "--model" && !a.startsWith(COST_FLAG) && !a.startsWith(MODEL_FLAG));
365258
365441
  if (unknownOption !== void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd: unknown option '${unknownOption}' — see \`gtd --help\``));
365442
+ const { cost, model } = yield* parseStepFlags(argv, positional);
365259
365443
  if (positional === "format") return yield* runFormatCommand(argv, json);
365260
365444
  if (positional === "lsp") return yield* runLspCommand(argv, json);
365261
365445
  const sub = yield* requireKnownSubcommand(positional, json, write);
@@ -365264,7 +365448,7 @@ function makeProgram(opts = {}) {
365264
365448
  yield* assertRunningFromRepoRoot(git, fs);
365265
365449
  yield* closeReviewWindow;
365266
365450
  yield* (yield* ConfigInit).ensure;
365267
- const outcome = yield* either(dispatchKnownSubcommand(sub, argv, json, write));
365451
+ const outcome = yield* either(dispatchKnownSubcommand(sub, argv, json, write, cost, model));
365268
365452
  const rearm = yield* either(openReviewWindow);
365269
365453
  if (isLeft(outcome)) return yield* fail$2(outcome.left);
365270
365454
  if (isLeft(rearm)) return yield* fail$2(rearm.left);
package/docs/cli.md CHANGED
@@ -6,7 +6,10 @@ Usage: gtd [command] [options]
6
6
  Commands:
7
7
  step <actor> Authenticate as <actor>, match the resolved rest's
8
8
  declared patterns against the pending changes, and commit
9
- (or squash) the one resulting transition
9
+ (or squash) the one resulting transition. Pass
10
+ --cost=<n> (optionally --model=<name>) to record the
11
+ just-finished invocation's token cost and model on the
12
+ turn commit (summed into it.processCost/processCostByModel)
10
13
  next Print the resolved rest's rendered script/prompt/message
11
14
  (no mutation)
12
15
  run Execute the resolved rest's emitted script, then step its
@@ -20,6 +23,8 @@ Commands:
20
23
 
21
24
  Options:
22
25
  --json Output structured JSON instead of plain text
26
+ --cost=<n> (gtd step only) record the invocation's token cost
27
+ --model=<name> (gtd step only, with --cost) tag that cost's model
23
28
  --version, -v Print version and exit
24
29
  --help, -h Print this help and exit
25
30
  ```
@@ -32,11 +37,15 @@ without touching the repository. Every other command must be run from the
32
37
  history relative to cwd, so it refuses with a clear error if invoked from a
33
38
  subdirectory.
34
39
 
35
- `--json` is the only long option. Any other `--` option (including a typo like
36
- `--jsn`) is rejected with a usage error rather than silently ignored, so a
37
- mistyped flag can never degrade a JSON caller to plain-text mode.
40
+ `--json`, `--cost=<n>`, and `--model=<name>` (the latter two only for
41
+ `gtd step`) are the only long options. Any other `--` option (including a typo
42
+ like `--jsn`) is rejected with a usage error rather than silently ignored, so a
43
+ mistyped flag can never degrade a JSON caller to plain-text mode. A bare
44
+ `--cost`/`--model` with no value, a non-numeric or negative `--cost`, an empty
45
+ `--model`, `--model` without `--cost`, or either flag on any command other than
46
+ `gtd step` are all usage errors.
38
47
 
39
- ## `gtd step <actor>`
48
+ ## `gtd step <actor> [--cost=<n>] [--model=<name>]`
40
49
 
41
50
  Authenticates `<actor>` against the resolved rest and performs the ONE resulting
42
51
  transition. Unlike v2, there is no fixpoint chain to drive: the pattern
@@ -45,6 +54,16 @@ authors at most one commit (a normal turn) or performs one squash
45
54
  (§[STATES.md](../STATES.md#8-the-squash-lifecycle)) — never both, never a chain
46
55
  of several. A caller that wants several transitions issues several invocations.
47
56
 
57
+ `--cost=<n>` records the token cost of the invocation that produced the pending
58
+ changes (a non-negative number), and the optional `--model=<name>` tags it with
59
+ the model that ran, as a `Gtd-Cost: <n> <model>` trailer on the turn commit —
60
+ persisted in the git log, summed across the process into `it.processCost` and
61
+ grouped by model into `it.processCostByModel` (see
62
+ [Configuration: Token cost](configuration.md#token-cost)). `--model` requires
63
+ `--cost`. On a squash they are folded into the process total/breakdown the
64
+ `commit:` template renders rather than written as a trailer. Both are orthogonal
65
+ to `--json`.
66
+
48
67
  - **Out-of-turn refusal** — `<actor>` isn't the resolved state's declared actor:
49
68
  exit non-zero, zero commits.
50
69
  `gtd step <actor>: out of turn — "<state>" awaits <awaited-actor>`
@@ -69,10 +88,17 @@ or, at a no-op:
69
88
  nothing to do at "idle"
70
89
  ```
71
90
 
72
- `--json` emits `{state, subject}` — `subject` is `null` at a no-op:
91
+ `--json` emits `{state, subject}` — `subject` is `null` at a no-op — plus
92
+ `cost`/`model` keys echoing what `--cost`/`--model` recorded (each omitted when
93
+ not passed):
73
94
 
74
95
  ```json
75
- { "state": "grilling", "subject": "gtd(human): grilling" }
96
+ {
97
+ "state": "grilling",
98
+ "subject": "gtd(human): grilling",
99
+ "cost": 1450,
100
+ "model": "claude-opus-4-8"
101
+ }
76
102
  ```
77
103
 
78
104
  ## `gtd next [--json]`
@@ -86,9 +112,9 @@ process in the same step that entered it — see
86
112
  [STATES.md §5](../STATES.md#5-resolution)).
87
113
 
88
114
  Plain mode prints the rendered content verbatim (with exactly one trailing
89
- newline) — never the `model`/`file`/`mode`/`edges` structured keys, which are
90
- JSON-only. `--json` emits
91
- `{state, actor, kind, content, model?, file?, mode?, edges?}`:
115
+ newline) — never the `model`/`memory`/`file`/`mode`/`edges` structured keys,
116
+ which are JSON-only. `--json` emits
117
+ `{state, actor, kind, content, model?, memory?, file?, mode?, edges?}`:
92
118
 
93
119
  ```json
94
120
  {
@@ -109,6 +135,12 @@ JSON-only. `--json` emits
109
135
  [Configuration](configuration.md#model--the-opaque-harness-hint-template-rendered)),
110
136
  present only when the state declares one; **omitted entirely** (never `null`)
111
137
  when unset.
138
+ - `memory` — the state's opaque `memory:` scope label, RENDERED the same way
139
+ (see
140
+ [Configuration](configuration.md#memory--the-memory-scope-label-template-rendered)).
141
+ A memory-aware driver retains an agent's memory across consecutive agent turns
142
+ sharing this label and starts fresh when it changes. Present only when the
143
+ state declares one; **omitted entirely** (never `null`) when unset.
112
144
  - `file` — the state's declared steering file, RENDERED the same way; `mode` —
113
145
  its format, verbatim (`"qa"` | `"review"`) (see
114
146
  [Configuration](configuration.md#filemode--the-steering-file-association)).
@@ -147,12 +179,12 @@ Pure, read-only dry-run reporter — the same resolution `gtd next` performs, bu
147
179
  reporting the resolved state/actor and, for every pending change, which declared
148
180
  `on` pattern (if any) matches it — no mutation, and no CONTENT rendering (the
149
181
  `script`/`prompt`/`message`/`commit` template is never rendered here, unlike
150
- `gtd next`). It DOES render the resolved state's `model:`/`file:` hints (if
151
- declared) through the same `it.vars`-carrying template context `gtd next` uses —
152
- see
182
+ `gtd next`). It DOES render the resolved state's `model:`/`memory:`/`file:`
183
+ hints (if declared) through the same `it.vars`-carrying template context
184
+ `gtd next` uses — see
153
185
  [Configuration](configuration.md#model--the-opaque-harness-hint-template-rendered)
154
- — so a templated `model:`/`file:` failing to render fails `gtd status` too,
155
- exactly like it would fail `gtd next`.
186
+ — so a templated `model:`/`memory:`/`file:` failing to render fails `gtd status`
187
+ too, exactly like it would fail `gtd next`.
156
188
 
157
189
  ```
158
190
  State: working
@@ -164,16 +196,22 @@ Pending:
164
196
 
165
197
  or, on a clean tree: `Pending: (clean)`. A `Model: <value>` line appears right
166
198
  after `Awaits:` when the resolved state declares a `model:` hint, and
167
- `File: <value>`/`Mode: <value>` lines appear after that (in that order) when
168
- declared — each independently, only when set.
199
+ `Memory: <value>`/`File: <value>`/`Mode: <value>` lines appear after that (in
200
+ that order) when declared — each independently, only when set. A `Cost: <n>`
201
+ line appears after those when the current process has accumulated any
202
+ `Gtd-Cost:` (see [`gtd step --cost`](#gtd-step-actor---costn-model-name)),
203
+ omitted when the running total is `0`; when the breakdown adds information (more
204
+ than one model, or a single tagged model), indented ` <model>: <cost>` lines
205
+ follow it.
169
206
 
170
207
  `--json` emits
171
- `{state, actor, changes: [{status, path, pattern}], model?, file?, mode?, edges?}`
172
- — `pattern` is `null` when no declared row matches that change; `model`/`file`/
173
- `mode` are present only when the resolved state declares them (omitted entirely,
174
- never `null`, otherwise); `edges` is the resting state's `on` edges as
175
- `[{ pattern, target, describe? }]` (same as `gtd next --json`), omitted when the
176
- state has no `on`:
208
+ `{state, actor, changes: [{status, path, pattern}], model?, memory?, file?, mode?, cost?, costByModel?, edges?}`
209
+ — `pattern` is `null` when no declared row matches that change;
210
+ `model`/`memory`/`file`/`mode` are present only when the resolved state declares
211
+ them; `cost`/`costByModel` (the `[{model, cost}]` breakdown) only when the
212
+ running process total is above `0`; and `edges` is the resting state's `on`
213
+ edges as `[{ pattern, target, describe? }]` (same as `gtd next --json`), omitted
214
+ when the state has no `on` (all omitted entirely, never `null`, otherwise):
177
215
 
178
216
  ```json
179
217
  {
@@ -61,6 +61,7 @@ workflow:
61
61
  max: <number>
62
62
  otherwise: <targetState>
63
63
  model: <string> # optional, opaque harness hint — forbidden on a commit state
64
+ memory: <string> # optional, opaque memory-scope label — forbidden on a commit state
64
65
  file: <string> # optional, an Eta template naming the state's steering file — forbidden on a commit state
65
66
  mode: qa | review # optional, requires "file" — forbidden on a commit state
66
67
  ```
@@ -167,6 +168,61 @@ committed.
167
168
  value) only when the resolved state declares one — it is **omitted entirely**,
168
169
  never emitted as `null`, when unset.
169
170
 
171
+ ### `memory:` — the memory-scope label, template-rendered
172
+
173
+ A state may declare `memory: <string>` — an OPAQUE label gtd never interprets;
174
+ it is passed through so a memory-aware driving loop can decide when an agent
175
+ turn continues from the previous turn's memory and when it starts fresh. The
176
+ contract is a **comparison, not a command**: two agent turns that emit the SAME
177
+ `memory` value belong to the same memory scope (the driver retains the agent's
178
+ memory across them), and a change in value — or the very first agent turn — is
179
+ where the driver starts fresh. Unset means "use the harness's default."
180
+ Forbidden on a commit state (never at rest):
181
+
182
+ ```yaml
183
+ workflow:
184
+ states:
185
+ grilling:
186
+ actor: agent
187
+ memory: plan
188
+ prompt: develop the plan; ask open questions
189
+ on:
190
+ "* **": grilling-answer
191
+ grilling-answer:
192
+ actor: human
193
+ message: answer the open questions
194
+ on:
195
+ "C": building
196
+ "* **": grilling
197
+ building:
198
+ actor: agent
199
+ memory: build # a different scope — implementation starts fresh
200
+ prompt: implement the settled plan
201
+ on:
202
+ "* **": done
203
+ done:
204
+ commit: "feat: done"
205
+ ```
206
+
207
+ Because `grilling` re-enters itself around the answer loop, every lap emits
208
+ `memory: plan` and the planner keeps its accumulated exploration; the move to
209
+ `building` emits a different label (`build`), which is where the driver clears
210
+ memory for a fresh implementation turn. A loop of same-labelled turns retains; a
211
+ differently-labelled state at a phase boundary clears. This is exactly how the
212
+ bundled default scopes its four agent states (`plan`/`build`/`fix`/`review` —
213
+ see [STATES.md §10](../STATES.md#10-the-bundled-default-workflow)), and how the
214
+ loop driver reads it is spelled out in `skills/loop/SKILL.md`.
215
+
216
+ Like `model`, `memory:` is rendered as an Eta template through the same context
217
+ as the state's content — a plain label (`plan`) passes through unchanged, while
218
+ `memory: "<%= it.vars.planScope %>"` resolves against the merged `it.vars`. A
219
+ render failure behaves exactly like a content render failure:
220
+ `gtd next`/`gtd status` error out, nothing committed.
221
+ `gtd next --json`/`gtd status --json` include a `"memory"` key (the RENDERED
222
+ value) only when the resolved state declares one — **omitted entirely**, never
223
+ `null`, when unset. Plain `gtd status` prints a `Memory:` line (right after
224
+ `Model:`, when present).
225
+
170
226
  ### `file:`/`mode:` — the steering-file association
171
227
 
172
228
  A state may additionally declare `file:` — an Eta template naming THE steering
@@ -263,26 +319,75 @@ head is preserved under `refs/gtd/review-head` (the base under
263
319
 
264
320
  ### Template variables
265
321
 
266
- Every `script`/`prompt`/`message`/`commit`/`model` template is rendered as an
267
- Eta template (`it.<name>`) with:
268
-
269
- | Variable | Meaning |
270
- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
271
- | `startCommit` | The hash the current process started from (before its first turn). |
272
- | `currentCommit` | HEAD's hash at render time. |
273
- | `previousCommit` | The hash before the last transition (HEAD's parent, in-process). |
274
- | `state` | The state whose content is being rendered. |
275
- | `actor` | The actor this render is for. |
276
- | `processDiff` | `startCommit..HEAD` plus the pending working-tree diff. |
277
- | `lastDiff` | The diff of the last transition alone. |
278
- | `read(path)` | Reads a working-tree file (pending contents, not HEAD's) by repo-relative path. Throws for a missing/unreadable path — for a `commit:` template, that throw refuses the step (see [STATES.md §8](../STATES.md#8-the-squash-lifecycle)). |
279
- | `vars` | The merged three-layer variable map, always a flat `Record<string, string>` — see ["Variables"](#variables) below. |
322
+ Every `script`/`prompt`/`message`/`commit`/`model`/`memory`/`file` template is
323
+ rendered as an Eta template (`it.<name>`) with:
324
+
325
+ | Variable | Meaning |
326
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
327
+ | `startCommit` | The hash the current process started from (before its first turn). |
328
+ | `currentCommit` | HEAD's hash at render time. |
329
+ | `previousCommit` | The hash before the last transition (HEAD's parent, in-process). |
330
+ | `state` | The state whose content is being rendered. |
331
+ | `actor` | The actor this render is for. |
332
+ | `processDiff` | `startCommit..HEAD` plus the pending working-tree diff. |
333
+ | `lastDiff` | The diff of the last transition alone. |
334
+ | `processCost` | The accumulated token cost of the current process — the sum of every `Gtd-Cost:` trailer recorded by `gtd step <actor> --cost=<n>` across its turn commits, plus the in-flight step's own `--cost` (so a `commit:` squash template sees the whole-process total). A number, `0` when none recorded. See ["Token cost"](#token-cost). |
335
+ | `processCostByModel` | The same accumulated cost broken down per model: an array of `{ model, cost }`, highest-cost first, one entry per distinct `--model` tag (costs recorded with no `--model` group under `"unspecified"`). A squash template iterates it to itemize the feature's cost per model. Empty when none recorded. See ["Token cost"](#token-cost). |
336
+ | `read(path)` | Reads a working-tree file (pending contents, not HEAD's) by repo-relative path. Throws for a missing/unreadable path — for a `commit:` template, that throw refuses the step (see [STATES.md §8](../STATES.md#8-the-squash-lifecycle)). |
337
+ | `vars` | The merged three-layer variable map, always a flat `Record<string, string>` — see ["Variables"](#variables) below. |
338
+
339
+ ### Token cost
340
+
341
+ A loop driver that knows how many tokens the invocation it just drove cost — and
342
+ on which model — can record both with
343
+ `gtd step <actor> --cost=<n> [--model=<name>]` (`<n>` a non-negative number).
344
+ gtd appends them to that turn's commit as a `Gtd-Cost: <n> <model>` trailer — a
345
+ blank line then the trailer, below the untouched `gtd(<actor>): <state>` subject
346
+ — so the per-turn cost and model are persisted in the git log:
347
+
348
+ ```
349
+ gtd(agent): reviewing
350
+
351
+ Gtd-Cost: 1450 claude-opus-4-8
352
+ ```
353
+
354
+ `--model` is optional and requires `--cost` (a model tag with no token count
355
+ records nothing to sum); a cost recorded without a model is grouped under
356
+ `"unspecified"`. `gtd status` shows the process's running total and, when the
357
+ breakdown adds information, a per-model split (a `Cost:` line with indented
358
+ `<model>: <cost>` lines / a `cost` number and a `costByModel` array, omitted
359
+ when nothing has been recorded). `gtd step --json` echoes what it recorded
360
+ (`{ "state": …, "subject": …, "cost": 1450, "model": "claude-opus-4-8" }`).
361
+
362
+ Every template sees the running total as `it.processCost` and the per-model
363
+ breakdown as `it.processCostByModel` (the table above): the current process's
364
+ turn-commit entries, plus the in-flight step's own `--cost`/`--model`. Their
365
+ intended use is a `commit:` squash template, which renders against the pending
366
+ tree as the process collapses — so the whole feature's total AND its per-model
367
+ itemization (including the squashing step itself) land in the squash message
368
+ even though the intermediate per-turn trailers are discarded with the turns they
369
+ rode on:
370
+
371
+ ```yaml
372
+ done:
373
+ commit: |
374
+ feat: ship it
375
+
376
+ Total token cost: <%= it.processCost %>
377
+ <% it.processCostByModel.forEach(function(m){ %>
378
+ - <%= m.model %>: <%= m.cost %>
379
+ <% }) %>
380
+ ```
381
+
382
+ `--cost`/`--model` are accepted only by `gtd step` (a usage error on any other
383
+ command), and gtd never interprets the number's unit (tokens, cents, whatever
384
+ the driver records) or the model name — it only records, sums, and groups them.
280
385
 
281
386
  ## Variables
282
387
 
283
- Every template — `script`/`prompt`/`message`/`commit`, and now `model` — sees
284
- `it.vars`: a flat `Record<string, string>` assembled from three layers, **later
285
- wins**:
388
+ Every template — `script`/`prompt`/`message`/`commit`, and `model`/`memory`/
389
+ `file` — sees `it.vars`: a flat `Record<string, string>` assembled from three
390
+ layers, **later wins**:
286
391
 
287
392
  1. **The workflow's own `vars:` key** (sibling to `states:`, shown above) — the
288
393
  workflow author's declared defaults. The bundled default workflow declares
@@ -405,9 +405,25 @@ workflow:
405
405
  "M .gtd/COMMIT_MSG.md": done
406
406
 
407
407
  done:
408
- commit: '<%~ it.read(".gtd/COMMIT_MSG.md") %>'
408
+ commit: |
409
+ <%~ it.read(".gtd/COMMIT_MSG.md") %>
410
+
411
+ Token cost: <%= it.processCost %>
412
+ <% it.processCostByModel.forEach(function(m){ %>
413
+ - <%= m.model %>: <%= m.cost %>
414
+ <% }) %>
409
415
  ````
410
416
 
417
+ The `done` commit appends a token-cost summary to the squashed message: if the
418
+ loop driving this workflow passes `gtd step <actor> --cost=<n> --model=<name>`
419
+ after each agent turn, `it.processCost` is the summed cost of the whole cycle
420
+ (including the `squashing` step itself) and `it.processCostByModel` itemizes it
421
+ per model — so the single squash commit records the feature's complete cost,
422
+ broken down by which model spent what, even though the per-turn trailers are
423
+ discarded with the turns they collapsed. Drop these lines if your driver doesn't
424
+ track cost — `it.processCost` is `0` and `it.processCostByModel` empty then. See
425
+ [Configuration: Token cost](../configuration.md#token-cost).
426
+
411
427
  A clean `gtd step human` at `grilling-answer`/`architecting-answer` (the `C`
412
428
  event) accepts the current phase's draft and moves on; any other pending change
413
429
  loops back for another round. `picking` is a `script` state whose only job is to
package/docs/loop.md CHANGED
@@ -5,7 +5,10 @@ gtd (v3, the pattern machine) exposes three commands a loop driver combines:
5
5
  - **`gtd step <actor>`** — authenticate as `<actor>`, match the resolved rest's
6
6
  declared patterns against the pending changes, and commit (or squash) the one
7
7
  resulting transition. Performs AT MOST one transition — there is no fixpoint
8
- chain to drive.
8
+ chain to drive. Pass `--cost=<n>` (optionally `--model=<name>`) to record the
9
+ just-finished invocation's token cost and model on the turn commit; gtd sums
10
+ them across the process into `it.processCost`/`it.processCostByModel` (see
11
+ [Configuration: Token cost](configuration.md#token-cost)).
9
12
  - **`gtd next`** — print the resolved rest's rendered script/prompt/message,
10
13
  without mutating anything.
11
14
  - **`gtd run`** — execute the resolved rest's emitted script (only for a
@@ -15,8 +18,8 @@ gtd (v3, the pattern machine) exposes three commands a loop driver combines:
15
18
  A loop is a simple cycle:
16
19
 
17
20
  1. Run `gtd next --json` — see [`cli.md`](cli.md#gtd-next---json) for the exact
18
- `{state, actor, kind, content, model?}` field shape. `kind` is the dispatch
19
- key: `"message"` → halt (a human rest); `"script"` → run `gtd run`;
21
+ `{state, actor, kind, content, model?, memory?}` field shape. `kind` is the
22
+ dispatch key: `"message"` → halt (a human rest); `"script"` → run `gtd run`;
20
23
  `"prompt"` → feed `content` to the agent, then `gtd step <actor>` yourself
21
24
  once it's done acting. The optional `model` is an opaque string the workflow
22
25
  author chose, which you map onto your own harness's model selection if you
@@ -43,13 +46,15 @@ A minimal bash implementation of the pinned protocol, driving an agent CLI (e.g.
43
46
  reference for what a loop driver must do; keep any other implementation
44
47
  (including `skills/loop/SKILL.md`) consistent with it rather than editing both
45
48
  independently. `bin/gtd-loop` is this exact script, packaged as the `gtd-loop`
46
- binary, with three additions: it stops with a diagnostic if the same `"prompt"`
49
+ binary, with four additions: it stops with a diagnostic if the same `"prompt"`
47
50
  state/content repeat with no progress (see `skills/loop/SKILL.md`'s "Stall
48
51
  detection"); it lets `GTD_LOOP_AGENT_CMD` swap in any coding agent CLI in place
49
- of the default `claude -p`, receiving the prompt via `$GTD_LOOP_PROMPT`; and it
52
+ of the default `claude -p`, receiving the prompt via `$GTD_LOOP_PROMPT`; it
50
53
  exports the resolved state's optional `model` hint as `$GTD_LOOP_MODEL`,
51
54
  appending `--model "$GTD_LOOP_MODEL"` to the default `claude -p` invocation
52
- whenever it's non-empty.
55
+ whenever it's non-empty; and it acts on the optional `memory` scope hint (see
56
+ "Agent memory scope" below) — continuing one agent session across consecutive
57
+ same-scope turns and starting fresh when the scope changes.
53
58
 
54
59
  ```bash
55
60
  #!/usr/bin/env bash
@@ -89,15 +94,39 @@ The driver — not the prompt text — owns ending the agent's turn
89
94
  (`gtd step "$actor"` right after the agent acts): every default-workflow agent
90
95
  prompt says explicitly not to run `gtd step agent` itself.
91
96
 
97
+ ## Agent memory scope
98
+
99
+ A `"prompt"` beat may carry an optional `memory` key — an opaque scope label the
100
+ workflow author chose (e.g. `plan`, `build`, `fix`). gtd never interprets it; it
101
+ signals to the driver whether this agent turn should continue from the previous
102
+ turn's memory or start fresh. The rule is a comparison, not a command: track the
103
+ `memory` of the last `"prompt"` beat (script/message beats have none and never
104
+ change it), and when a new `"prompt"` beat's `memory` **equals** the tracked
105
+ value, continue the same agent session; when it **differs** — or it is the first
106
+ turn, or either side has no `memory` — start fresh.
107
+
108
+ This is what makes a loop retain memory while a phase boundary clears it: a loop
109
+ that keeps re-entering one state emits the same label every lap, so the agent
110
+ accumulates context across the loop; the move to the next phase's
111
+ differently-labelled state resets it. `bin/gtd-loop` implements this by mapping
112
+ the signal onto claude's session flags — `--session-id` to pin a fresh session
113
+ when the scope changes, `--resume` to continue it while the scope holds — and
114
+ persists the current scope + session id in the git dir (never the working tree,
115
+ so `gtd status` and the pending diff never see them) so retention survives even
116
+ across the restarts a human gate forces mid-loop. See `skills/loop/SKILL.md`'s
117
+ "Agent memory scope" for the same contract stated for a hand-driven loop.
118
+
92
119
  ## Using a different agent
93
120
 
94
121
  `gtd-loop` defaults to
95
122
  `claude -p "$GTD_LOOP_PROMPT" --dangerously-skip-permissions`, but the agent
96
123
  invocation is swappable: set `GTD_LOOP_AGENT_CMD` to any shell command, and it
97
- runs with the prompt available as `$GTD_LOOP_PROMPT` (and the resolved state's
98
- opaque `model` hint, if any, as `$GTD_LOOP_MODEL`) in its environment — an
99
- adapter that ignores `$GTD_LOOP_MODEL` keeps working unchanged. For example, to
100
- drive a different agent CLI:
124
+ runs with the prompt available as `$GTD_LOOP_PROMPT` in its environment, along
125
+ with the resolved state's opaque `model` hint (if any) as `$GTD_LOOP_MODEL` and
126
+ its `memory` scope as `$GTD_LOOP_MEMORY` / `$GTD_LOOP_SESSION_ID` /
127
+ `$GTD_LOOP_MEMORY_RESUME` (`1` when this turn should continue the prior
128
+ session). An adapter that ignores any of these keeps working unchanged. For
129
+ example, to drive a different agent CLI:
101
130
 
102
131
  ```bash
103
132
  GTD_LOOP_AGENT_CMD='my-agent-cli --prompt "$GTD_LOOP_PROMPT"' gtd-loop
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
package/schema.json CHANGED
@@ -97,6 +97,10 @@
97
97
  "type": "string",
98
98
  "description": "Opaque harness hint passed through `gtd next --json`/`gtd status --json` (e.g. \"smart\"). Never interpreted by gtd. Forbidden on a commit state."
99
99
  },
100
+ "memory": {
101
+ "type": "string",
102
+ "description": "Opaque memory-scope label passed through `gtd next --json`/`gtd status --json` (e.g. \"plan\"). A memory-aware driver retains an agent's memory across consecutive agent turns sharing this label and starts fresh when it changes. Never interpreted by gtd. Forbidden on a commit state."
103
+ },
100
104
  "file": {
101
105
  "type": "string",
102
106
  "description": "The state's steering file: an Eta template naming the file a human/editor should look at while the machine rests here. Forbidden on a commit state."