@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 +270 -87
- package/bin/gtd +79 -28
- package/dist/gtd.bundle.mjs +1597 -895
- package/package.json +1 -1
- package/schema.json +147 -265
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
|
|
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:
|
|
109
|
-
product then technical Q&A (`.gtd/REQUIREMENTS.md` →
|
|
110
|
-
each open question offers a couple
|
|
111
|
-
`- [ ] _your answer_` slot, and you tick exactly
|
|
112
|
-
won't let a phase advance while any question is
|
|
113
|
-
decomposition into work **packages** (each a set of
|
|
114
|
-
build turn fans out to parallel subagents), a
|
|
115
|
-
per-package **agentic review** that verifies the
|
|
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.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 —
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
review-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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>`,
|
|
240
|
-
`gtd step`
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
flag
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
532
|
+
entry:
|
|
533
|
+
default: <machine name> # which machine is the ROOT instance
|
|
534
|
+
machines:
|
|
459
535
|
<name>:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
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
|
|
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`,
|
|
505
|
-
`
|
|
506
|
-
assembled from
|
|
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 `
|
|
509
|
-
author's declared defaults. The unified template declares
|
|
510
|
-
`vars: { testCommand: "npm test" }`, read by `
|
|
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.
|
|
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
|
|
517
|
-
environment can only OVERRIDE a name
|
|
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,
|
|
565
|
-
|
|
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
|
|
376
|
-
#
|
|
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
|
|
422
|
-
# longer exists on disk. Rather than failing the turn
|
|
423
|
-
# it, since the dead id would just be resumed again),
|
|
424
|
-
#
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
#
|
|
428
|
-
#
|
|
429
|
-
#
|
|
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
|
-
|
|
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:
|
|
578
|
-
#
|
|
579
|
-
#
|
|
580
|
-
#
|
|
581
|
-
#
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
644
|
+
if found_session_id="$(lookup_scope_session "$memory")"; then
|
|
592
645
|
resume=1
|
|
593
|
-
session_id="$
|
|
646
|
+
session_id="$found_session_id"
|
|
594
647
|
else
|
|
595
648
|
session_id="$(new_session_id)"
|
|
596
649
|
fi
|
|
597
|
-
|
|
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
|