@pmelab/gtd 8.3.0 → 8.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/README.md CHANGED
@@ -95,8 +95,8 @@ reads intent first, glob second.
95
95
  The unified workflow has **entry points behind a green-baseline gate, into one
96
96
  shared tail**. Every entry first runs your test suite and only starts once it's
97
97
  green — you never build (or review) on top of a red baseline; a red run halts
98
- and tells you to repair it first (that's what `gtd fix` is for). The two
99
- steering-file entries are chosen by which file you create:
98
+ and tells you to repair it first (that's what `gtd --entry fix-precheck` is
99
+ for). The two steering-file entries are chosen by which file you create:
100
100
 
101
101
  - Create **`.gtd/TODO.md`** with a short sketch to start the **simple** flow: an
102
102
  agent develops your sketch into a concrete plan — deciding open points itself
@@ -105,39 +105,66 @@ steering-file entries are chosen by which file you create:
105
105
  runs your tests (looping on failures) — the check run also mechanically sweeps
106
106
  `.gtd/TODO.md` and other spent steering files, so a plan that's left in place
107
107
  by mistake never leaks into the review diff or the final squash.
108
- - Create **`.gtd/REQUIREMENTS.md`** to start the **advanced** flow: two-phase
109
- product then technical Q&A (`.gtd/REQUIREMENTS.md` → `.gtd/ARCHITECTURE.md`) —
110
- each open question offers a couple of candidate answers plus a
111
- `- [ ] _your answer_` slot, and you tick exactly one per question (the gate
112
- won't let a phase advance while any question is unanswered) — then
113
- decomposition into work **packages** (each a set of independent tasks a single
114
- build turn fans out to parallel subagents), a per-package test loop, and a
115
- per-package **agentic review** that verifies the package against its spec.
108
+ - Create **`.gtd/REQUIREMENTS.md`** to start the **advanced** flow: one design
109
+ conversation across product then technical Q&A (`.gtd/REQUIREMENTS.md` →
110
+ `.gtd/ARCHITECTURE.md`) and decomposition — each open question offers a couple
111
+ of candidate answers plus a `- [ ] _your answer_` slot, and you tick exactly
112
+ one per question (the gate won't let a phase advance while any question is
113
+ unanswered) — then decomposition into work **packages** (each a set of
114
+ independent tasks a single build turn fans out to parallel subagents), a
115
+ per-package test loop, and a per-package **agentic review** that verifies the
116
+ package against its spec. All three phases share one machine identity, so the
117
+ agent never re-explores the codebase from scratch between them, and the
118
+ human's answers carry forward into decomposition.
116
119
 
117
120
  Both flows converge on the same tail: an agent hands you a `.gtd/REVIEW.md`
118
121
  checkbox review of the diff — the prompt never inlines the diff itself; it names
119
122
  the commit the changes are based at and the agent runs `git diff` to read the
120
- range before writing the review. Tick a box as you review each hunk (ticking
121
- just records "I read this"), and leave a **comment** to request changes: a note
122
- on a line, an inline `// TODO`-style comment in the code, or a direct code edit.
123
- Any comment sends a build + re-review round — an agent first turns your comments
124
- into an explicit instruction list, then a build turn implements it (a re-review
125
- then covers only the follow-through, and a hand-edit is treated as your own fix
126
- the agent completes without reverting your lines; a comment can't be silently
127
- dropped — a build turn that addresses nothing is refused). Ticking every box
128
- with no comment is the sign-off, which collapses the whole cycle into one commit
129
- (a **squash finale** whose message an agent drafts). Stepping with a box still
123
+ range before writing the review. While the cycle rests at that gate, gtd opens a
124
+ **review checkout window**: HEAD is rewound to the review base with the working
125
+ tree untouched, so the whole reviewable change shows up as ordinary uncommitted
126
+ changes in your editor's normal git integration (and files added during the
127
+ cycle show up as ordinary untracked files, so discarding one deletes it). The
128
+ window closes on the next gtd invocation. Tick a box as you review each hunk
129
+ (ticking just records "I read this"), and leave a **comment** to request
130
+ changes: a note on a line, an inline `// TODO`-style comment in the code, or a
131
+ direct code edit. Any comment sends a build + re-review round — an agent first
132
+ turns your comments into an explicit instruction list, then a build turn
133
+ implements it (a re-review then covers only the follow-through, and a hand-edit
134
+ is treated as your own fix the agent completes without reverting your lines; a
135
+ comment can't be silently dropped — a build turn that addresses nothing is
136
+ refused). For the simple flow, the build turn that follows through on feedback
137
+ and the turn that drafts the final squash message both resume the same session
138
+ that built the feature in the first place, since the review tail is nested
139
+ inside that build identity rather than sitting beside it. Ticking every box with
140
+ no comment is the sign-off, which collapses the whole cycle into one commit (a
141
+ **squash finale** whose message an agent drafts). Stepping with a box still
130
142
  unticked and no comment is refused (finish reviewing first), as is deleting
131
143
  `.gtd/REVIEW.md`.
132
144
 
133
- The same review tail also has a direct entry point — `gtd review <commitish>`
134
- starts a brand new process reviewing `<commitish>..HEAD` with no cycle of its
135
- own, e.g. a colleague's PR branch. Its squash keeps and describes only the fixes
136
- made _during_ the review (not the reviewed changeset); a clean sign-off with no
137
- fixes becomes an empty `chore: human review` commit. A fourth entry, `gtd fix`,
138
- starts from a clean `idle` and goes straight into repairing a red baseline —
139
- repair, review, and squash into one commit. If the suite is already green there
140
- is nothing to fix, and the log is left untouched — no commit is left behind.
145
+ The same review tail also has a direct entry point —
146
+ `gtd --entry review-gate.check --var reviewBase=<commitish>` starts a brand new
147
+ process reviewing `<commitish>..HEAD` with no cycle of its own, e.g. a
148
+ colleague's PR branch (`review-gate.check`'s `reviewBase:` is a template bound
149
+ to the `reviewBase` var, so supplying it via `--var` fixes the whole process's
150
+ diff base to that commitish). Its squash keeps and describes only the fixes made
151
+ _during_ the review (not the reviewed changeset); a clean sign-off with no fixes
152
+ becomes an empty `chore: human review` commit. A fourth entry,
153
+ `gtd --entry fix-precheck`, starts from a clean `idle` and goes straight into
154
+ repairing a red baseline — repair, review, and squash into one commit. If the
155
+ suite is already green there is nothing to fix, and the log is left untouched —
156
+ no commit is left behind.
157
+
158
+ `--entry` itself isn't limited to states flagged `entry: true` — it accepts
159
+ **any** declared, non-commit state of the active workflow (see
160
+ [`gtd step`](#commands)/`gtd --entry` below). `entry: true` only marks a state
161
+ as an _extra_ reachability root (and drives a badge in `gtd visualize`) for a
162
+ state that would otherwise be unreachable from the ordinary `idle` rest —
163
+ `review-gate.check` and `fix-precheck` need it for exactly that reason, while
164
+ `plan-gate.check`/`spec-gate.check` carry it too (the bundled template dedups
165
+ the three `entryGate` instances into one shared machine, so flagging the shared
166
+ state flags all three) even though `idle` already reaches them the ordinary way.
167
+ `entry: true` is not a precondition for `--entry` to target a state.
141
168
 
142
169
  Every agent state routes its model through two `vars` tiers — `plannerModel`
143
170
  (heavier planning and review) and `coderModel` (the coding turns) — so you can
@@ -178,16 +205,16 @@ Commands:
178
205
  (or squash) the one resulting transition. Pass
179
206
  --cost=<n> (optionally --model=<name>) to record the
180
207
  just-finished invocation's token cost and model on the
181
- turn commit (summed into it.processCost/processCostByModel)
182
- review <commitish>
183
- Start a NEW review process at the workflow's declared
184
- review-entry state (reviewEntry: true), reviewing
185
- <commitish>..HEAD — e.g. a colleague's PR branch. Requires
186
- a clean tree resting at the workflow's initial state
187
- fix Start a NEW process at the workflow's declared fix-entry
188
- state (fixEntry: true) that goes straight into repairing the
189
- current failing tests. Requires a clean tree resting at the
190
- workflow's initial state
208
+ turn commit (summed into it.processCost/processCostByModel).
209
+ Pass --entry <state> to start a brand NEW process at
210
+ <state> instead — any declared, non-commit state (e.g.
211
+ review-gate.check or fix-precheck on the bundled unified
212
+ template) — with repeatable --var <name>=<value> supplying
213
+ that new process's fixed it.vars overrides
214
+ (no command) --entry <state>
215
+ Short form of 'step human --entry <state>' — starts a new
216
+ process authenticated as human, e.g.
217
+ 'gtd --entry review-gate.check'
191
218
  abandon End the process currently underway without completing it:
192
219
  close any open review checkout window, then rewind HEAD to
193
220
  the commit the process started from, keeping everything it
@@ -219,6 +246,15 @@ Options:
219
246
  --no-open (gtd visualize only) do not open the browser
220
247
  --cost=<n> (gtd step only) record the invocation's token cost
221
248
  --model=<name> (gtd step only, with --cost) tag that cost's model
249
+ --entry <state> (gtd step, or with no command at all) start a brand new
250
+ process at <state> — any declared, non-commit state —
251
+ instead of stepping the one currently resting. Not
252
+ combinable with --cost/--model (an entry is not a metered
253
+ agent turn)
254
+ --var <name>=<value>
255
+ (with --entry; repeatable) supply a fixed it.vars
256
+ override for the new process; the name must already be
257
+ declared by the workflow's own vars: or the .gtdrc vars:
222
258
  --once (bare gtd or gtd loop only) run exactly one loop beat (one
223
259
  human-gate capture, one script check+step, or one agent
224
260
  prompt+step), then exit
@@ -236,13 +272,18 @@ touching the repository. Every other (recognized) command must be run from the
236
272
  history relative to cwd, so it refuses with a clear error if invoked from a
237
273
  subdirectory.
238
274
 
239
- `--json`, `--cost=<n>`, and `--model=<name>` (the latter two only for
240
- `gtd step`) are the only long options the compiled bundle recognizes. Any other
241
- `--` option (including a typo like `--jsn`) is rejected with a usage error
242
- rather than silently ignored, so a mistyped flag can never degrade a JSON caller
243
- to plain-text mode. A bare `--cost`/`--model` with no value, a non-numeric or
244
- negative `--cost`, an empty `--model`, `--model` without `--cost`, or either
245
- flag on any command other than `gtd step` are all usage errors.
275
+ `--json`, `--cost=<n>`, `--model=<name>` (the latter two only for `gtd step`),
276
+ `--entry <state>` (`gtd step` or no command at all), and `--var <name>=<value>`
277
+ (with `--entry`, repeatable) are the only long options the compiled bundle
278
+ recognizes. `--entry`/`--var` accept both the `--flag=value` and the
279
+ space-separated `--flag value` form. Any other `--` option (including a typo
280
+ like `--jsn`) is rejected with a usage error rather than silently ignored, so a
281
+ mistyped flag can never degrade a JSON caller to plain-text mode. `--var` with
282
+ no `--entry`, a duplicate `--var` name, or `--cost`/`--model` combined with
283
+ `--entry` are all usage errors too. A bare `--cost`/`--model` with no value, a
284
+ non-numeric or negative `--cost`, an empty `--model`, `--model` without
285
+ `--cost`, or either flag on any command other than `gtd step` are all usage
286
+ errors.
246
287
 
247
288
  `--once` is a separate, bash-level flag handled entirely by the `bin/gtd` driver
248
289
  itself, stripped before anything reaches the bundle.
@@ -308,12 +349,45 @@ name surfaced in `gtd next --json`/`gtd status`. The driver uses it for its
308
349
  per-beat progress lines; an outer wrapper (a terminal multiplexer, a notifier)
309
350
  can use it the same way.
310
351
 
311
- A state can also declare an optional `memory:` scope label: consecutive agent
312
- turns sharing the same label continue one agent session (remembered in the git
313
- dir, so it survives restarts across the same worktree), while a changed or
314
- absent label starts a fresh one. If the remembered session no longer exists
315
- (retention expired, `~/.claude/projects` wiped, a machine change), the driver
316
- degrades to a fresh session with a warning instead of stopping the loop.
352
+ Memory is **entry-scoped to a machine**, not a state-authored label: each
353
+ machine instance (a node in the `machines:` tree, e.g. `build`, `build.health`,
354
+ `packages.item`, `packages.item.health`) owns its own conversational scope, and
355
+ a `prompt`-content state's `memory` key — surfaced in `gtd next --json`/
356
+ `gtd status --json`'s `memory` field, and as a `Memory: <key>` line in plain
357
+ `gtd status` — is computed, never authored, as `<scope>#<hash7>`: `<scope>` is
358
+ that machine instance's dotted path (the root instance is shown as `root`), and
359
+ `<hash7>` anchors to the commit the CURRENT unbroken entry into that scope
360
+ started FROM. Entering a **descendant** scope (e.g. dipping from `build` into
361
+ `build.health`) does not break the parent's unbroken run — a full agent turn in
362
+ a nested child machine, then back to the parent, still resumes the SAME parent
363
+ conversation; entering a **sibling or unrelated** scope does start a fresh one.
364
+ The bundled template's `build.review` (the human review tail) is a worked
365
+ example: it is nested INSIDE `build` (the builder's own machine) precisely
366
+ because that descendant relationship is what lets `build.addressing` and
367
+ `build.squashing` resume the session that built the feature across a full review
368
+ round-trip, instead of a root-level sibling breaking that run on every pass
369
+ through the tail. Two instances of the same reusable machine (e.g.
370
+ `build.health` and `packages.item.health`, both instantiating `healthGate`) get
371
+ different scopes and so never share a key, even though they're the "same shaped"
372
+ machine. One consequence is a structural guarantee: **a reviewer's turn never
373
+ resumes an implementer's session, and vice versa** — a reviewer machine and the
374
+ implementer machine it reviews are always different instances with different
375
+ scopes.
376
+
377
+ The loop driver (`bin/gtd`) tracks this as a per-scope **session table** — one
378
+ `<key> <session_id>` row per scope — persisted at `$gitdir/gtd-loop-memory` (the
379
+ git dir, never the working tree, so `gtd status` and the pending diff never see
380
+ it), not a single "last" value: this is what lets a scope's session survive an
381
+ excursion into a child machine's own agent turn (the child writes its own row;
382
+ the parent's row is untouched). On each agent-prompt turn, the driver looks the
383
+ current key up by exact string match against the table (a hit resumes that row's
384
+ session, a miss starts fresh) and, on write, replaces only the ONE row whose
385
+ scope matches the current key's scope — every other scope's row is left as-is.
386
+ This is exported to the agent adapter as `$GTD_LOOP_MEMORY` (the key),
387
+ `$GTD_LOOP_SESSION_ID`, and `$GTD_LOOP_MEMORY_RESUME` (`"1"` when resuming). If
388
+ the remembered session no longer exists (retention expired, `~/.claude/projects`
389
+ wiped, a machine change), the driver degrades to a fresh session with a warning
390
+ instead of stopping the loop.
317
391
 
318
392
  ### Terminal-multiplexer status: a herdr wrapper
319
393
 
@@ -455,38 +529,70 @@ workflow:
455
529
  <name>:
456
530
  format: <shell command> # at least one of format/validate
457
531
  validate: <shell command>
458
- states:
532
+ entry:
533
+ default: <machine name> # which machine is the ROOT instance
534
+ machines:
459
535
  <name>:
460
- actor: <string> # forbidden on a commit state, required otherwise
461
- script: <string> # exactly one of script/prompt/message/commit
462
- prompt: <string>
463
- message: <string>
464
- commit: <string>
465
- on: # a mapping, DECLARATION ORDER PRESERVED
466
- "<pattern>": <targetState> # short form
467
- "<pattern>": {
468
- to: <targetState>,
469
- describe: <sentence>,
470
- action: <label>,
471
- } # description/action
472
- initial: true # exactly one state across the whole workflow
473
- retry:
474
- max: <number>
475
- otherwise: <targetState>
476
- model: <string> # optional, opaque harness hint — forbidden on a commit state
477
- memory: <string> # optional, opaque memory-scope label — forbidden on a commit state
478
- file: <string> # optional, an Eta template naming the state's steering file
479
- mode: <modeName> # optional, requires "file" — a built-in (qa/review/prose) or a `modes:` entry
536
+ model: <string> # optional, opaque harness hint — stamped onto every one of THIS machine's own `prompt` states; declared ONCE per machine, never per state
537
+ params: [<param>, ...] # optional, advisory — documents which $params a caller may bind
538
+ entry: <local or ref key> # this machine's own default local, resolved recursively
539
+ states:
540
+ <local>:
541
+ actor: <string> # forbidden on a commit state, required otherwise
542
+ script: <string> # exactly one of script/prompt/message/commit
543
+ prompt: <string>
544
+ message: <string>
545
+ commit: <string>
546
+ on: # a mapping, DECLARATION ORDER PRESERVED
547
+ "<pattern>": <targetState> # short form
548
+ "<pattern>": {
549
+ to: <targetState>,
550
+ describe: <sentence>,
551
+ action: <label>,
552
+ } # description/action
553
+ retry:
554
+ max: <number>
555
+ otherwise: <targetState>
556
+ file: <string> # optional, an Eta template naming the state's steering file
557
+ mode: <modeName> # optional, requires "file" — a built-in (qa/review/prose) or a `modes:` entry
558
+ reviewWindow: true # optional — open the review checkout window at rest here
559
+ reviewBase: true # optional — anchor the review window's diff base to this state's most-recent commit
560
+ # reviewBase: <Eta template> # OR a template — rendered (only meaningful entering via --entry) to a commitish that fixes the WHOLE PROCESS's diff base
561
+ entry: true # optional — an EXTRA reachability root (`entries.manual`), enterable via `gtd --entry <this state's qualified name>` — NOT a precondition for `--entry` (any declared, non-commit state is a valid target)
562
+ <local>: { machine: <name>, with: { <param>: <value> } } # a REFERENCE — instantiates <name> as a child, qualified as `<local>.<childLocal>`
480
563
  ```
481
564
 
565
+ There is no `memory:` key anywhere in this shape — a state's memory scope is
566
+ never authored, only computed from its position in the machine tree (see
567
+ [Driving the loop](#driving-the-loop) above for the key format and the driver's
568
+ per-scope session table).
569
+
570
+ The top-level `entry:` key (naming the root machine, `entry.default`) and a
571
+ state's own `entry: true` flag are the same word at two different levels, by
572
+ design: one selects the workflow's root machine, the other opts one state in as
573
+ an extra manual entry point.
574
+
575
+ A workflow is authored as a TREE of reusable, parameterized machines — a
576
+ gate/loop written once and instantiated several times with different `with:`
577
+ bindings (dedup), or a complex cluster grouped under one name for source
578
+ comprehension (encapsulation). Every reference is expanded at load time into
579
+ concrete, qualified states (`<local>.<childLocal>`, however deep) before the
580
+ engine ever sees the definition — see `src/Machines.ts` for the mechanism.
581
+ MACHINE BOUNDARIES ARE THE UNIT OF CONVERSATIONAL IDENTITY: a machine that holds
582
+ an identity (a planner or a coder persona) declares its own `model:` once, at
583
+ the machine level, instead of repeating it per state — and, per the memory rule
584
+ above, two references to the SAME machine (a dedup instantiation) are always two
585
+ independent instances with two independent memory scopes, never one shared
586
+ conversation across both call sites.
587
+
482
588
  Besides `it.vars` (below), a `script`/`prompt`/`message`/`commit` template sees:
483
589
 
484
590
  - **`it.startCommit`** — the process's diff base (the commit the current cycle
485
- started from, or a `gtd review <commitish>` entry's resolved base).
591
+ started from, or the base a `--var reviewBase=<commitish>` entry resolved to).
486
592
  - **`it.reviewBase`** — the previous review round's boundary, falling back to
487
593
  `it.startCommit` on a first review.
488
594
  - **`it.retainedBase`** — the process's trace/retry boundary, what a squash
489
- actually keeps (never moved by a `gtd review` entry).
595
+ actually keeps (never moved by a review entry's fixed base).
490
596
  - **`it.currentCommit`** / **`it.previousCommit`** — HEAD's hash and its parent,
491
597
  at render time.
492
598
 
@@ -499,26 +605,103 @@ Authoring or editing a workflow with a coding agent? `skills/authoring/SKILL.md`
499
605
  is the agent-facing contract for producing a valid `workflow:` — the state
500
606
  model, pattern grammar, load-time rules, and how to verify a change compiles.
501
607
 
608
+ > **Upgrading from a pre-8.2 `workflow:`?** The old flat `states:` shape (with a
609
+ > per-state `initial: true`/`reviewEntry: true`/`fixEntry: true` flag) is no
610
+ > longer accepted — finish or `gtd abandon` any in-flight cycle before
611
+ > upgrading, since the old and new shapes aren't compatible mid-cycle. Wrap your
612
+ > states under a single
613
+ > `machines: { <name>: { entry: <initial state>, states: {...} } }` and declare
614
+ > `entry: { default: <name> }` at the top level (moving any
615
+ > `reviewEntry`/`fixEntry` state to a plain per-state `entry: true` flag,
616
+ > entered via `gtd --entry <state>` — see the next note).
617
+
618
+ > **Upgrading a `workflow:` that still declares `entry.review`/`entry.fix`?**
619
+ > Those two keys, and the `gtd review <commitish>`/`gtd fix` commands that used
620
+ > them, are gone. Replace `entry.review: <target>`/`entry.fix: <target>` with a
621
+ > plain `entry: true` flag on that same state, and enter it with
622
+ > `gtd step <actor> --entry <state>` (or the actor-less short form
623
+ > `gtd --entry <state>`) instead of the removed commands.
624
+ > `gtd review <commitish>` required a clean tree and a `<commitish>` argument;
625
+ > the replacement instead captures whatever is pending in the working tree (just
626
+ > like an ordinary `gtd step`) and takes the commitish as a
627
+ > `--var reviewBase=<commitish>` override consumed by that state's own
628
+ > template-form `reviewBase:` (see the `workflow:` shape above and
629
+ > [`gtd --entry`](#commands)). `gtd fix` likewise becomes
630
+ > `gtd --entry <the state that was entry.fix>` (e.g. the bundled template's
631
+ > `gtd --entry fix-precheck`).
632
+
633
+ > **Upgrading a `workflow:` that still declares a per-state `model:` or
634
+ > `memory:`?** `model:` moved from a state key to a MACHINE key: declare it once
635
+ > on the `machines.<name>:` entry instead of on every one of that machine's
636
+ > states — it is stamped onto every one of that machine's own `prompt` states
637
+ > automatically (see the `workflow:` shape above). A state that still declares
638
+ > its own `model:` is a load error naming the machine to move it to, never a
639
+ > silently ignored key. `memory:` is gone outright, with **no** replacement key
640
+ > — a workflow author simply removes it; a state's memory scope is now computed
641
+ > from its position in the machine tree instead of authored (see
642
+ > [Driving the loop](#driving-the-loop)). The bundled template was also
643
+ > restructured so machine boundaries line up with this new identity model,
644
+ > renaming twenty-two states:
645
+ >
646
+ > - `building` → `build.building`
647
+ > - `decompose` → `build.decompose` → `design.decompose`
648
+ > - `squashing` → `build.squashing`
649
+ > - `review.building` → `build.addressing`
650
+ > - `packages.building` → `packages.item.building`
651
+ > - `packages.closing` → `packages.item.closing`
652
+ > - `packages.health.check` → `packages.item.health.check`
653
+ > - `packages.health.fix` → `packages.item.fix-suite`
654
+ > - `packages.health.escalate` → `packages.item.health.escalate`
655
+ > - `packages.spec.review` → `packages.item.spec.review`
656
+ > - `packages.spec.fix` → `packages.item.fix-spec`
657
+ > - `build.check` → `build.health.check`
658
+ > - `build.escalate` → `build.health.escalate`
659
+ > - `review.reviewing` → `build.review.reviewing`
660
+ > - `review.await-review` → `build.review.await-review`
661
+ > - `review.deciding` → `build.review.deciding`
662
+ > - `review.collecting` → `build.review.collecting`
663
+ > - `product.author` → `design.product-author`
664
+ > - `product.answer` → `design.product-answer`
665
+ > - `technical.author` → `design.technical-author`
666
+ > - `technical.answer` → `design.technical-answer`
667
+ >
668
+ > (`decompose`'s two hops both land in this same release, so a process upgrading
669
+ > from before either restructure only ever sees one hop: `decompose` →
670
+ > `design.decompose`.) Because of these renames, an in-flight process left
671
+ > resting at one of the old qualified state names can no longer be resumed after
672
+ > upgrading — those names no longer exist in the definition, and gtd refuses
673
+ > loudly rather than silently treating the rest as idle. Run `gtd abandon` to
674
+ > discard it and start over (or finish the process on the pre-upgrade workflow
675
+ > version first).
676
+
502
677
  ### Variables
503
678
 
504
- Every template — `script`/`prompt`/`message`/`commit`, and
505
- `model`/`memory`/`file` — sees `it.vars`: a flat `Record<string, string>`
506
- assembled from three layers, **later wins**:
679
+ Every template — `script`/`prompt`/`message`/`commit`, a machine's own `model:`,
680
+ and a state's `file:` — sees `it.vars`: a flat `Record<string, string>`
681
+ assembled from four layers, **later wins**:
507
682
 
508
- 1. **The workflow's own `vars:` key** (sibling to `states:`) — the workflow
509
- author's declared defaults. The unified template declares
510
- `vars: { testCommand: "npm test" }`, read by `checking`'s script as
683
+ 1. **The workflow's own `vars:` key** (sibling to `entry:`/`machines:`) — the
684
+ workflow author's declared defaults. The unified template declares
685
+ `vars: { testCommand: "npm test" }`, read by `build.health.check`'s script as
511
686
  `<%~ it.vars.testCommand %>`.
512
687
  2. **A top-level `.gtdrc` `vars:` key** (a sibling of `workflow:`, NOT nested
513
688
  inside it) — per-repo tuning without redefining the whole workflow.
514
- 3. **`GTD_<UPPERCASE-name>` environment variables** — highest precedence,
689
+ 3. **The current process's entry `--var` overrides**, if it was started via
690
+ `gtd step <actor> --entry <state>`/`gtd --entry <state>` — repeatable
691
+ `--var <name>=<value>` flags fixed at the moment of entry and recorded as
692
+ `Gtd-Var: <name>=<value>` trailers on the process's oldest commit, re-parsed
693
+ on every turn for as long as that process is underway. Each `--var` name must
694
+ already be declared by layer 1 or 2; an undeclared name is a usage error, not
695
+ a silent no-op.
696
+ 4. **`GTD_<UPPERCASE-name>` environment variables** — highest precedence,
515
697
  checked at every invocation, case-insensitively against each name already
516
- declared by layer 1 or 2: `GTD_TESTCOMMAND` overrides `testCommand`. The
517
- environment can only OVERRIDE a name a config layer already declared — a
698
+ declared by layers 1–3: `GTD_TESTCOMMAND` overrides `testCommand`. The
699
+ environment can only OVERRIDE a name an earlier layer already declared — a
518
700
  `GTD_*` var matching no declared name is silently ignored.
519
701
 
520
702
  Values in layers 1–2 must be YAML scalars (string/number/boolean), coerced to
521
- strings at load time; an object or array value is a load error.
703
+ strings at load time; an object or array value is a load error. A `--var` value
704
+ (layer 3) is always a single-line string as given on the command line.
522
705
 
523
706
  ```yaml
524
707
  # .gtdrc — overriding the unified template's testCommand
@@ -561,9 +744,9 @@ Those findings include the **semantic graph checks**: every `on` target and
561
744
 
562
745
  Many of these problems never reach gtd at all if your editor validates against
563
746
  the [published schema](#schema), which fully types the `workflow:` key. The
564
- rules JSON Schema cannot express — exactly one content kind, exactly one
565
- `initial: true`, targets naming defined states, reachability — remain the
566
- compiler's job at load time.
747
+ rules JSON Schema cannot express — exactly one content kind, `entry.default`
748
+ resolving to a real state, targets naming defined states, reachability — remain
749
+ the compiler's job at load time.
567
750
 
568
751
  ## Repository requirements
569
752
 
package/bin/gtd CHANGED
@@ -369,11 +369,63 @@ new_session_id() {
369
369
  uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || echo ""
370
370
  }
371
371
 
372
+ # scope_of <key> — the scope part of a "<scope>#<hash>" memory key: everything
373
+ # before the first "#". Used to decide which existing table row a fresh entry
374
+ # into a scope displaces (see write_scope_session).
375
+ scope_of() {
376
+ printf '%s' "${1%%#*}"
377
+ }
378
+
379
+ # lookup_scope_session <key> — print the session id remembered for EXACTLY
380
+ # this key (a row whose first field string-equals it), or fail with no output
381
+ # if no row matches (a lookup miss -> the caller starts fresh). $memory_marker
382
+ # absent counts as no rows at all.
383
+ lookup_scope_session() {
384
+ local key="$1" row_key row_sid
385
+ [[ -f "$memory_marker" ]] || return 1
386
+ while read -r row_key row_sid; do
387
+ if [[ "$row_key" == "$key" ]]; then
388
+ printf '%s' "$row_sid"
389
+ return 0
390
+ fi
391
+ done <"$memory_marker"
392
+ return 1
393
+ }
394
+
395
+ # write_scope_session <key> <session_id> — replace the ONE row for this key's
396
+ # SCOPE (scope_of) with a fresh row for <key>, leaving every OTHER scope's row
397
+ # in the table untouched. A scope has at most one live conversation at any
398
+ # moment, so an existing row for this scope is dead the instant a new entry
399
+ # into it begins — this is the entire pruning story, no process concept
400
+ # needed (see the module's own memory-scope doc comment below). The table is
401
+ # rewritten via a sibling temp file + rename, so a crash mid-write never
402
+ # leaves a half-written table.
403
+ write_scope_session() {
404
+ local key="$1" session_id="$2" scope tmp row_key row_sid
405
+ scope="$(scope_of "$key")"
406
+ tmp="$(mktemp "${memory_marker}.XXXXXX")"
407
+ if [[ -f "$memory_marker" ]]; then
408
+ while read -r row_key row_sid; do
409
+ [[ "$(scope_of "$row_key")" == "$scope" ]] && continue
410
+ printf '%s %s\n' "$row_key" "$row_sid" >>"$tmp"
411
+ done <"$memory_marker"
412
+ fi
413
+ printf '%s %s\n' "$key" "$session_id" >>"$tmp"
414
+ mv "$tmp" "$memory_marker"
415
+ }
416
+
372
417
  prev_state=""
373
418
  prev_content=""
374
419
  prev_head=""
375
- # Where the last "prompt" turn's `memory` scope + session id are remembered
376
- # across iterations and across restarts. In the git dir, never the work tree.
420
+ # Where each scope's remembered agent session is persisted across iterations
421
+ # and across restarts, as a TABLE — one "<key> <session_id>" row per scope
422
+ # (keys contain no whitespace, so plain field-splitting is safe) — never a
423
+ # single last-label value: with machine-scoped memory, a scope's key can
424
+ # legitimately reappear AFTER the loop has run turns in a completely
425
+ # different (or nested) scope — e.g. build.fix -> into a nested health-gate
426
+ # child's own agent turn -> back to build.fix, still the same builder
427
+ # conversation. "The last label" can't represent that; a table where each
428
+ # scope owns exactly one row can. In the git dir, never the work tree.
377
429
  memory_marker="$(worktree_git_dir 2>/dev/null || echo .git)/gtd-loop-memory"
378
430
 
379
431
  # Truncated once here (not every iteration), so the file always reflects the
@@ -418,15 +470,17 @@ invoke_agent() {
418
470
 
419
471
  # run_agent_turn <prompt> <resume> — invoke_agent, with one recovery: a
420
472
  # resumed turn (do_resume == 1) that fails AND whose output carries the
421
- # missing-session signature (session_gone_since) means the marker's session no
422
- # longer exists on disk. Rather than failing the turn (and every turn after
423
- # it, since the dead id would just be resumed again), mint a fresh session,
424
- # rewrite $memory_marker with it — the self-healing part, so a later launch
425
- # doesn't repeat this — and retry the SAME prompt once with resume off. $resume
426
- # and $session_id are the enclosing iteration's globals; recovery rewrites them
427
- # too, so a later validate-gate re-prompt in the same turn resumes the newly
428
- # pinned session. Blind to the failure mode when session_gone_since can't tell
429
- # (no log configured): a genuine agent failure then correctly propagates.
473
+ # missing-session signature (session_gone_since) means the CURRENT scope's
474
+ # remembered session no longer exists on disk. Rather than failing the turn
475
+ # (and every turn after it, since the dead id would just be resumed again),
476
+ # mint a fresh session and rewrite ONLY this scope's row in $memory_marker
477
+ # (write_scope_session — every other scope's row survives untouched) — the
478
+ # self-healing part, so a later launch doesn't repeat this — and retry the
479
+ # SAME prompt once with resume off. $resume and $session_id are the enclosing
480
+ # iteration's globals; recovery rewrites them too, so a later validate-gate
481
+ # re-prompt in the same turn resumes the newly pinned session. Blind to the
482
+ # failure mode when session_gone_since can't tell (no log configured): a
483
+ # genuine agent failure then correctly propagates.
430
484
  run_agent_turn() {
431
485
  local prompt="$1" do_resume="$2" offset status=0
432
486
  offset="$(log_offset)"
@@ -436,7 +490,7 @@ run_agent_turn() {
436
490
  emit_warn "the remembered agent session for '$memory' is gone — continuing in a fresh session (this turn starts without its previous context)"
437
491
  session_id="$(new_session_id)"
438
492
  resume=0
439
- printf '%s\n%s\n' "$memory" "$session_id" >"$memory_marker"
493
+ write_scope_session "$memory" "$session_id"
440
494
  invoke_agent "$prompt" 0
441
495
  return $?
442
496
  fi
@@ -574,29 +628,26 @@ $next_out"
574
628
  prev_content="$content"
575
629
  prev_head="$cur_head"
576
630
 
577
- # Agent-memory scope: compare this turn's `memory` label against the last
578
- # prompt turn's (persisted in $memory_marker). Same non-empty label AND a
579
- # known prior session -> resume that session; otherwise start fresh with a
580
- # new session id. An unset label carries no constraint (harness default) and
581
- # clears the marker so a later labelled turn can't accidentally match it.
582
- prev_memory=""
583
- prev_session=""
584
- if [[ -f "$memory_marker" ]]; then
585
- prev_memory="$(sed -n 1p "$memory_marker")"
586
- prev_session="$(sed -n 2p "$memory_marker")"
587
- fi
631
+ # Agent-memory scope: look this turn's `memory` key up in the per-scope
632
+ # table (lookup_scope_session — exact string equality against a row's full
633
+ # key, "<scope>#<hash>", not just its scope part). A hit -> resume that
634
+ # session; a miss -> start fresh with a new session id, then write ONLY this
635
+ # key's scope's row (write_scope_session), leaving every other scope's row
636
+ # untouched — a scope can legitimately go quiet and reappear later (see the
637
+ # $memory_marker declaration's own comment) without losing its place. An
638
+ # absent/empty key carries no constraint (harness default) and leaves the
639
+ # table completely untouched — it may hold live sessions for OTHER scopes
640
+ # this turn knows nothing about, so it's neither read nor written here.
588
641
  resume=0
589
642
  session_id=""
590
643
  if [[ -n "$memory" ]]; then
591
- if [[ "$memory" == "$prev_memory" && -n "$prev_session" ]]; then
644
+ if found_session_id="$(lookup_scope_session "$memory")"; then
592
645
  resume=1
593
- session_id="$prev_session"
646
+ session_id="$found_session_id"
594
647
  else
595
648
  session_id="$(new_session_id)"
596
649
  fi
597
- printf '%s\n%s\n' "$memory" "$session_id" >"$memory_marker"
598
- else
599
- rm -f "$memory_marker"
650
+ write_scope_session "$memory" "$session_id"
600
651
  fi
601
652
 
602
653
  # Run the agent turn (see run_agent_turn's own comment for the adapter