@pmelab/gtd 3.2.0 → 3.4.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
@@ -189536,12 +189546,14 @@ const validateDefinition = (def) => {
189536
189546
  * actor: <string> # forbidden on a commit state, required otherwise
189537
189547
  * script: <string> # exactly one of script/prompt/message/commit
189538
189548
  * on: # a mapping, DECLARATION ORDER PRESERVED
189539
- * "<pattern>": <targetState>
189549
+ * "<pattern>": <targetState> # short form
189550
+ * "<pattern>": { to: <targetState>, describe: <sentence> } # with a human-readable route description
189540
189551
  * initial: true # exactly one state across the whole workflow
189541
189552
  * retry:
189542
189553
  * max: <number>
189543
189554
  * otherwise: <targetState>
189544
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
189545
189557
  * file: <string> # optional, an Eta template naming the state's steering file — never on a commit state
189546
189558
  * mode: qa | review # optional, requires "file" — never on a commit state
189547
189559
  * ```
@@ -189635,6 +189647,7 @@ const KNOWN_STATE_KEYS = /* @__PURE__ */ new Set([
189635
189647
  "initial",
189636
189648
  "retry",
189637
189649
  "model",
189650
+ "memory",
189638
189651
  "file",
189639
189652
  "mode",
189640
189653
  "reviewWindow",
@@ -189663,7 +189676,42 @@ const resolveContent = (value, configDir, where, errors) => {
189663
189676
  return;
189664
189677
  }
189665
189678
  };
189666
- /** The `on` mapping: pattern -> target, preserving declaration order as `OnEdge` tuples. */
189679
+ const KNOWN_EDGE_KEYS = /* @__PURE__ */ new Set(["to", "describe"]);
189680
+ /**
189681
+ * Compile one `on` row's value into an `OnEdge` (or `undefined`, pushing a
189682
+ * finding, when the value is malformed). The value is EITHER a target-state
189683
+ * name (a string) OR a `{ to: <target>, describe: <sentence> }` object — the
189684
+ * object form attaches an optional human-readable `describe` a `message:`
189685
+ * template can surface at a rest (see `PatternMachine.OnEdge`).
189686
+ */
189687
+ const compileOnEdge = (pattern, value, name, errors) => {
189688
+ if (typeof value === "string") return [pattern, value];
189689
+ if (!isPlainObject$1(value)) {
189690
+ errors.push(`state "${name}": "on" entry for pattern "${pattern}" must be a target state name (string) or a { to, describe } object`);
189691
+ return;
189692
+ }
189693
+ const unknownKeys = Object.keys(value).filter((k) => !KNOWN_EDGE_KEYS.has(k));
189694
+ if (unknownKeys.length > 0) errors.push(`state "${name}": "on" entry for pattern "${pattern}" has unknown key(s) ${unknownKeys.join(", ")}`);
189695
+ const { to, describe } = value;
189696
+ if (typeof to !== "string") {
189697
+ errors.push(`state "${name}": "on.${pattern}.to" must be a target state name (string)`);
189698
+ return;
189699
+ }
189700
+ if (describe !== void 0 && typeof describe !== "string") {
189701
+ errors.push(`state "${name}": "on.${pattern}.describe" must be a string`);
189702
+ return;
189703
+ }
189704
+ return describe !== void 0 ? [
189705
+ pattern,
189706
+ to,
189707
+ describe
189708
+ ] : [pattern, to];
189709
+ };
189710
+ /**
189711
+ * The `on` mapping: pattern -> edge, preserving declaration order as `OnEdge`
189712
+ * tuples. Each row's value is compiled by `compileOnEdge` (a target string or
189713
+ * a `{ to, describe }` object).
189714
+ */
189667
189715
  const compileOn = (raw, name, errors) => {
189668
189716
  if (raw === void 0) return void 0;
189669
189717
  if (!isPlainObject$1(raw)) {
@@ -189671,12 +189719,9 @@ const compileOn = (raw, name, errors) => {
189671
189719
  return;
189672
189720
  }
189673
189721
  const edges = [];
189674
- for (const [pattern, target] of Object.entries(raw)) {
189675
- if (typeof target !== "string") {
189676
- errors.push(`state "${name}": "on" target for pattern "${pattern}" must be a string`);
189677
- continue;
189678
- }
189679
- edges.push([pattern, target]);
189722
+ for (const [pattern, value] of Object.entries(raw)) {
189723
+ const edge = compileOnEdge(pattern, value, name, errors);
189724
+ if (edge !== void 0) edges.push(edge);
189680
189725
  }
189681
189726
  return edges;
189682
189727
  };
@@ -189732,6 +189777,15 @@ const compileModel = (raw, name, errors) => {
189732
189777
  }
189733
189778
  return raw.model;
189734
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
+ };
189735
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`. */
189736
189790
  const compileFile = (raw, name, errors) => {
189737
189791
  if (raw.file === void 0) return void 0;
@@ -189788,6 +189842,7 @@ const assembleStateDef = (parts) => ({
189788
189842
  initial: parts.initial,
189789
189843
  retry: parts.retry,
189790
189844
  model: parts.model,
189845
+ memory: parts.memory,
189791
189846
  file: parts.file,
189792
189847
  mode: parts.mode,
189793
189848
  reviewWindow: parts.reviewWindow,
@@ -189810,6 +189865,7 @@ const compileState = (name, raw, configDir, errors) => {
189810
189865
  initial: compileInitial(raw, name, errors),
189811
189866
  retry: compileRetry(raw.retry, name, errors),
189812
189867
  model: compileModel(raw, name, errors),
189868
+ memory: compileMemory(raw, name, errors),
189813
189869
  file: compileFile(raw, name, errors),
189814
189870
  mode: compileMode(raw, name, errors),
189815
189871
  reviewWindow: compileBooleanFlag(raw, "reviewWindow", name, errors),
@@ -189863,7 +189919,7 @@ const compileWorkflowConfig = (raw, configDir) => {
189863
189919
  * default.yaml's content values start with `./`/`../`, so
189864
189920
  * `compileWorkflowConfig` never resolves a file reference against it.
189865
189921
  */
189866
- const compiled = compileWorkflowConfig((0, import_dist$1.parse)("# The bundled default gtd workflow (v3 \"pattern machine\" — see\n# docs/design/pattern-machine-plan.md, and\n# docs/design/steering-file-loops.md for the two validation loops below).\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/default.ts does the compiling).\n#\n# Every content string below is INLINE (no `./`-relative file references):\n# the bundled default ships inside a single-file build\n# (`dist/gtd.bundle.mjs`), so its content must already be resolved at BUILD\n# time rather than read from disk at RUNTIME — the same reason\n# `src/prompts/*.md` are imported through tsdown's `.md`-as-text loader\n# instead of read via `fs` at runtime. `default.yaml` itself is imported the\n# same way (tsdown/vitest both treat `.yaml` as text — see\n# tsdown.config.ts / tests/vitest.rawMd.ts), so this file never needs a\n# sibling prompts/ directory to exist on disk once installed.\n#\n# States, in cycle order — 12 states, no squash:\n# idle (initial, human, message)\n# -> grilling (agent, prompt): reads .gtd/TODO.md's sketch (or a prior\n# lap's feedback), explores the codebase, and develops it into a concrete\n# implementation plan, stating any open question it can't settle itself\n# under a `## Open Questions` heading (see \"Loop 1\" below) rather than\n# guessing silently.\n# grilling -> todo-validating (check, script): mechanics-only parser port\n# of the open-questions format (see src/OpenQuestions.ts's module doc for\n# the executable spec this is a bash rendition of). Malformed ->\n# .gtd/FORMAT.md -> back to grilling; valid -> grilling-answer.\n# todo-validating -> grilling-answer (human, message): tick off suggested\n# defaults by replacing them with `Answer:` lines; a clean step accepts\n# every remaining default -> building; any edit loops back to grilling.\n# grilling-answer -> building (agent, prompt): implements the plan in\n# .gtd/TODO.md directly (no task decomposition, no per-task queue) and\n# deletes .gtd/TODO.md once the work is complete and verified.\n# building -> checking (check, script): runs the test command inline;\n# A/M .gtd/FEEDBACK.md -> fixing (capped, redirects to escalate); D\n# .gtd/FEEDBACK.md (a green run that just cleaned up prior feedback) or C\n# (green, nothing to clean) -> reviewing.\n# fixing (agent, prompt) <-> checking\n# escalate (human, message): manual intervention, then back to checking\n# checking (green) -> reviewing (agent, prompt): writes .gtd/REVIEW.md in\n# the checkbox review format (see \"Loop 2\" below), grouping the cycle's\n# full diff into reviewable chunks.\n# reviewing -> review-validating (check, script): mechanics-only parser\n# port of the review format (see src/ReviewDoc.ts's module doc for the\n# executable spec). Malformed -> .gtd/FORMAT.md -> back to reviewing;\n# valid -> await-review.\n# review-validating -> await-review (human, message): tick a pointer's `- [\n# ]` to `- [x]` to approve that item; deleting .gtd/REVIEW.md outright is\n# the power-user approve-everything shortcut; anything else pending\n# (code edits with REVIEW.md untouched) is feedback straight to grilling.\n# await-review -> review-deciding (check, script), entered by any `M\n# .gtd/REVIEW.md` step: if no unticked `- [ ]` pointer remains, the cycle\n# is approved (rm .gtd/REVIEW.md -> idle); otherwise the still-unticked\n# pointers (with their notes) are extracted into a fresh .gtd/TODO.md and\n# .gtd/REVIEW.md is removed (-> grilling, feedback lap).\n#\n# The cycle ends at human approval, resting back at idle — there is no\n# squash state. The cycle's commits stay in history; whether/how to squash\n# them (amend, interactive rebase, a PR's squash-merge) is entirely the\n# human's business once they're done reviewing — gtd makes no assumption.\n# That squash-flavored finale (a `squashing` prompt state + a `done` commit\n# state) still exists as an ENGINE capability (see STATES.md §8) and is kept\n# as part of the fuller machine at docs/examples/advanced-workflow.md.\n#\n# A `gtd(<actor>): idle` commit — entering the workflow's own initial state —\n# is therefore a PROCESS BOUNDARY (see `computeProcessRun` in src/Edge.ts):\n# the next cycle's `retry` counts, `startCommit`, and diffs never reach back\n# across an approved cycle's own idle-entering commit.\n#\n# HYGIENE INVARIANT (asserted in e2e — see default-workflow.feature): an\n# approved cycle leaves `.gtd/` empty. FEEDBACK.md is cleaned up by a green\n# `checking` run; FORMAT.md by a valid validator run (either loop); REVIEW.md\n# by the review-deciding approval branch; TODO.md by `building`.\n#\n# Loop 1 — TODO.md open questions (src/OpenQuestions.ts is this format's\n# EXECUTABLE SPEC — its unit tests are the format's spec tests; the\n# `todo-validating`/`review-validating` bash scripts below are a pragmatic\n# grep/awk PORT of the exact same rules, not a full markdown parser. Keep\n# both in sync by hand — there is no shared code path between the two\n# implementations, by design: the engine stays git/filesystem/Effect-free,\n# and the LSP's parsers stay dependency-free).\n#\n# Loop 2 — REVIEW.md checkboxes: same dual-implementation contract, this time\n# against src/ReviewDoc.ts.\n#\n# `grilling` and `reviewing` both declare `model: smart` — an opaque harness\n# hint (see the comment at `grilling` below) for their heavier one-shot\n# planning/reviewing turns. Every other state is unset (harness default).\n#\n# `vars.testCommand` is this workflow's own declared default for `checking`'s\n# script (see it.vars in PatternTemplates.ts) — override it per-repo with a\n# top-level `.gtdrc` `vars: { testCommand: ... }` key, or per-invocation with\n# a `GTD_VAR_testCommand` environment variable (highest precedence).\n#\n# `vars.todoFile`/`reviewFile`/`feedbackFile` are this workflow's ONE source\n# of truth for the three steering-file paths every prompt/script below reads\n# or writes — each appears in templates as `<%~ it.vars.todoFile %>` etc\n# (see docs/design/state-file-association.md §2). Every state that rests on\n# one of these files also declares a `file:`/`mode:` pair (see STATES.md's\n# property table) so `gtd status`/`gtd next --json` and the LSP (`gtd lsp`)\n# know which file a human/editor should look at, and in what format\n# (`qa` for TODO.md's open-questions format, `review` for REVIEW.md's\n# checkbox format) — `fixing`/`escalate` declare `file:` alone (FEEDBACK.md\n# is plain text, no LSP format). KNOWN LIMITATION: `on` pattern keys below are\n# NOT Eta templates — they keep the LITERAL `.gtd/...` paths matching these\n# vars' DEFAULT values. Repointing `todoFile`/`reviewFile`/`feedbackFile` via\n# `.gtdrc`'s top-level `vars:` or a `GTD_VAR_` override changes what `file:`\n# renders to (and what a template reads/writes) WITHOUT changing what the\n# `on` patterns match against — desyncing the machine. See\n# docs/configuration.md's `file:`/`mode:` section for the full caveat.\n#\n# A fuller machine — two-phase Q&A planning, an architecture phase, task\n# decomposition, a deterministic per-task builder loop, agent-prepared\n# review, and the squash finale (`squashing` + `done`) — is preserved as a\n# copy-paste `.gtdrc` example at docs/examples/advanced-workflow.md.\n\nvars:\n testCommand: npm test\n todoFile: .gtd/TODO.md\n reviewFile: .gtd/REVIEW.md\n feedbackFile: .gtd/FEEDBACK.md\n\nstates:\n idle:\n actor: human\n initial: true\n message: |\n No active gtd cycle.\n\n To start one: write `.gtd/TODO.md` with a short sketch of what you want\n built — a few sentences is enough — then run `gtd step human`.\n on:\n \"* **\": grilling\n\n grilling:\n actor: agent\n file: <%= it.vars.todoFile %>\n mode: qa\n # `model` is an opaque harness hint — gtd never interprets this string, it\n # only passes it through to `gtd next --json`/`gtd status --json` so the\n # driving loop can map it onto whatever models its agent harness\n # provides. \"smart\" here just names the harness-chosen tier for the\n # heavier planning turn; states without a `model` use the harness's\n # default.\n model: smart\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.todoFile %>`, which this prompt tells you to develop. Never\n touch `.gtd/FORMAT.md` yourself if it exists — a validator writes it,\n and only the validator removes it once you've fixed the underlying\n problem.\n\n If `.gtd/FORMAT.md` exists, it holds format findings from your last\n draft of `<%= it.vars.todoFile %>` — fix those findings first, then\n continue below.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build (or\n feedback from a prior review lap). Read it, explore the codebase, and\n develop it into a concrete implementation plan.\n\n For every question you cannot settle yourself, add it under a\n `## Open Questions` heading, one `### <question>` sub-heading each,\n whose body's FIRST non-blank line is exactly\n `Suggested default: <your answer>` — the human can accept it as-is or\n override it. Omit the section entirely once there are no open\n questions. If `<%= it.vars.todoFile %>` already carries\n `## Open Questions` entries from a previous lap, fold any\n `Answer: <text>` line into the plan; every remaining entry's body must\n still start with either `Suggested default: ...` or `Answer: ...` —\n never anything else.\n\n Leave `<%= it.vars.todoFile %>` uncommitted and finish your turn — do\n not commit, and do not run `gtd step agent` yourself; the harness does\n that.\n on:\n \"* **\": todo-validating\n\n todo-validating:\n actor: check\n file: <%= it.vars.todoFile %>\n mode: qa\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: a pragmatic grep/awk port of\n # src/OpenQuestions.ts's parser rules (the format's executable spec) —\n # free-form prose, plus an OPTIONAL \"## Open Questions\" section; every\n # \"### \" sub-heading directly under it is one question whose body's\n # first non-blank line must start with \"Suggested default: \" or\n # \"Answer: \". A violation is one finding line (file + line + rule) in\n # .gtd/FORMAT.md; a clean parse removes any stale .gtd/FORMAT.md\n # instead. What FORMAT.md's presence/absence MEANS (another drafting\n # round vs. proceeding to the human) is decided by this state's own\n # `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n awk '\n BEGIN { in_section = 0; have_question = 0; question = \"\"; qline = 0; body_seen = 0 }\n {\n trimmed = $0\n gsub(/^[ \\t]+|[ \\t]+$/, \"\", trimmed)\n\n level = 0\n if (match(trimmed, /^#+/)) level = RLENGTH\n\n if (in_section && level >= 1 && level <= 2) {\n if (have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n in_section = 0\n have_question = 0\n }\n\n if (trimmed == \"## Open Questions\") { in_section = 1; next }\n\n if (in_section && level == 3) {\n if (have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n question = trimmed\n sub(/^### */, \"\", question)\n qline = NR\n have_question = 1\n body_seen = 0\n next\n }\n\n if (in_section && have_question && !body_seen && trimmed != \"\") {\n if (trimmed ~ /^Suggested default: *.+/ || trimmed ~ /^Answer: *.+/) {\n body_seen = 1\n } else {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n body_seen = 1\n }\n }\n }\n END {\n if (in_section && have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n }\n ' <%~ it.vars.todoFile %> > .gtd/.format-findings\n\n if [ -s .gtd/.format-findings ]; then\n mv .gtd/.format-findings .gtd/FORMAT.md\n else\n rm -f .gtd/.format-findings\n rm -f .gtd/FORMAT.md\n fi\n on:\n \"A .gtd/FORMAT.md\": grilling\n \"M .gtd/FORMAT.md\": grilling\n \"D .gtd/FORMAT.md\": grilling-answer\n \"C\": grilling-answer\n\n grilling-answer:\n actor: human\n file: <%= it.vars.todoFile %>\n mode: qa\n message: |\n `<%= it.vars.todoFile %>` holds the plan under development, with any\n open questions under `## Open Questions` — each carrying a suggested\n default.\n\n To answer a question, edit its entry in place (replace\n `Suggested default: ...` with `Answer: ...`, or add further detail),\n then run `gtd step human`.\n\n To accept all suggested defaults as-is and move on to implementation,\n run `gtd step human` with no edits.\n on:\n \"C\": building\n \"* **\": grilling\n\n building:\n actor: agent\n file: <%= it.vars.todoFile %>\n mode: qa\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create or edit anything under `.gtd/` except deleting\n `<%= it.vars.todoFile %>`, which this prompt tells you to delete once\n its work is complete.\n\n The plan to implement is: <%~ it.read(it.vars.todoFile) %>\n\n Read that plan and implement it in full. Use TDD discipline: one test,\n then the implementation that passes it, then the next — never write\n all the tests first.\n\n Delete `<%= it.vars.todoFile %>` once the work it describes is complete\n and verified. Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": checking\n\n checking:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: run the test suite and record a\n # red run's output as .gtd/FEEDBACK.md. What that output MEANS (another\n # fix round vs escalation) is decided by the workflow's own `on`/`retry`\n # rules at capture time — never here.\n #\n # `it.vars.testCommand` defaults to \"npm test\" (this workflow's own\n # `vars:` above) — override it with a top-level `.gtdrc`\n # `vars: { testCommand: ... }` key, or a `GTD_VAR_testCommand`\n # environment variable (highest precedence).\n #\n # A green run also removes any `<%= it.vars.feedbackFile %>` left over\n # from an earlier red run — there is no squash to discard it for us\n # anymore, so the script cleans it up itself before moving on to\n # review.\n set +e\n mkdir -p .gtd\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output <%~ it.vars.feedbackFile %>\n else\n rm -f .gtd/.check-output\n printf 'npm test failed with exit code %s and produced no output.' \"$code\" > <%~ it.vars.feedbackFile %>\n fi\n else\n rm -f .gtd/.check-output\n rm -f <%~ it.vars.feedbackFile %>\n fi\n on:\n \"A .gtd/FEEDBACK.md\": fixing\n \"M .gtd/FEEDBACK.md\": fixing\n \"D .gtd/FEEDBACK.md\": reviewing\n \"C\": reviewing\n\n fixing:\n actor: agent\n file: <%= it.vars.feedbackFile %>\n retry:\n max: 3\n otherwise: escalate\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.feedbackFile %>`, which this prompt tells you to address.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix\n the code so the suite passes. Keep the change focused — do not\n refactor unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue; the\n machine picks the dispute up either way.\n\n Leave everything uncommitted and finish your turn — do not commit.\n on:\n \"* **\": checking\n\n escalate:\n actor: human\n file: <%= it.vars.feedbackFile %>\n message: |\n The agent could not get the check to pass after repeated attempts.\n `<%= it.vars.feedbackFile %>` holds the last failing output.\n\n Investigate and fix it yourself (editing code and/or\n `<%= it.vars.feedbackFile %>`), then run `gtd step human` to try the\n check again.\n on:\n \"* **\": checking\n\n reviewing:\n actor: agent\n file: <%= it.vars.reviewFile %>\n mode: review\n model: smart\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewFile %>`, which this prompt tells you to write.\n Never touch `.gtd/FORMAT.md` yourself if it exists — a validator writes\n it, and only the validator removes it once you've fixed the underlying\n problem.\n\n If `.gtd/FORMAT.md` exists, it holds format findings from your last\n draft of `<%= it.vars.reviewFile %>` — fix those findings first, then\n continue below.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the cycle's\n full diff (inlined below) in this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.startCommit %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation\n of what changed and why, then one\n `- [ ] ./path/to/file.ts#42 — note` file pointer per hunk (checkboxes\n are for the human to tick, not you; every path is `./`-relative, with\n an optional `#line` and an optional ` — note`).\n\n Leave `<%= it.vars.reviewFile %>` uncommitted and finish your turn.\n <% if (it.processDiff.trim()) { %>\n\n ## Full cycle diff\n\n ```diff\n <%~ it.processDiff %>\n ```\n <% } %>\n on:\n \"* **\": review-validating\n\n review-validating:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: a pragmatic grep/awk port of\n # src/ReviewDoc.ts's parser rules (the format's executable spec) — a\n # `# Review: <hash>` header as the first non-blank line, a\n # `<!-- base: <hash> -->` comment, and at least one `##` chunk with at\n # least one `- [ ]`/`- [x]` file pointer. A violation is one finding\n # line (file + line + rule) in .gtd/FORMAT.md; a clean parse removes any\n # stale .gtd/FORMAT.md instead. What FORMAT.md's presence/absence MEANS\n # (another drafting round vs. handing off to the human) is decided by\n # this state's own `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n awk '\n BEGIN {\n first_nonblank_seen = 0\n header_ok = 0\n base_ok = 0\n chunk_open = 0\n chunk_title = \"\"\n chunk_line = 0\n chunk_files = 0\n chunk_count = 0\n }\n {\n trimmed = $0\n gsub(/^[ \\t]+|[ \\t]+$/, \"\", trimmed)\n\n if (!first_nonblank_seen && trimmed != \"\") {\n first_nonblank_seen = 1\n if (trimmed ~ /^# Review: [^ ]+$/) {\n header_ok = 1\n } else {\n printf \"<%~ it.vars.reviewFile %>:%d: first non-blank line must be \\\"# Review: <hash>\\\"\\n\", NR\n }\n }\n\n if (trimmed ~ /^<!-- *base: *[^ ]+ *-->$/) base_ok = 1\n\n if (trimmed ~ /^## [^ ]/) {\n if (chunk_open && chunk_files == 0) {\n printf \"<%~ it.vars.reviewFile %>:%d: chunk \\\"%s\\\" has no file pointers\\n\", chunk_line, chunk_title\n }\n chunk_open = 1\n chunk_title = trimmed\n sub(/^## /, \"\", chunk_title)\n chunk_line = NR\n chunk_files = 0\n chunk_count++\n next\n }\n\n if (chunk_open) {\n if (trimmed ~ /^-[ \\t]*\\[[ xX]\\][ \\t]*\\.\\/[^ \\t]+/) {\n chunk_files++\n } else if (trimmed ~ /^-[ \\t]*\\[/) {\n printf \"<%~ it.vars.reviewFile %>:%d: file pointer does not match \\\"- [ ] ./path#line — note\\\"\\n\", NR\n }\n }\n }\n END {\n if (chunk_open && chunk_files == 0) {\n printf \"<%~ it.vars.reviewFile %>:%d: chunk \\\"%s\\\" has no file pointers\\n\", chunk_line, chunk_title\n }\n if (!header_ok) {\n print \"<%~ it.vars.reviewFile %>:1: missing or malformed \\\"# Review: <hash>\\\" header as first line\"\n }\n if (!base_ok) {\n print \"<%~ it.vars.reviewFile %>:1: missing \\\"<!-- base: <hash> -->\\\" comment\"\n }\n if (chunk_count == 0) {\n print \"<%~ it.vars.reviewFile %>:1: no \\\"##\\\" chunks found\"\n }\n }\n ' <%~ it.vars.reviewFile %> > .gtd/.format-findings\n\n if [ -s .gtd/.format-findings ]; then\n mv .gtd/.format-findings .gtd/FORMAT.md\n else\n rm -f .gtd/.format-findings\n rm -f .gtd/FORMAT.md\n fi\n on:\n \"A .gtd/FORMAT.md\": reviewing\n \"M .gtd/FORMAT.md\": reviewing\n \"D .gtd/FORMAT.md\": await-review\n \"C\": await-review\n\n await-review:\n actor: human\n file: <%= it.vars.reviewFile %>\n mode: review\n # While the cycle rests here for human review, gtd opens a \"review checkout\n # window\": HEAD and the index are temporarily rewound to this cycle's start\n # (the process boundary — no `reviewBase` state is declared, so the base\n # defaults to it) with the working tree untouched, so the whole cycle diff\n # surfaces as ordinary uncommitted changes in the editor's git integration.\n # The window closes automatically on the next invocation, once the cycle\n # rests anywhere else (see src/ReviewWindow.ts, STATES.md §11).\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the completed\n cycle — tick a pointer's `- [ ]` box to `- [x]` to approve that item;\n add notes, untick, or edit code for feedback.\n\n To approve: tick every box, then run `gtd step human`.\n\n To approve the whole cycle without ticking every box individually:\n delete `<%= it.vars.reviewFile %>` and run `gtd step human`.\n\n To request changes: edit `<%= it.vars.reviewFile %>`\n (ticking/unticking boxes, adding notes) and/or the code, then run\n `gtd step human` — any edit to `<%= it.vars.reviewFile %>` sends the\n cycle to a deterministic check that either closes the cycle out (every\n box ticked) or extracts the still-unticked items into a fresh\n `<%= it.vars.todoFile %>` and routes back through grilling with that\n feedback. Code-only edits (leaving `<%= it.vars.reviewFile %>`\n untouched) go straight back to grilling.\n on:\n \"D .gtd/REVIEW.md\": idle\n \"M .gtd/REVIEW.md\": review-deciding\n \"* **\": grilling\n\n review-deciding:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: if .gtd/REVIEW.md still has any\n # unticked \"- [ ]\" pointer, extract those pointers (with their notes)\n # into a fresh .gtd/TODO.md; either way, remove .gtd/REVIEW.md. What\n # the resulting diff MEANS (feedback lap vs. full approval) is decided\n # by this state's own `on` rules at capture time — never here. The\n # A/M .gtd/TODO.md row is declared BEFORE the D .gtd/REVIEW.md row (see\n # this workflow's `on` map below) so a feedback lap — which always also\n # deletes .gtd/REVIEW.md — is captured as feedback, not mistaken for an\n # approval.\n set +e\n mkdir -p .gtd\n if grep -qE '^[[:space:]]*-[[:space:]]*\\[[[:space:]]*\\]' <%~ it.vars.reviewFile %> 2>/dev/null; then\n {\n echo \"Feedback from review — address these before continuing:\"\n echo\n grep -E '^[[:space:]]*-[[:space:]]*\\[[[:space:]]*\\]' <%~ it.vars.reviewFile %>\n } > <%~ it.vars.todoFile %>\n fi\n rm -f <%~ it.vars.reviewFile %>\n on:\n \"A .gtd/TODO.md\": grilling\n \"M .gtd/TODO.md\": grilling\n \"D .gtd/REVIEW.md\": idle\n \"C\": await-review\n"), ".");
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"), ".");
189867
189923
  const defaultWorkflowDefinition = compiled.definition;
189868
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`). */
189869
189925
  const defaultWorkflowVars = compiled.vars;
@@ -189956,8 +190012,25 @@ const workflowJsonSchema = {
189956
190012
  },
189957
190013
  on: {
189958
190014
  type: "object",
189959
- description: "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise).",
189960
- additionalProperties: { type: "string" }
190015
+ description: "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise). A value is either the target state name (string) or a { to, describe } object whose describe is a human-readable sentence templates can surface as it.edges (e.g. in a human gate's message).",
190016
+ additionalProperties: { oneOf: [{
190017
+ type: "string",
190018
+ description: "The target state name."
190019
+ }, {
190020
+ type: "object",
190021
+ additionalProperties: false,
190022
+ required: ["to"],
190023
+ properties: {
190024
+ to: {
190025
+ type: "string",
190026
+ description: "The target state name."
190027
+ },
190028
+ describe: {
190029
+ type: "string",
190030
+ description: "Human-readable sentence describing where this change routes; surfaced verbatim (never Eta-rendered) to templates as it.edges[].describe."
190031
+ }
190032
+ }
190033
+ }] }
189961
190034
  },
189962
190035
  initial: {
189963
190036
  type: "boolean",
@@ -189984,6 +190057,10 @@ const workflowJsonSchema = {
189984
190057
  type: "string",
189985
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."
189986
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
+ },
189987
190064
  file: {
189988
190065
  type: "string",
189989
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."
@@ -191777,8 +191854,17 @@ const resolveVars = (workflowVars, rcVars, env) => ({
191777
191854
  ...rcVars,
191778
191855
  ...envVarsFrom(env)
191779
191856
  });
191780
- /** Build the `PatternTemplates.TemplateContext` for rendering `state`'s content at the resolved rest. `vars` is the already-merged three-layer map (see `resolveVars`). */
191781
- const buildTemplateContext = (git, read, state, actor, run, vars) => gen(function* () {
191857
+ /** Map a state's raw `on` edges to the `{ pattern, target, describe? }` shape templates see as `it.edges`. `undefined` (a commit state, no `on`) yields an empty list. */
191858
+ const toTemplateEdges = (edges) => (edges ?? []).map(([pattern, target, describe]) => describe !== void 0 ? {
191859
+ pattern,
191860
+ target,
191861
+ describe
191862
+ } : {
191863
+ pattern,
191864
+ target
191865
+ });
191866
+ /** Build the `PatternTemplates.TemplateContext` for rendering `state`'s content at the resolved rest. `vars` is the already-merged three-layer map (see `resolveVars`); `edges` is the resting state's own `on` edges (see `toTemplateEdges`). */
191867
+ const buildTemplateContext = (git, read, state, actor, run, vars, edges) => gen(function* () {
191782
191868
  const hasCommits = yield* git.hasCommits();
191783
191869
  const currentCommit = hasCommits ? yield* git.resolveRef("HEAD") : "";
191784
191870
  const previousCommit = hasCommits ? yield* git.resolveRef("HEAD~1").pipe(catchAll(() => succeed$3(run.startParentHash))) : "";
@@ -191793,7 +191879,8 @@ const buildTemplateContext = (git, read, state, actor, run, vars) => gen(functio
191793
191879
  processDiff,
191794
191880
  lastDiff,
191795
191881
  read,
191796
- vars
191882
+ vars,
191883
+ edges: toTemplateEdges(edges)
191797
191884
  };
191798
191885
  });
191799
191886
  /**
@@ -191811,6 +191898,18 @@ const renderModel = (stateDef, context) => try_({
191811
191898
  catch: (e) => e instanceof Error ? e : new Error(String(e))
191812
191899
  });
191813
191900
  /**
191901
+ * Render a state's declared `memory:` scope label (if any) through the SAME
191902
+ * template context as its content/`model` — see `renderModel`'s doc comment;
191903
+ * a plain label (e.g. `"plan"`) passes through unchanged, while
191904
+ * `memory: "<%= it.vars.planScope %>"` resolves against the merged `it.vars`.
191905
+ * The render-failure semantics are identical (propagates as a thrown/rejected
191906
+ * error, same call site as `gtd next`/`gtd status`).
191907
+ */
191908
+ const renderMemory = (stateDef, context) => try_({
191909
+ try: () => stateDef.memory !== void 0 ? renderStateTemplate(stateDef.memory, context) : void 0,
191910
+ catch: (e) => e instanceof Error ? e : new Error(String(e))
191911
+ });
191912
+ /**
191814
191913
  * Render a state's declared `file:` steering-file template (if any) through
191815
191914
  * the SAME template context as its content/`model` — see `renderModel`'s doc
191816
191915
  * comment; the render-failure semantics are identical (propagates as a
@@ -191820,7 +191919,7 @@ const renderFile = (stateDef, context) => try_({
191820
191919
  try: () => stateDef.file !== void 0 ? renderStateTemplate(stateDef.file, context) : void 0,
191821
191920
  catch: (e) => e instanceof Error ? e : new Error(String(e))
191822
191921
  });
191823
- /** 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. */
191922
+ /** 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. */
191824
191923
  const renderRest = (rest, context) => gen(function* () {
191825
191924
  const kind = contentKindOf(rest.stateDef);
191826
191925
  if (kind === void 0) return yield* fail$2(/* @__PURE__ */ new Error(`state "${rest.state}" declares no content — invalid definition`));
@@ -191830,6 +191929,7 @@ const renderRest = (rest, context) => gen(function* () {
191830
191929
  catch: (e) => e instanceof Error ? e : new Error(String(e))
191831
191930
  });
191832
191931
  const model = yield* renderModel(rest.stateDef, context);
191932
+ const memory = yield* renderMemory(rest.stateDef, context);
191833
191933
  const file = yield* renderFile(rest.stateDef, context);
191834
191934
  return {
191835
191935
  state: rest.state,
@@ -191837,8 +191937,10 @@ const renderRest = (rest, context) => gen(function* () {
191837
191937
  kind,
191838
191938
  content,
191839
191939
  ...model !== void 0 ? { model } : {},
191940
+ ...memory !== void 0 ? { memory } : {},
191840
191941
  ...file !== void 0 ? { file } : {},
191841
- ...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {}
191942
+ ...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {},
191943
+ edges: toTemplateEdges(rest.stateDef.on)
191842
191944
  };
191843
191945
  });
191844
191946
  /**
@@ -364628,7 +364730,8 @@ const buildFileModeMap = (def, vars, root) => {
364628
364730
  read: (path) => {
364629
364731
  throw new Error(`"file:" is not readable while building the path→mode map (path: ${path})`);
364630
364732
  },
364631
- vars
364733
+ vars,
364734
+ edges: []
364632
364735
  });
364633
364736
  } catch (e) {
364634
364737
  warnings.push(`state "${name}": "file:" failed to render, skipped — ${e instanceof Error ? e.message : String(e)}`);
@@ -364717,7 +364820,7 @@ const resolveSteeringFile = (root) => gen(function* () {
364717
364820
  const rest = yield* resolveRest();
364718
364821
  const run = yield* computeProcessRun(git, rest.def);
364719
364822
  const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
364720
- const context = yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars);
364823
+ const context = yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars, rest.stateDef.on);
364721
364824
  const file = yield* renderFile(rest.stateDef, context);
364722
364825
  return {
364723
364826
  state: rest.state,
@@ -364949,7 +365052,7 @@ const resolveRestContext = (git) => gen(function* () {
364949
365052
  return {
364950
365053
  rest,
364951
365054
  run,
364952
- context: yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars)
365055
+ context: yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars, rest.stateDef.on)
364953
365056
  };
364954
365057
  });
364955
365058
  /**
@@ -364982,7 +365085,8 @@ const stepAsActor = (invoker) => gen(function* () {
364982
365085
  };
364983
365086
  const executable = decision;
364984
365087
  const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
364985
- const outcome = yield* executeDecision(git, run, executable, yield* buildTemplateContext(git, worktree.read, decision.kind === "squash" ? decision.state : rest.state, invoker, run, vars));
365088
+ const renderedState = decision.kind === "squash" ? decision.state : rest.state;
365089
+ const outcome = yield* executeDecision(git, run, executable, yield* buildTemplateContext(git, worktree.read, renderedState, invoker, run, vars, rest.def.states[renderedState]?.on));
364986
365090
  return {
364987
365091
  state: rest.state,
364988
365092
  subject: outcome.kind === "noop" ? null : outcome.subject
@@ -365015,8 +365119,10 @@ const runNextCommand = (json, write) => gen(function* () {
365015
365119
  kind: rendered.kind,
365016
365120
  content: rendered.content,
365017
365121
  ...rendered.model !== void 0 ? { model: rendered.model } : {},
365122
+ ...rendered.memory !== void 0 ? { memory: rendered.memory } : {},
365018
365123
  ...rendered.file !== void 0 ? { file: rendered.file } : {},
365019
- ...rendered.mode !== void 0 ? { mode: rendered.mode } : {}
365124
+ ...rendered.mode !== void 0 ? { mode: rendered.mode } : {},
365125
+ ...rendered.edges.length > 0 ? { edges: rendered.edges } : {}
365020
365126
  }) + "\n");
365021
365127
  else write(rendered.content.endsWith("\n") ? rendered.content : rendered.content + "\n");
365022
365128
  });
@@ -365058,21 +365164,25 @@ const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
365058
365164
  pattern: matchedRow?.[0] ?? null
365059
365165
  };
365060
365166
  });
365061
- /** `gtd status --json`'s emission — `{state, actor, changes, model?, file?, mode?}`. */
365062
- const writeStatusJson = (write, rest, statusChanges, model, file) => {
365167
+ /** `gtd status --json`'s emission — `{state, actor, changes, model?, memory?, file?, mode?}`. */
365168
+ const writeStatusJson = (write, rest, statusChanges, model, memory, file) => {
365169
+ const edges = toTemplateEdges(rest.stateDef.on);
365063
365170
  write(JSON.stringify({
365064
365171
  state: rest.state,
365065
365172
  actor: rest.actor,
365066
365173
  changes: statusChanges,
365067
365174
  ...model !== void 0 ? { model } : {},
365175
+ ...memory !== void 0 ? { memory } : {},
365068
365176
  ...file !== void 0 ? { file } : {},
365069
- ...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {}
365177
+ ...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {},
365178
+ ...edges.length > 0 ? { edges } : {}
365070
365179
  }) + "\n");
365071
365180
  };
365072
- /** `gtd status`'s plain-text emission — `State:`/`Awaits:`/`Model:`/`File:`/`Mode:`/`Pending:` lines. */
365073
- const writeStatusPlain = (write, rest, statusChanges, model, file) => {
365181
+ /** `gtd status`'s plain-text emission — `State:`/`Awaits:`/`Model:`/`Memory:`/`File:`/`Mode:`/`Pending:` lines. */
365182
+ const writeStatusPlain = (write, rest, statusChanges, model, memory, file) => {
365074
365183
  const lines = [`State: ${rest.state}`, `Awaits: ${rest.actor}`];
365075
365184
  if (model !== void 0) lines.push(`Model: ${model}`);
365185
+ if (memory !== void 0) lines.push(`Memory: ${memory}`);
365076
365186
  if (file !== void 0) lines.push(`File: ${file}`);
365077
365187
  if (rest.stateDef.mode !== void 0) lines.push(`Mode: ${rest.stateDef.mode}`);
365078
365188
  if (statusChanges.length === 0) lines.push("Pending: (clean)");
@@ -365089,10 +365199,11 @@ const runStatusCommand = (argv, json, write) => gen(function* () {
365089
365199
  const { rest, context } = yield* resolveRestContext(git);
365090
365200
  const changes = yield* pendingChanges(git);
365091
365201
  const model = yield* renderModel(rest.stateDef, context);
365202
+ const memory = yield* renderMemory(rest.stateDef, context);
365092
365203
  const file = yield* renderFile(rest.stateDef, context);
365093
365204
  const statusChanges = computeStatusChanges(rest.stateDef.on ?? [], changes);
365094
- if (json) writeStatusJson(write, rest, statusChanges, model, file);
365095
- else writeStatusPlain(write, rest, statusChanges, model, file);
365205
+ if (json) writeStatusJson(write, rest, statusChanges, model, memory, file);
365206
+ else writeStatusPlain(write, rest, statusChanges, model, memory, file);
365096
365207
  });
365097
365208
  /**
365098
365209
  * `gtd mermaid`: pure emitter of the active workflow's SHAPE (not the
package/docs/cli.md CHANGED
@@ -86,8 +86,9 @@ process in the same step that entered it — see
86
86
  [STATES.md §5](../STATES.md#5-resolution)).
87
87
 
88
88
  Plain mode prints the rendered content verbatim (with exactly one trailing
89
- newline) — never the `model`/`file`/`mode` hints, which are JSON-only. `--json`
90
- emits `{state, actor, kind, content, model?, file?, mode?}`:
89
+ newline) — never the `model`/`memory`/`file`/`mode`/`edges` structured keys,
90
+ which are JSON-only. `--json` emits
91
+ `{state, actor, kind, content, model?, memory?, file?, mode?, edges?}`:
91
92
 
92
93
  ```json
93
94
  {
@@ -108,11 +109,25 @@ emits `{state, actor, kind, content, model?, file?, mode?}`:
108
109
  [Configuration](configuration.md#model--the-opaque-harness-hint-template-rendered)),
109
110
  present only when the state declares one; **omitted entirely** (never `null`)
110
111
  when unset.
112
+ - `memory` — the state's opaque `memory:` scope label, RENDERED the same way
113
+ (see
114
+ [Configuration](configuration.md#memory--the-memory-scope-label-template-rendered)).
115
+ A memory-aware driver retains an agent's memory across consecutive agent turns
116
+ sharing this label and starts fresh when it changes. Present only when the
117
+ state declares one; **omitted entirely** (never `null`) when unset.
111
118
  - `file` — the state's declared steering file, RENDERED the same way; `mode` —
112
119
  its format, verbatim (`"qa"` | `"review"`) (see
113
120
  [Configuration](configuration.md#filemode--the-steering-file-association)).
114
121
  Both present only when the state declares them; **omitted entirely** (never
115
122
  `null`) otherwise.
123
+ - `edges` — the resting state's `on` edges as `[{ pattern, target, describe? }]`
124
+ (declaration order) — the same list the content template sees as `it.edges`
125
+ (see
126
+ [Configuration](configuration.md#on-values--a-target-or-a--to-describe--route-description)).
127
+ A driver relaying a human gate's message has the routing (and each edge's
128
+ human-readable `describe`) alongside the rendered text. **Omitted entirely**
129
+ when the state has no `on` (a commit state); a per-edge `describe` is likewise
130
+ omitted when that edge declares none.
116
131
 
117
132
  ## `gtd run`
118
133
 
@@ -138,12 +153,12 @@ Pure, read-only dry-run reporter — the same resolution `gtd next` performs, bu
138
153
  reporting the resolved state/actor and, for every pending change, which declared
139
154
  `on` pattern (if any) matches it — no mutation, and no CONTENT rendering (the
140
155
  `script`/`prompt`/`message`/`commit` template is never rendered here, unlike
141
- `gtd next`). It DOES render the resolved state's `model:`/`file:` hints (if
142
- declared) through the same `it.vars`-carrying template context `gtd next` uses —
143
- see
156
+ `gtd next`). It DOES render the resolved state's `model:`/`memory:`/`file:`
157
+ hints (if declared) through the same `it.vars`-carrying template context
158
+ `gtd next` uses — see
144
159
  [Configuration](configuration.md#model--the-opaque-harness-hint-template-rendered)
145
- — so a templated `model:`/`file:` failing to render fails `gtd status` too,
146
- exactly like it would fail `gtd next`.
160
+ — so a templated `model:`/`memory:`/`file:` failing to render fails `gtd status`
161
+ too, exactly like it would fail `gtd next`.
147
162
 
148
163
  ```
149
164
  State: working
@@ -155,14 +170,16 @@ Pending:
155
170
 
156
171
  or, on a clean tree: `Pending: (clean)`. A `Model: <value>` line appears right
157
172
  after `Awaits:` when the resolved state declares a `model:` hint, and
158
- `File: <value>`/`Mode: <value>` lines appear after that (in that order) when
159
- declared — each independently, only when set.
173
+ `Memory: <value>`/`File: <value>`/`Mode: <value>` lines appear after that (in
174
+ that order) when declared — each independently, only when set.
160
175
 
161
176
  `--json` emits
162
- `{state, actor, changes: [{status, path, pattern}], model?, file?, mode?}` —
163
- `pattern` is `null` when no declared row matches that change; `model`/`file`/
164
- `mode` are present only when the resolved state declares them (omitted entirely,
165
- never `null`, otherwise):
177
+ `{state, actor, changes: [{status, path, pattern}], model?, memory?, file?, mode?, edges?}`
178
+ — `pattern` is `null` when no declared row matches that change;
179
+ `model`/`memory`/`file`/`mode` are present only when the resolved state declares
180
+ them (omitted entirely, never `null`, otherwise); `edges` is the resting state's
181
+ `on` edges as `[{ pattern, target, describe? }]` (same as `gtd next --json`),
182
+ omitted when the state has no `on`:
166
183
 
167
184
  ```json
168
185
  {
@@ -54,12 +54,14 @@ workflow:
54
54
  message: <string>
55
55
  commit: <string>
56
56
  on: # a mapping, DECLARATION ORDER PRESERVED
57
- "<pattern>": <targetState>
57
+ "<pattern>": <targetState> # short form
58
+ "<pattern>": { to: <targetState>, describe: <sentence> } # with a human-readable route description
58
59
  initial: true # exactly one state across the whole workflow
59
60
  retry:
60
61
  max: <number>
61
62
  otherwise: <targetState>
62
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
63
65
  file: <string> # optional, an Eta template naming the state's steering file — forbidden on a commit state
64
66
  mode: qa | review # optional, requires "file" — forbidden on a commit state
65
67
  ```
@@ -90,6 +92,51 @@ A `vars:` key sibling to `states:` inside `workflow:` declares the workflow's
90
92
  own defaults for `it.vars` — see ["Variables"](#variables) below for the full
91
93
  three-layer picture.
92
94
 
95
+ ### `on:` values — a target, or a `{ to, describe }` route description
96
+
97
+ An `on` row's value is normally the target state name. It may instead be an
98
+ object `{ to: <target>, describe: <sentence> }`, attaching a human-readable
99
+ `describe` — a plain sentence explaining what making that kind of change does
100
+ next. `describe` is **inert to the engine**: it plays no part in matching, is
101
+ never Eta-rendered (exactly like the pattern key itself — see the "Known
102
+ limitation" note below), and never affects a decision. It exists only to be
103
+ surfaced. A state's own edges are handed to its content template as `it.edges`
104
+ (an array of `{ pattern, target, describe? }`, in declaration order), so a human
105
+ gate's `message:` can render a "what each change does next" list from the same
106
+ routing the engine uses — one source of truth:
107
+
108
+ ```yaml
109
+ workflow:
110
+ states:
111
+ await-review:
112
+ actor: human
113
+ message: |
114
+ Review the diff, then run `gtd step human`.
115
+
116
+ What each change does next:
117
+ <% it.edges.forEach(function (e) { if (e.describe) { %>
118
+ <%~ "- " + e.describe + "\n" %>
119
+ <% } }) %>
120
+ on:
121
+ "D .gtd/REVIEW.md":
122
+ to: idle
123
+ describe:
124
+ "Delete `.gtd/REVIEW.md` to approve the whole cycle and rest at
125
+ idle."
126
+ "* **":
127
+ to: grilling
128
+ describe:
129
+ "Change any source file to leave feedback and start another round."
130
+ ```
131
+
132
+ The `<%~ "- " + e.describe + "\n" %>` idiom keeps each bullet's own newline: Eta
133
+ strips the template's line breaks around `<% %>` control tags (its `autoTrim`
134
+ default), not text inside an interpolation. Edges without a `describe` are
135
+ skipped by the `if (e.describe)` guard. `gtd next --json` and
136
+ `gtd status --json` also emit the `edges` array; both omit it when the state has
137
+ no `on` (a commit state), and omit a per-edge `describe` when that edge declares
138
+ none.
139
+
93
140
  ### `model:` — the opaque harness hint, template-rendered
94
141
 
95
142
  A state may declare `model: <string>` — an OPAQUE label (e.g. `smart`, `fast`,
@@ -121,6 +168,61 @@ committed.
121
168
  value) only when the resolved state declares one — it is **omitted entirely**,
122
169
  never emitted as `null`, when unset.
123
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
+
124
226
  ### `file:`/`mode:` — the steering-file association
125
227
 
126
228
  A state may additionally declare `file:` — an Eta template naming THE steering
@@ -217,8 +319,8 @@ head is preserved under `refs/gtd/review-head` (the base under
217
319
 
218
320
  ### Template variables
219
321
 
220
- Every `script`/`prompt`/`message`/`commit`/`model` template is rendered as an
221
- Eta template (`it.<name>`) with:
322
+ Every `script`/`prompt`/`message`/`commit`/`model`/`memory`/`file` template is
323
+ rendered as an Eta template (`it.<name>`) with:
222
324
 
223
325
  | Variable | Meaning |
224
326
  | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -234,9 +336,9 @@ Eta template (`it.<name>`) with:
234
336
 
235
337
  ## Variables
236
338
 
237
- Every template — `script`/`prompt`/`message`/`commit`, and now `model` — sees
238
- `it.vars`: a flat `Record<string, string>` assembled from three layers, **later
239
- wins**:
339
+ Every template — `script`/`prompt`/`message`/`commit`, and `model`/`memory`/
340
+ `file` — sees `it.vars`: a flat `Record<string, string>` assembled from three
341
+ layers, **later wins**:
240
342
 
241
343
  1. **The workflow's own `vars:` key** (sibling to `states:`, shown above) — the
242
344
  workflow author's declared defaults. The bundled default workflow declares
package/docs/loop.md CHANGED
@@ -15,8 +15,8 @@ gtd (v3, the pattern machine) exposes three commands a loop driver combines:
15
15
  A loop is a simple cycle:
16
16
 
17
17
  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`;
18
+ `{state, actor, kind, content, model?, memory?}` field shape. `kind` is the
19
+ dispatch key: `"message"` → halt (a human rest); `"script"` → run `gtd run`;
20
20
  `"prompt"` → feed `content` to the agent, then `gtd step <actor>` yourself
21
21
  once it's done acting. The optional `model` is an opaque string the workflow
22
22
  author chose, which you map onto your own harness's model selection if you
@@ -43,13 +43,15 @@ A minimal bash implementation of the pinned protocol, driving an agent CLI (e.g.
43
43
  reference for what a loop driver must do; keep any other implementation
44
44
  (including `skills/loop/SKILL.md`) consistent with it rather than editing both
45
45
  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"`
46
+ binary, with four additions: it stops with a diagnostic if the same `"prompt"`
47
47
  state/content repeat with no progress (see `skills/loop/SKILL.md`'s "Stall
48
48
  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
49
+ of the default `claude -p`, receiving the prompt via `$GTD_LOOP_PROMPT`; it
50
50
  exports the resolved state's optional `model` hint as `$GTD_LOOP_MODEL`,
51
51
  appending `--model "$GTD_LOOP_MODEL"` to the default `claude -p` invocation
52
- whenever it's non-empty.
52
+ whenever it's non-empty; and it acts on the optional `memory` scope hint (see
53
+ "Agent memory scope" below) — continuing one agent session across consecutive
54
+ same-scope turns and starting fresh when the scope changes.
53
55
 
54
56
  ```bash
55
57
  #!/usr/bin/env bash
@@ -89,15 +91,39 @@ The driver — not the prompt text — owns ending the agent's turn
89
91
  (`gtd step "$actor"` right after the agent acts): every default-workflow agent
90
92
  prompt says explicitly not to run `gtd step agent` itself.
91
93
 
94
+ ## Agent memory scope
95
+
96
+ A `"prompt"` beat may carry an optional `memory` key — an opaque scope label the
97
+ workflow author chose (e.g. `plan`, `build`, `fix`). gtd never interprets it; it
98
+ signals to the driver whether this agent turn should continue from the previous
99
+ turn's memory or start fresh. The rule is a comparison, not a command: track the
100
+ `memory` of the last `"prompt"` beat (script/message beats have none and never
101
+ change it), and when a new `"prompt"` beat's `memory` **equals** the tracked
102
+ value, continue the same agent session; when it **differs** — or it is the first
103
+ turn, or either side has no `memory` — start fresh.
104
+
105
+ This is what makes a loop retain memory while a phase boundary clears it: a loop
106
+ that keeps re-entering one state emits the same label every lap, so the agent
107
+ accumulates context across the loop; the move to the next phase's
108
+ differently-labelled state resets it. `bin/gtd-loop` implements this by mapping
109
+ the signal onto claude's session flags — `--session-id` to pin a fresh session
110
+ when the scope changes, `--resume` to continue it while the scope holds — and
111
+ persists the current scope + session id in the git dir (never the working tree,
112
+ so `gtd status` and the pending diff never see them) so retention survives even
113
+ across the restarts a human gate forces mid-loop. See `skills/loop/SKILL.md`'s
114
+ "Agent memory scope" for the same contract stated for a hand-driven loop.
115
+
92
116
  ## Using a different agent
93
117
 
94
118
  `gtd-loop` defaults to
95
119
  `claude -p "$GTD_LOOP_PROMPT" --dangerously-skip-permissions`, but the agent
96
120
  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:
121
+ runs with the prompt available as `$GTD_LOOP_PROMPT` in its environment, along
122
+ with the resolved state's opaque `model` hint (if any) as `$GTD_LOOP_MODEL` and
123
+ its `memory` scope as `$GTD_LOOP_MEMORY` / `$GTD_LOOP_SESSION_ID` /
124
+ `$GTD_LOOP_MEMORY_RESUME` (`1` when this turn should continue the prior
125
+ session). An adapter that ignores any of these keeps working unchanged. For
126
+ example, to drive a different agent CLI:
101
127
 
102
128
  ```bash
103
129
  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.2.0",
3
+ "version": "3.4.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
package/schema.json CHANGED
@@ -47,9 +47,29 @@
47
47
  },
48
48
  "on": {
49
49
  "type": "object",
50
- "description": "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise).",
50
+ "description": "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise). A value is either the target state name (string) or a { to, describe } object whose describe is a human-readable sentence templates can surface as it.edges (e.g. in a human gate's message).",
51
51
  "additionalProperties": {
52
- "type": "string"
52
+ "oneOf": [
53
+ {
54
+ "type": "string",
55
+ "description": "The target state name."
56
+ },
57
+ {
58
+ "type": "object",
59
+ "additionalProperties": false,
60
+ "required": ["to"],
61
+ "properties": {
62
+ "to": {
63
+ "type": "string",
64
+ "description": "The target state name."
65
+ },
66
+ "describe": {
67
+ "type": "string",
68
+ "description": "Human-readable sentence describing where this change routes; surfaced verbatim (never Eta-rendered) to templates as it.edges[].describe."
69
+ }
70
+ }
71
+ }
72
+ ]
53
73
  }
54
74
  },
55
75
  "initial": {
@@ -77,6 +97,10 @@
77
97
  "type": "string",
78
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."
79
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
+ },
80
104
  "file": {
81
105
  "type": "string",
82
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."