@pmelab/gtd 8.3.1 → 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,14 +105,17 @@ 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
@@ -130,19 +133,38 @@ turns your comments into an explicit instruction list, then a build turn
130
133
  implements it (a re-review then covers only the follow-through, and a hand-edit
131
134
  is treated as your own fix the agent completes without reverting your lines; a
132
135
  comment can't be silently dropped — a build turn that addresses nothing is
133
- refused). Ticking every box with no comment is the sign-off, which collapses the
134
- whole cycle into one commit (a **squash finale** whose message an agent drafts).
135
- Stepping with a box still unticked and no comment is refused (finish reviewing
136
- first), as is deleting `.gtd/REVIEW.md`.
137
-
138
- The same review tail also has a direct entry point — `gtd review <commitish>`
139
- starts a brand new process reviewing `<commitish>..HEAD` with no cycle of its
140
- own, e.g. a colleague's PR branch. Its squash keeps and describes only the fixes
141
- made _during_ the review (not the reviewed changeset); a clean sign-off with no
142
- fixes becomes an empty `chore: human review` commit. A fourth entry, `gtd fix`,
143
- starts from a clean `idle` and goes straight into repairing a red baseline —
144
- repair, review, and squash into one commit. If the suite is already green there
145
- is nothing to fix, and the log is left untouched — no commit is left behind.
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
142
+ unticked and no comment is refused (finish reviewing first), as is deleting
143
+ `.gtd/REVIEW.md`.
144
+
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.
146
168
 
147
169
  Every agent state routes its model through two `vars` tiers — `plannerModel`
148
170
  (heavier planning and review) and `coderModel` (the coding turns) — so you can
@@ -183,16 +205,16 @@ Commands:
183
205
  (or squash) the one resulting transition. Pass
184
206
  --cost=<n> (optionally --model=<name>) to record the
185
207
  just-finished invocation's token cost and model on the
186
- turn commit (summed into it.processCost/processCostByModel)
187
- review <commitish>
188
- Start a NEW review process at the workflow's declared
189
- review-entry state (reviewEntry: true), reviewing
190
- <commitish>..HEAD — e.g. a colleague's PR branch. Requires
191
- a clean tree resting at the workflow's initial state
192
- fix Start a NEW process at the workflow's declared fix-entry
193
- state (fixEntry: true) that goes straight into repairing the
194
- current failing tests. Requires a clean tree resting at the
195
- 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'
196
218
  abandon End the process currently underway without completing it:
197
219
  close any open review checkout window, then rewind HEAD to
198
220
  the commit the process started from, keeping everything it
@@ -224,6 +246,15 @@ Options:
224
246
  --no-open (gtd visualize only) do not open the browser
225
247
  --cost=<n> (gtd step only) record the invocation's token cost
226
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:
227
258
  --once (bare gtd or gtd loop only) run exactly one loop beat (one
228
259
  human-gate capture, one script check+step, or one agent
229
260
  prompt+step), then exit
@@ -241,13 +272,18 @@ touching the repository. Every other (recognized) command must be run from the
241
272
  history relative to cwd, so it refuses with a clear error if invoked from a
242
273
  subdirectory.
243
274
 
244
- `--json`, `--cost=<n>`, and `--model=<name>` (the latter two only for
245
- `gtd step`) are the only long options the compiled bundle recognizes. Any other
246
- `--` option (including a typo like `--jsn`) is rejected with a usage error
247
- rather than silently ignored, so a mistyped flag can never degrade a JSON caller
248
- to plain-text mode. A bare `--cost`/`--model` with no value, a non-numeric or
249
- negative `--cost`, an empty `--model`, `--model` without `--cost`, or either
250
- 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.
251
287
 
252
288
  `--once` is a separate, bash-level flag handled entirely by the `bin/gtd` driver
253
289
  itself, stripped before anything reaches the bundle.
@@ -313,12 +349,45 @@ name surfaced in `gtd next --json`/`gtd status`. The driver uses it for its
313
349
  per-beat progress lines; an outer wrapper (a terminal multiplexer, a notifier)
314
350
  can use it the same way.
315
351
 
316
- A state can also declare an optional `memory:` scope label: consecutive agent
317
- turns sharing the same label continue one agent session (remembered in the git
318
- dir, so it survives restarts across the same worktree), while a changed or
319
- absent label starts a fresh one. If the remembered session no longer exists
320
- (retention expired, `~/.claude/projects` wiped, a machine change), the driver
321
- 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.
322
391
 
323
392
  ### Terminal-multiplexer status: a herdr wrapper
324
393
 
@@ -460,38 +529,70 @@ workflow:
460
529
  <name>:
461
530
  format: <shell command> # at least one of format/validate
462
531
  validate: <shell command>
463
- states:
532
+ entry:
533
+ default: <machine name> # which machine is the ROOT instance
534
+ machines:
464
535
  <name>:
465
- actor: <string> # forbidden on a commit state, required otherwise
466
- script: <string> # exactly one of script/prompt/message/commit
467
- prompt: <string>
468
- message: <string>
469
- commit: <string>
470
- on: # a mapping, DECLARATION ORDER PRESERVED
471
- "<pattern>": <targetState> # short form
472
- "<pattern>": {
473
- to: <targetState>,
474
- describe: <sentence>,
475
- action: <label>,
476
- } # description/action
477
- initial: true # exactly one state across the whole workflow
478
- retry:
479
- max: <number>
480
- otherwise: <targetState>
481
- model: <string> # optional, opaque harness hint — forbidden on a commit state
482
- memory: <string> # optional, opaque memory-scope label — forbidden on a commit state
483
- file: <string> # optional, an Eta template naming the state's steering file
484
- 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>`
485
563
  ```
486
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
+
487
588
  Besides `it.vars` (below), a `script`/`prompt`/`message`/`commit` template sees:
488
589
 
489
590
  - **`it.startCommit`** — the process's diff base (the commit the current cycle
490
- started from, or a `gtd review <commitish>` entry's resolved base).
591
+ started from, or the base a `--var reviewBase=<commitish>` entry resolved to).
491
592
  - **`it.reviewBase`** — the previous review round's boundary, falling back to
492
593
  `it.startCommit` on a first review.
493
594
  - **`it.retainedBase`** — the process's trace/retry boundary, what a squash
494
- actually keeps (never moved by a `gtd review` entry).
595
+ actually keeps (never moved by a review entry's fixed base).
495
596
  - **`it.currentCommit`** / **`it.previousCommit`** — HEAD's hash and its parent,
496
597
  at render time.
497
598
 
@@ -504,26 +605,103 @@ Authoring or editing a workflow with a coding agent? `skills/authoring/SKILL.md`
504
605
  is the agent-facing contract for producing a valid `workflow:` — the state
505
606
  model, pattern grammar, load-time rules, and how to verify a change compiles.
506
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
+
507
677
  ### Variables
508
678
 
509
- Every template — `script`/`prompt`/`message`/`commit`, and
510
- `model`/`memory`/`file` — sees `it.vars`: a flat `Record<string, string>`
511
- 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**:
512
682
 
513
- 1. **The workflow's own `vars:` key** (sibling to `states:`) — the workflow
514
- author's declared defaults. The unified template declares
515
- `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
516
686
  `<%~ it.vars.testCommand %>`.
517
687
  2. **A top-level `.gtdrc` `vars:` key** (a sibling of `workflow:`, NOT nested
518
688
  inside it) — per-repo tuning without redefining the whole workflow.
519
- 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,
520
697
  checked at every invocation, case-insensitively against each name already
521
- declared by layer 1 or 2: `GTD_TESTCOMMAND` overrides `testCommand`. The
522
- 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
523
700
  `GTD_*` var matching no declared name is silently ignored.
524
701
 
525
702
  Values in layers 1–2 must be YAML scalars (string/number/boolean), coerced to
526
- 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.
527
705
 
528
706
  ```yaml
529
707
  # .gtdrc — overriding the unified template's testCommand
@@ -566,9 +744,9 @@ Those findings include the **semantic graph checks**: every `on` target and
566
744
 
567
745
  Many of these problems never reach gtd at all if your editor validates against
568
746
  the [published schema](#schema), which fully types the `workflow:` key. The
569
- rules JSON Schema cannot express — exactly one content kind, exactly one
570
- `initial: true`, targets naming defined states, reachability — remain the
571
- 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.
572
750
 
573
751
  ## Repository requirements
574
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