@pmelab/gtd 2.8.0 → 3.1.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 +61 -40
- package/bin/gtd-loop +43 -23
- package/dist/gtd.bundle.mjs +2511 -3481
- package/docs/cli.md +222 -281
- package/docs/configuration.md +350 -71
- package/docs/design/configurable-state-machine.md +1439 -0
- package/docs/design/delta-purity-plan.md +283 -0
- package/docs/design/pattern-machine-plan.md +164 -0
- package/docs/design/scripted-actor.md +289 -0
- package/docs/design/state-file-association.md +112 -0
- package/docs/design/state-machine-validation.md +437 -0
- package/docs/design/steering-file-loops.md +164 -0
- package/docs/design/work-packages.md +194 -0
- package/docs/development.md +55 -36
- package/docs/examples/advanced-workflow.md +428 -0
- package/docs/loop.md +60 -60
- package/docs/upgrading.md +125 -0
- package/package.json +2 -2
- package/schema.json +89 -65
- package/docs/upgrading-from-v1.md +0 -45
- package/docs/workflow.md +0 -428
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Implementation plan: full δ(label, diff)
|
|
2
|
+
|
|
3
|
+
> **Historical design record.** The counters/labels machinery this plan built on
|
|
4
|
+
> (turn labels, capture rules, `Gtd-Counters` trailers) was deleted by the v3
|
|
5
|
+
> rewrite; superseded by `docs/design/pattern-machine-plan.md` and
|
|
6
|
+
> [STATES.md](../../STATES.md).
|
|
7
|
+
|
|
8
|
+
> **Status (updated during implementation):**
|
|
9
|
+
>
|
|
10
|
+
> - **Phase 0+A — LANDED.** Phase 0's table review surfaced a major
|
|
11
|
+
> simplification the plan's draft table over-engineered: the actor is already
|
|
12
|
+
> part of every turn label (`gtd(human): X` ≠ `gtd(agent): X`), so only labels
|
|
13
|
+
> whose OWN diff disambiguated their meaning needed splitting — six new labels
|
|
14
|
+
> (`grilling-accepted`, `architecting-accepted`, `review-approved`,
|
|
15
|
+
> `review-feedback`, `agentic-approved`, `agentic-findings`) instead of the
|
|
16
|
+
> drafted ~30-state redraw. Turn capture is rule-driven (`captureRules` per
|
|
17
|
+
> state), inert-by-default with opt-in `empty` rules, the fixpoint is a label
|
|
18
|
+
> fact, `headTurnIsEmpty` / `headTurnReviewSubstantive` are deleted, and the
|
|
19
|
+
> FEEDBACK.md interrupt dissolved into the verdict labels (a fallback rung
|
|
20
|
+
> keeps rider/crash recovery).
|
|
21
|
+
> - **Phase B — LANDED.** Template probes are pending-diff matchers
|
|
22
|
+
> (`squashMsgDirty` / `learningMsgDirty`); content inspection is down to
|
|
23
|
+
> FEEDBACK.md emptiness and REVIEW.md checkbox-only, both consumed at capture.
|
|
24
|
+
> The effects-as-diffs invariant holds structurally (every `perform()` arm
|
|
25
|
+
> ends in a commit or a stop).
|
|
26
|
+
> - **Phase C1 — LANDED.** `gtd: escalated`: a red check at the cap writes
|
|
27
|
+
> ERRORS.md and labels the escalation at write time (runTest AND
|
|
28
|
+
> runHealthCheck). `gtd: test-failed`/`gtd: health-check` classify
|
|
29
|
+
> unconditionally, the ERRORS interrupt dissolved (fallback recovery rung
|
|
30
|
+
> kept), `ClassifyFlags.errorsPresent` and the at-cap health carve-out are
|
|
31
|
+
> deleted.
|
|
32
|
+
> - **Phase C2 — LANDED.** `runTest.onGreen` decided at dispatch: packages +
|
|
33
|
+
> threshold → INLINE close (one `gtd: close-package` commit, no marker);
|
|
34
|
+
> packages → new machine label `gtd: agentic-review` (rest for the verdict);
|
|
35
|
+
> no packages → `gtd: tests-green`, now health-path-only with a settle-only
|
|
36
|
+
> routing rule. The step-agent checkpoint guard keys on the agentic-review
|
|
37
|
+
> label; the recovery-ladder `runTest` gets the same fill.
|
|
38
|
+
> - **Phase C3 — LANDED.** Counter trailers: every machine-written commit (turns
|
|
39
|
+
> AND labels) carries its vector as a `Gtd-Counters: t=n r=n h=n` body
|
|
40
|
+
> trailer, computed by the writer — label stamps live in `labelCounterStamps`
|
|
41
|
+
> (`test-failed` t+1, `health-check` h+1, `building`/`close-package` t=r=0,
|
|
42
|
+
> `tests-green` h=0), turn stamps in `CaptureRule.stamp` (verdict turns r+1,
|
|
43
|
+
> the escalate turn's ERRORS-deletion t=h=0). The machine attaches vectors at
|
|
44
|
+
> dispatch (`attachCounters` + `fillInActionBudgets`); write-time-decided
|
|
45
|
+
> labels (runTest/runHealthCheck outcomes, the inline close) get the previous
|
|
46
|
+
> vector and the edge applies the chosen label's stamp. `gatherEvents` reads
|
|
47
|
+
> ONE trailer (nearest workflow commit) into `payload.counters`;
|
|
48
|
+
> `foldCounters` and all `CommitEvent` counter flags are deleted; squash
|
|
49
|
+
> commits carry no trailer. Zero vector is the no-trailer default (the upgrade
|
|
50
|
+
> rule). E2e grew `Given a commit "…" with counters "t=… r=… h=…"` and the
|
|
51
|
+
> cap/threshold scenarios spell their vectors explicitly; two scenarios pin
|
|
52
|
+
> the trailer wire format on machine-written commits.
|
|
53
|
+
> - **Phase D — LANDED.** (1) The δ conformance property test
|
|
54
|
+
> (`src/Machine.property.test.ts`): with the history fold gone, `resolve` /
|
|
55
|
+
> `predictTurn` output is a function of the nearest workflow commit plus the
|
|
56
|
+
> RESOLVE payload — the test replaces older history, injects boundary commits
|
|
57
|
+
> before and after the nearest workflow commit, and asserts identical output
|
|
58
|
+
> (including thrown corruption errors). (2) Hardening as
|
|
59
|
+
> definition-consistency unit tests (`src/Workflow.test.ts`): declared-actor
|
|
60
|
+
> references in states, prompts, capture rules, turn/routing/ladder outcomes;
|
|
61
|
+
> stamp non-negativity. (No new compiler machinery — the contract types
|
|
62
|
+
> already make gates/phases/outcome sets closed at compile time.) (3) Docs:
|
|
63
|
+
> STATES.md §1 states the δ contract, §2 rewritten as the current transition
|
|
64
|
+
> tables (branch labels, write-time check outcomes, `gtd: escalated` /
|
|
65
|
+
> `gtd: agentic-review` rows, the `Gtd-Counters` trailer, the v1
|
|
66
|
+
> `grilling`/`building` collision note); AGENTS.md and the upgrading doc
|
|
67
|
+
> describe the trailer scheme and its zero-vector upgrade rule.
|
|
68
|
+
|
|
69
|
+
> Companion to `configurable-state-machine.md` Appendix C. Decisions taken:
|
|
70
|
+
> **full purity** (all five moves, including counters and config at write time),
|
|
71
|
+
> **keep the corruption refusal** (steering files + no label → hard error),
|
|
72
|
+
> **split states are the public vocabulary** (labels, `gtd status`, `--json`,
|
|
73
|
+
> docs), **phased rollout** with every phase landing green.
|
|
74
|
+
>
|
|
75
|
+
> Target invariant, stated once: at every resolution,
|
|
76
|
+
> `next = δ(nearest label (incl. its trailers), pending diff)` — the invoker
|
|
77
|
+
> authenticates but adds no information; counters, config outcomes, and branch
|
|
78
|
+
> decisions are baked into labels at write time; the external world enters only
|
|
79
|
+
> through diffs; exactly two sanctioned impurities remain (the boundary-skip
|
|
80
|
+
> walk and the corruption refusal).
|
|
81
|
+
|
|
82
|
+
## 1. The target machine (draft label/state table)
|
|
83
|
+
|
|
84
|
+
States have exactly ONE awaited actor (rest) or ONE machine act —
|
|
85
|
+
`awaits: "dynamic"` is abolished. Labels are the transitions; `stateOf(label)`
|
|
86
|
+
is a total map. Names are draft — Phase 0 finalizes them.
|
|
87
|
+
|
|
88
|
+
### Entry (from `start` = no labeled commit reachable)
|
|
89
|
+
|
|
90
|
+
| From state | Diff matcher (invoker: human) | Label written | Enters state |
|
|
91
|
+
| ---------- | ----------------------------- | -------------------------------- | -------------------- |
|
|
92
|
+
| start | adds `.gtd/HEALTH.md` | `gtd(human): health-entry` | health-briefed |
|
|
93
|
+
| start | adds `.gtd/PLAN.md` | `gtd(human): plan-entry` | seed-from-plan (act) |
|
|
94
|
+
| start | adds `.gtd/ARCHITECTURE.md` | `gtd(human): architecting-entry` | architecting-draft |
|
|
95
|
+
| start | anything else (dirty) | `gtd(human): grilling-entry` | grilling-draft |
|
|
96
|
+
|
|
97
|
+
`start` with steering files present but NO reachable label → **corruption
|
|
98
|
+
refusal** (the one retained guess-refusal; supersedes today's illegal-
|
|
99
|
+
combination table for the no-label case).
|
|
100
|
+
|
|
101
|
+
### Grilling / architecting (the dynamic-gate split)
|
|
102
|
+
|
|
103
|
+
| From state (awaits) | Diff matcher | Label | Enters |
|
|
104
|
+
| ----------------------- | --------------------------- | ------------------------------- | -------------------- |
|
|
105
|
+
| grilling-draft (agent) | touches TODO.md (validated) | `gtd(agent): grilling-draft` | grilling-answer |
|
|
106
|
+
| grilling-answer (human) | non-empty | `gtd(human): grilling-answered` | grilling-draft |
|
|
107
|
+
| grilling-answer (human) | **empty** | `gtd(human): grilling-accepted` | seed-from-todo (act) |
|
|
108
|
+
| seed-from-todo (act) | writes ARCH, deletes TODO | `gtd: architecting` | architecting-draft |
|
|
109
|
+
|
|
110
|
+
Architecting mirrors it (`architecting-draft/-answered/-accepted`, accepted →
|
|
111
|
+
`gtd: grilled` → decompose). The empty-turn _meaning_ is now a declared
|
|
112
|
+
diff-matcher rule (`empty`), not a classification of HEAD's past diff —
|
|
113
|
+
`headTurnIsEmpty` and `headTurnReviewSubstantive` are deleted from
|
|
114
|
+
`ResolvePayload`.
|
|
115
|
+
|
|
116
|
+
### Build / test / fix loop (counters ride labels — see §2)
|
|
117
|
+
|
|
118
|
+
| From state | Trigger | Label | Enters |
|
|
119
|
+
| ------------------ | ------------------------------ | ------------------------------- | ------------------ |
|
|
120
|
+
| decompose (agent) | adds package dirs | `gtd(agent): decomposed` | consume-arch (act) |
|
|
121
|
+
| consume-arch (act) | removes ARCH | `gtd: building` | building |
|
|
122
|
+
| building (agent) | code diff | `gtd(agent): building` | testing (act) |
|
|
123
|
+
| testing (act) | red below cap, writes FEEDBACK | `gtd: test-failed` + trailers | fixing |
|
|
124
|
+
| testing (act) | red at cap, writes ERRORS | `gtd: escalated` | escalate |
|
|
125
|
+
| testing (act) | green, pkgs, threshold reached | `gtd: close-package` | close-pkg (act) |
|
|
126
|
+
| testing (act) | green, pkgs | `gtd: agentic-review` | agentic-review |
|
|
127
|
+
| testing (act) | green, no pkgs (health rerun) | settle label (§ settle) | — |
|
|
128
|
+
| fixing (agent) | any diff | `gtd(agent): fixing` + trailers | testing (act) |
|
|
129
|
+
| escalate (human) | deletes ERRORS.md | `gtd(human): escalate-resumed` | testing (act) |
|
|
130
|
+
|
|
131
|
+
`gtd: tests-green` and `gtd: test-failed`'s read-time branches are gone: **the
|
|
132
|
+
check decides and labels** (write-time config + counters). The force-approve
|
|
133
|
+
guard moves from `routingRules["tests-green"]` into the `runTest` act.
|
|
134
|
+
|
|
135
|
+
### Agentic review (emptiness read from the DIFF)
|
|
136
|
+
|
|
137
|
+
| From state | Diff matcher | Label | Enters |
|
|
138
|
+
| ---------------------- | ------------------------------ | ------------------------------ | --------------- |
|
|
139
|
+
| agentic-review (agent) | adds FEEDBACK, zero content | `gtd(agent): agentic-approved` | close-pkg (act) |
|
|
140
|
+
| agentic-review (agent) | adds FEEDBACK, non-empty | `gtd(agent): agentic-findings` | fixing |
|
|
141
|
+
| close-pkg (act) | rm pkg+FEEDBACK; pkgs remain | `gtd: building` | building |
|
|
142
|
+
| close-pkg (act) | none remain, reviewable | `gtd: review-record` | review-record |
|
|
143
|
+
| close-pkg (act) | none remain, nothing to review | settle label | — |
|
|
144
|
+
|
|
145
|
+
The delete-dispute (`pendingFeedbackDeletion`) becomes a matcher row on the
|
|
146
|
+
fixing state (deletes/empties FEEDBACK → still a fixing turn), not a payload
|
|
147
|
+
special case.
|
|
148
|
+
|
|
149
|
+
### Human review, learning, squash, health, idle
|
|
150
|
+
|
|
151
|
+
| From state | Trigger | Label | Enters |
|
|
152
|
+
| ---------------------- | ------------------------------------ | ------------------------------------------------------------------------- | --------------------- |
|
|
153
|
+
| review-record (agent) | adds REVIEW.md (validated) | `gtd(agent): review-record` | await-review (window) |
|
|
154
|
+
| await-review (human) | empty / checkbox-only / deletes R | `gtd(human): review-approved` | cycle-close (act) |
|
|
155
|
+
| await-review (human) | anything else | `gtd(human): review-feedback` | regrill (act) |
|
|
156
|
+
| cycle-close (act) | rm REVIEW | `gtd: done` | settle |
|
|
157
|
+
| regrill (act) | rm REVIEW | `gtd: grilling` | grilling-draft |
|
|
158
|
+
| settle (act) | learning on + base | `gtd: learning` (writes template) | learning-draft |
|
|
159
|
+
| settle (act) | squash on + base | `gtd: squashing` (writes template) | squash-draft |
|
|
160
|
+
| settle (act) | neither | `gtd: idle` | idle |
|
|
161
|
+
| learning-draft (agent) | **touches LEARNINGS.md** | `gtd(agent): learning-draft` | learning-review |
|
|
162
|
+
| squash-draft (agent) | **touches SQUASH_MSG.md** | `gtd(agent): squash-draft` | do-squash (act) |
|
|
163
|
+
| idle (human) | step (clean) → act runs health check | red: `gtd: health-fixing` / cap: `gtd: escalated` / green: stop or settle | … |
|
|
164
|
+
| health-briefed (agent) | any NON-empty diff | `gtd(agent): health-fixed` | testing (act) |
|
|
165
|
+
| health-fixing (agent) | any diff **or empty** | `gtd(agent): health-fixed` | testing (act) |
|
|
166
|
+
|
|
167
|
+
Two dissolutions worth naming: the template-unmodified probe becomes "diff
|
|
168
|
+
touches the template file" (a matcher), and health-fixing's
|
|
169
|
+
`inertWhen: humanEntryHead` guard becomes two states — `health-briefed` (entered
|
|
170
|
+
via the human's entry; no empty-rule, so an empty agent step is inert by
|
|
171
|
+
_absence of a rule_) vs `health-fixing` (entered via a red check; empty-rule
|
|
172
|
+
present = environmental-fix signal).
|
|
173
|
+
|
|
174
|
+
## 2. Counters: commit-message trailers, not subject suffixes
|
|
175
|
+
|
|
176
|
+
Counters must be derivable from the nearest label alone, but a loop's counter
|
|
177
|
+
lives across several states (test-failed → fixing → testing), and
|
|
178
|
+
`reviewFixCount` spans the whole package loop. Encoding decision:
|
|
179
|
+
|
|
180
|
+
- Subjects stay clean (`gtd(agent): fixing`).
|
|
181
|
+
- Every label written _inside a counted loop_ carries a structured trailer:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
gtd(agent): fixing
|
|
185
|
+
|
|
186
|
+
Gtd-Counters: t=2/3 r=1/3
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- The writer computes each value from the previous label's trailer (+increment /
|
|
190
|
+
reset per the definition's counter rules); a label with no trailer means
|
|
191
|
+
all-zero (loop entry). `δ` reads label = (subject, trailers).
|
|
192
|
+
- `foldCounters` is deleted; caps are checked at act time from the trailer.
|
|
193
|
+
|
|
194
|
+
Why trailers: subjects remain human-scannable and e2e-assertable; trailers are
|
|
195
|
+
already precedent (`Gtd-Decisions`); `git log --format=%B` is already fetched
|
|
196
|
+
(decisionLog reuses it — no new subprocess); and squash collapses them away at
|
|
197
|
+
cycle end. Wire-format cost acknowledged: changing a budget's _semantics_
|
|
198
|
+
becomes a history-compat concern (mitigated: values are `n/cap` so the cap
|
|
199
|
+
travels with the count; a config cap change affects only future writes).
|
|
200
|
+
|
|
201
|
+
## 3. What gets deleted (the payoff)
|
|
202
|
+
|
|
203
|
+
| Deleted | Replaced by |
|
|
204
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
|
|
205
|
+
| interrupt ladder + fallback ladder | `stateOf(label)` + per-state matcher rules; ONE retained refusal |
|
|
206
|
+
| `classifyHead` turn/routing branch rules | total `stateOf` map (labels are unambiguous) |
|
|
207
|
+
| `foldCounters` + `CommitEvent` flags (`isErrors`, `isFeedback`, …) | counter trailers |
|
|
208
|
+
| `headTurnIsEmpty`, `headTurnReviewSubstantive`, `pendingFeedbackDeletion`, `squashMsgIsTemplate`, `learningMsgIsTemplate`, `feedbackEmpty` (as guards) | diff matchers at capture time |
|
|
209
|
+
| `emptyAgentTurn` policies (`inert`/`inertWhen`) | inert-by-default: no matching rule = no-op (empty-rules are opt-in) |
|
|
210
|
+
| illegal-combination table (mostly) | can't arise: every write commits with its label; kept as capture-time lint |
|
|
211
|
+
| `agenticReviewForceApproved` read-time guard, `settle` read-time branches | write-time decisions in `runTest`/`closePackage`/`settle` acts |
|
|
212
|
+
| `awaits: "dynamic"`, `GATE_OVERRIDES`-style sharing | one actor per state |
|
|
213
|
+
|
|
214
|
+
`ResolvePayload` shrinks to: invoker, pending-diff facts (per-artifact
|
|
215
|
+
touched/added/deleted/empty-add/checkbox-only), steering-file presence (for the
|
|
216
|
+
corruption refusal + prompts), config (consumed by ACTS only), and prompt
|
|
217
|
+
passthrough (diffs/content for templates — prompts remain impure by design, per
|
|
218
|
+
Appendix C §C.4.2).
|
|
219
|
+
|
|
220
|
+
## 4. Phases
|
|
221
|
+
|
|
222
|
+
Each phase: full suite green (unit + all 263 e2e scenarios' behavior preserved,
|
|
223
|
+
labels/state names mapped), `feat!:` commit, docs synced.
|
|
224
|
+
|
|
225
|
+
**Phase 0 — Spec (no code).** Finalize the label/state table above as the new
|
|
226
|
+
STATES.md §1 (names, matchers, trailer grammar, `stateOf`). Acceptance: a table
|
|
227
|
+
review — every current e2e scenario hand-traced onto new labels with no
|
|
228
|
+
ambiguity. This is where naming bikesheds happen, deliberately.
|
|
229
|
+
|
|
230
|
+
**Phase A — State splitting + capture-time turn labels.** (`~21 → ~30` states;
|
|
231
|
+
the `dynamic` gates, review approval/feedback, health-briefed split, entry
|
|
232
|
+
labels.) Turn capture evaluates diff matchers and writes the decided label;
|
|
233
|
+
`stateOf` replaces turn-rule branching; `headTurn*` payload fields deleted.
|
|
234
|
+
Machine act labels unchanged in this phase. Biggest e2e churn (every
|
|
235
|
+
`gtd(actor): <gate>` assertion), but mechanical per scenario since the exercised
|
|
236
|
+
branch is evident from surrounding steps.
|
|
237
|
+
|
|
238
|
+
**Phase B — Probes → diff matchers + effects-as-diffs invariant.** Template
|
|
239
|
+
probes become touch-matchers; feedback emptiness read from the capture diff;
|
|
240
|
+
delete-dispute becomes a matcher row. Add the engine invariant (dev/test
|
|
241
|
+
assert): every `perform()` must commit a label and/or change files, or return
|
|
242
|
+
stop. Mostly internal; small e2e deltas around crash-recovery scenarios (retrace
|
|
243
|
+
them explicitly — this is where content-vs-diff divergence lives).
|
|
244
|
+
|
|
245
|
+
**Phase C — Write-time outcomes + counter trailers.** `tests-green`/
|
|
246
|
+
`test-failed` read-time branches move into the acts (labels
|
|
247
|
+
`agentic-review`/`close-package`/`escalated`/`test-failed`+trailer);
|
|
248
|
+
close-package decides building/review/settle at write time; settle decided at
|
|
249
|
+
`done`/green with config in hand; `foldCounters`, commit-event flags, and both
|
|
250
|
+
ladders deleted; corruption refusal re-anchored on "steering files present, no
|
|
251
|
+
label reachable". Second wire-format break; e2e label updates are
|
|
252
|
+
context-dependent this time (a green assertion becomes whichever outcome label
|
|
253
|
+
the scenario's context produced) — budget one careful pass.
|
|
254
|
+
|
|
255
|
+
**Phase D — Conformance + hardening.** (1) The δ property test: generate
|
|
256
|
+
histories, then mutate anything _other than_ the nearest label+trailers and the
|
|
257
|
+
pending diff (permute older commits, inject boundary commits, vary config where
|
|
258
|
+
an act already decided) and assert `resolve` output is byte-identical. This test
|
|
259
|
+
IS the purity claim. (2) Compiler checks: `stateOf` total, one actor per state,
|
|
260
|
+
every act's outcome set covered, trailer continuity (a counted loop's labels all
|
|
261
|
+
carry the vector). (3) STATES.md rewritten as the transition table; upgrading
|
|
262
|
+
doc gains the third grammar migration note (old labels → boundary → `start`,
|
|
263
|
+
refusal if steering files linger).
|
|
264
|
+
|
|
265
|
+
## 5. Risks and open items
|
|
266
|
+
|
|
267
|
+
- **Kill-switch semantics change (accepted):** config flips no longer affect a
|
|
268
|
+
rest already labeled; document loudly in configuration.md.
|
|
269
|
+
- **Idle green stop:** a green idle health check commits nothing (today's
|
|
270
|
+
behavior) — fine for δ (no transition happened), but it means `start`/ `idle`
|
|
271
|
+
must be re-derivable cheaply every run; unchanged from today.
|
|
272
|
+
- **Review checkout window:** unchanged (program-edge; keyed on the await-review
|
|
273
|
+
state's label).
|
|
274
|
+
- **Trailer parsing:** extend `parseSubject` → `parseLabel(subject, body)`; Git
|
|
275
|
+
already returns full messages on the history read — verify no extra subprocess
|
|
276
|
+
creeps in (AGENTS.md decisionLog rule).
|
|
277
|
+
- **Squash-base / review-base scans:** remain history reads inside acts and
|
|
278
|
+
prompts (sanctioned impurity); unchanged.
|
|
279
|
+
- **Naming risk:** the table above is draft; Phase 0 exists so name changes cost
|
|
280
|
+
a review, not a re-implementation.
|
|
281
|
+
- **Rough sizing:** Phase 0 small; A the largest (engine + ~35 feature files); B
|
|
282
|
+
small-medium; C medium with careful e2e mapping; D medium (property test is
|
|
283
|
+
the substance).
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Plan: the pattern machine (gtd v3)
|
|
2
|
+
|
|
3
|
+
> **Status: implemented** (commits `b89e1ae..HEAD`). Two amendments made during
|
|
4
|
+
> implementation, not reflected in the plan text below: (1) the commit subject's
|
|
5
|
+
> `<actor>` half was fixed to record WHO STEPPED (the invoker), not the resolved
|
|
6
|
+
> state's own declared actor — see `PatternMachine.ts`'s `resolveState` doc
|
|
7
|
+
> comment and commit `09cd9df`; (2) the bundled default workflow's content is
|
|
8
|
+
> fully INLINE, with no `./`-relative file references — the single-file
|
|
9
|
+
> `dist/gtd.bundle.mjs` build can't ship a sibling `prompts/` directory to
|
|
10
|
+
> resolve them against at runtime, unlike a `.gtdrc`-configured workflow, which
|
|
11
|
+
> resolves file refs relative to its own config file's location on disk.
|
|
12
|
+
|
|
13
|
+
> Interview outcome (all decided with the user, 2026-07-22):
|
|
14
|
+
>
|
|
15
|
+
> 1. **Ground-up v3.** The current definition model (gates, guard functions,
|
|
16
|
+
> actor kinds, interrupt/fallback ladders, counters-as-trailers, conflicts)
|
|
17
|
+
> is DELETED. The engine shrinks to: states, change-pattern transitions, and
|
|
18
|
+
> one content property per state. The shipped default workflow is re-authored
|
|
19
|
+
> in the new vocabulary as a bundled config; the e2e suite is rewritten
|
|
20
|
+
> around it; behavior changes where the old shape doesn't map.
|
|
21
|
+
> 2. **Commit format:** `gtd(<actor>): <state>` — every workflow commit records
|
|
22
|
+
> the state being ENTERED and who authored the step. History is an attributed
|
|
23
|
+
> state trace; resolution = read HEAD's state name.
|
|
24
|
+
> 3. **Scripts: the driver executes.** gtd only ever emits the rendered script
|
|
25
|
+
> (`gtd next`); the outer loop or the built-in `gtd run` convenience executes
|
|
26
|
+
> it and steps. The machine stays execution-free.
|
|
27
|
+
> 4. **Budgets:** one engine affordance only — a per-state retry limit that
|
|
28
|
+
> redirects the transition to a named state when the limit is hit. Everything
|
|
29
|
+
> else (verdict encoding, force-approve, content signals) moves out of the
|
|
30
|
+
> engine into which files scripts/agents write or delete. `Gtd-Counters`
|
|
31
|
+
> trailers die.
|
|
32
|
+
> 5. **Patterns:** contains-match over the pending diff, declaration order,
|
|
33
|
+
> first match wins. Statuses `A` (added), `M` (modified), `D` (deleted), `*`
|
|
34
|
+
> (any change kind), plus the special `C` (clean tree). Glob paths (`*`
|
|
35
|
+
> within a segment, `**` across segments).
|
|
36
|
+
> 6. **No-match dirty step:** refusal — exit non-zero, name the state's declared
|
|
37
|
+
> patterns, commit nothing.
|
|
38
|
+
> 7. **Squash = the `commit:` content kind** (revised 2026-07-22, replacing the
|
|
39
|
+
> earlier `final: true` flag). A state whose content property is `commit:` is
|
|
40
|
+
> the final state — no separate flag, no actor. Entering it performs, in one
|
|
41
|
+
> execution: (a) render the `commit:` Eta template against the PENDING
|
|
42
|
+
> working tree (`it.read(path)` reads files as they sit — the message file is
|
|
43
|
+
> never committed first; a failed render refuses the step and touches
|
|
44
|
+
> nothing), (b) soft-reset to the process's start parent and write ONE commit
|
|
45
|
+
> with the rendered message, (c) discard all remaining uncommitted changes,
|
|
46
|
+
> message file included. Squashing is optional (no `commit:` state = no
|
|
47
|
+
> squash) and nothing about it is hardcoded: the message filename appears
|
|
48
|
+
> only in user-authored patterns/templates, and the standard shape is an
|
|
49
|
+
> ordinary prompt state (`squashing`) whose agent authors the message file,
|
|
50
|
+
> with `"A COMMIT_MSG.md": done` routing to the commit state.
|
|
51
|
+
> 8. **Template variables** for script/prompt/message/commit (all Eta; inline or
|
|
52
|
+
> a path relative to the config file, auto-inlined): `startCommit`,
|
|
53
|
+
> `currentCommit`, `previousCommit`, `state`, `actor`, `processDiff`
|
|
54
|
+
> (startCommit..HEAD + pending), `lastDiff` (the last transition's diff),
|
|
55
|
+
> `read(path)` (working-tree file read, available in every content kind), and
|
|
56
|
+
> `config` passthrough. No blessed config keys: there is no `testCommand` — a
|
|
57
|
+
> check's command lives inline in its `script:` (or the referenced file).
|
|
58
|
+
|
|
59
|
+
## 1. The model
|
|
60
|
+
|
|
61
|
+
A workflow is a set of named **states**. Each state declares:
|
|
62
|
+
|
|
63
|
+
| Property | Meaning |
|
|
64
|
+
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| `actor` | A plain string: who is expected to act here. No kinds — every actor just creates modifications in the tree. `gtd step <actor>` authenticates against it. Commit states have NO actor — gtd itself performs them. |
|
|
66
|
+
| `script` \| `prompt` \| `message` \| `commit` | Exactly one — the content kind. `script` — an executable the DRIVER runs (`gtd next` emits it rendered; `gtd run` executes + steps). `prompt` — instructions for an agent. `message` — text for a human (drivers halt here). `commit` — the squash-commit message template; having it makes the state FINAL (see decision 7). Inline string or a file path relative to the config file (auto-inlined). All are Eta templates over the variable set above. |
|
|
67
|
+
| `on` | An ordered map of change patterns → next state. Evaluated at step time against the pending diff; first match wins; the step commits everything pending as `gtd(<actor>): <next-state>` — unless the target is a commit state, in which case the step performs the squash instead (decision 7). A commit state has no `on`: the process ends there. |
|
|
68
|
+
| `initial: true` | Exactly one state: where an unrecognized HEAD (any non-`gtd(actor): state` subject, or a state name outside this workflow) resolves. THE entry point; replaces entry rules, the fallback ladder, and boundary handling. |
|
|
69
|
+
| `retry` | Optional: `{ max: <n>, otherwise: <state> }` — when a transition would enter this state for the (max+1)-th time within the current process, it enters `otherwise` instead, decided at write time and encoded in the committed label. Visit counting walks only the current process's commits (start → HEAD), so it stays bounded and resets naturally at entry/squash. |
|
|
70
|
+
|
|
71
|
+
Pattern grammar: `<status> <glob>` where status ∈ `A|M|D|*`, or the bare token
|
|
72
|
+
`C` (clean tree — the empty step as an explicit, opt-in event). Contains-match:
|
|
73
|
+
a pattern fires if any pending change matches it. `"* *"` is the catch-all for
|
|
74
|
+
any dirty tree.
|
|
75
|
+
|
|
76
|
+
Resolution stays δ-pure and gets simpler:
|
|
77
|
+
`next = f(HEAD's state name, pending diff pattern)` with exactly one impurity
|
|
78
|
+
left (the retry count's process-scoped walk). Turn-taking keeps only:
|
|
79
|
+
out-of-turn refusal (invoker ≠ state's actor), the no-match refusal, and the
|
|
80
|
+
clean-step no-op when no `C` event is declared.
|
|
81
|
+
|
|
82
|
+
## 2. What dies
|
|
83
|
+
|
|
84
|
+
- Actor kinds (`interactive`/`autonomous`/`scripted`) and every carve-out keyed
|
|
85
|
+
on them (dirty-boundary entry turn, inert-clean-step defaults, draft
|
|
86
|
+
validation).
|
|
87
|
+
- Gates, `TurnGate`, capture rules, turn rules, routing rules — replaced by `on`
|
|
88
|
+
maps. The two-namespace grammar collapses to one subject form.
|
|
89
|
+
- Guard functions and the `WorkflowConfig` guard vocabulary (fact/all/any/
|
|
90
|
+
counterAtLeast/…) — replaced by change patterns.
|
|
91
|
+
- Interrupt/fallback ladders, conflicts (illegal combinations), entry rules —
|
|
92
|
+
replaced by the initial state + explicit `on` edges.
|
|
93
|
+
- `Gtd-Counters` trailers, stamps, forceApprove, fixAttemptCap/ reviewThreshold
|
|
94
|
+
config keys — replaced by `retry` + script-owned logic.
|
|
95
|
+
- The content-inspection exceptions (FEEDBACK.md emptiness, checkbox-only
|
|
96
|
+
REVIEW.md diffs, doc-structure validation): verdicts are now expressed by
|
|
97
|
+
which file is written or deleted (`D FEEDBACK.md` = approve, `M FEEDBACK.md` =
|
|
98
|
+
findings).
|
|
99
|
+
- The review checkout window (v3.0 drops it; may return later as a state flag —
|
|
100
|
+
noted as a follow-up, not in scope).
|
|
101
|
+
|
|
102
|
+
## 3. What the CLI becomes
|
|
103
|
+
|
|
104
|
+
- `gtd step <actor>` — authenticate, match patterns, commit
|
|
105
|
+
`gtd(<actor>): <next-state>` (with retry redirection at write time). A
|
|
106
|
+
transition targeting a commit state performs the squash instead of a turn
|
|
107
|
+
commit. Refusals: out-of-turn, no-match, failed `commit:` render.
|
|
108
|
+
- `gtd next [--json]` — emit the resolved state's rendered
|
|
109
|
+
script/prompt/message; JSON carries
|
|
110
|
+
`{state, actor, kind: script|prompt|message, content}` as the driver dispatch
|
|
111
|
+
key. Resolution never rests AT a commit state (entering one ends the process),
|
|
112
|
+
so `kind: commit` never appears here.
|
|
113
|
+
- `gtd run` — the built-in script driver: execute the emitted script verbatim,
|
|
114
|
+
then `gtd step <actor>`.
|
|
115
|
+
- `gtd status` — state, actor, and which pattern each pending change matches.
|
|
116
|
+
- `bin/gtd-loop` / the loop skill dispatch on `kind`.
|
|
117
|
+
|
|
118
|
+
## 4. The bundled default workflow (re-authored)
|
|
119
|
+
|
|
120
|
+
Sketch (final shape decided during Phase 3): `idle` (initial; message;
|
|
121
|
+
`"* *" → grilling`… actually entry captures the dirty tree), `grilling` ⇄
|
|
122
|
+
`grilling-answer`, `architecting` ⇄ `architecting-answer`, `decompose`,
|
|
123
|
+
`building`, `checking` (script — the test command lives INLINE in the script, no
|
|
124
|
+
`testCommand` config key; `A FEEDBACK.md → fixing`, retry max 3 → `escalate`,
|
|
125
|
+
`C → reviewing`), `fixing`, `reviewing`, `await-review`
|
|
126
|
+
(`D REVIEW.md → squashing`, `M REVIEW.md → grilling`), `escalate`, `squashing`
|
|
127
|
+
(prompt: author the commit-message file; `A COMMIT_MSG.md → done`), `done`
|
|
128
|
+
(`commit: <%~ it.read("COMMIT_MSG.md") %>`). Health path becomes `idle`'s own
|
|
129
|
+
script + `A HEALTH.md` edge. Verdict changes vs today: agentic approval = DELETE
|
|
130
|
+
FEEDBACK.md; review approval = DELETE REVIEW.md; the check script owns its
|
|
131
|
+
attempt count only if we want output-dependent budgets (the engine `retry`
|
|
132
|
+
covers the standard case).
|
|
133
|
+
|
|
134
|
+
## 5. Phases (each lands green, committed, pushed)
|
|
135
|
+
|
|
136
|
+
1. **Engine core.** New `src/PatternMachine.ts` (or repurposed Machine.ts):
|
|
137
|
+
state defs, pattern parser/matcher, resolve (HEAD state name → state;
|
|
138
|
+
unrecognized → initial), step semantics (refusals, C-event, no-match), retry
|
|
139
|
+
redirection, the commit-state squash action (render-then-squash-then-drop,
|
|
140
|
+
refusal on render failure). New minimal `ResolvePayload` (pending changes as
|
|
141
|
+
`{status, path}[]`, hashes, clean flag). Unit + property tests (δ: next =
|
|
142
|
+
f(state, diff); totality; refusal invariants).
|
|
143
|
+
2. **Config + templates.** The `workflow:` key becomes the ONLY definition
|
|
144
|
+
source (bundled default = a YAML asset compiled the same way): states, actor
|
|
145
|
+
strings, script/prompt/message/commit (inline or file-relative, auto-inlined
|
|
146
|
+
at load), `on` maps, validation (exactly one initial, exactly one content
|
|
147
|
+
property, every target defined, retry.otherwise defined, commit states carry
|
|
148
|
+
no actor and no `on`). Eta rendering with the agreed variable set, including
|
|
149
|
+
the `read(path)` helper.
|
|
150
|
+
3. **Default workflow re-authoring + CLI.** Write the bundled default config;
|
|
151
|
+
rewire program.ts (step/next/run/status), gtd-loop, loop skill; delete the
|
|
152
|
+
old model (Workflow.ts rules, WorkflowConfig guard language, Subjects
|
|
153
|
+
two-namespace grammar → one subject parser over the active state set;
|
|
154
|
+
Events.ts flag/trailer plumbing).
|
|
155
|
+
4. **E2e rewrite.** New feature files pinning the engine semantics (patterns,
|
|
156
|
+
refusals, retry, final squash, initial-state entry, driver protocol) +
|
|
157
|
+
default-workflow journeys. Old features deleted or rewritten.
|
|
158
|
+
5. **Docs.** STATES.md replaced by the v3 model reference; configuration.md
|
|
159
|
+
workflow section rewritten; upgrading doc gains the v3 note (old histories:
|
|
160
|
+
all previous subjects are unrecognized → initial state, by design); AGENTS.md
|
|
161
|
+
updated.
|
|
162
|
+
|
|
163
|
+
Sizing: phases 1–2 are the substance (new core + compiler); phase 3 is the big
|
|
164
|
+
deletion; phase 4 is broad but mechanical; phase 5 small.
|