@pmelab/gtd 8.0.0 → 8.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.
@@ -1,283 +0,0 @@
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).
@@ -1,164 +0,0 @@
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.
@@ -1,154 +0,0 @@
1
- # Pluggable steering-file modes
2
-
3
- > Status: IMPLEMENTED. The authoritative reference is
4
- > [STATES.md §12](../../STATES.md) (the model) and
5
- > [docs/configuration.md](../configuration.md#modes--pluggable-steering-file-modes)
6
- > (the config surface); this doc is the design record. Builds on
7
- > [steering-file-validation-command.md](steering-file-validation-command.md),
8
- > which introduced `gtd validate` and the `gtd step` capture gate.
9
-
10
- ## 1. Motivation
11
-
12
- `mode:` was a CLOSED vocabulary of two names — `qa` and `review` — each
13
- hardwired to a parser gtd ships (`src/OpenQuestions.ts` / `src/ReviewDoc.ts`).
14
- That is fine for the bundled default workflow, whose steering files ARE those
15
- two formats, but it makes the rest of the machine's extensibility story stop
16
- short: a workflow is data (states, actors, patterns, content, `vars:`) right up
17
- to the moment it wants a state whose output is checkable in some other way — an
18
- ADR, a JSON spec, an OpenAPI document, a translation catalogue. Such a state can
19
- declare a `file:`, but not what "valid" means for it, so the format gate the
20
- whole design leans on (a human gate is only ever handed a well-formed file)
21
- simply doesn't exist for it.
22
-
23
- Meanwhile gtd already knows how to let a workflow bring its own executable
24
- behavior: the scripted check actor (`script:` content run verbatim through
25
- `bash`, its outcome read back off the tree). Modes are the same shape of problem
26
- and deserve the same answer — a shell command, not a code change in gtd.
27
-
28
- ## 2. The design
29
-
30
- A **mode is a pair of shell commands over one file**: `format` (rewrite it in
31
- place) and `validate` (exit 0, or exit non-zero with the findings on
32
- stdout/stderr). Both optional; at least one required.
33
-
34
- ```yaml
35
- workflow:
36
- modes:
37
- adr:
38
- format: "npx prettier --write <%= it.file %>"
39
- validate: "./scripts/check-adr.sh <%= it.file %>"
40
- states:
41
- drafting:
42
- actor: agent
43
- prompt: "Write the ADR."
44
- file: docs/adr/<%= it.vars.slug %>.md
45
- mode: adr
46
- ```
47
-
48
- Five decisions:
49
-
50
- ### 2.1 Modes are workflow data, with a project layer over it
51
-
52
- A `modes:` map sits beside `states:` and `vars:` INSIDE `workflow:`, compiling
53
- onto `WorkflowDefinition.modes`: a mode exists to serve a state's `mode:`, and
54
- states are workflow data, so a workflow that needs a custom mode carries its
55
- own.
56
-
57
- A top-level `.gtdrc` `modes:` key layers over that, per half (`mergeModes`),
58
- exactly the way the top-level `vars:` key layers over the workflow's own
59
- `vars:`. Without it, a project on the BUNDLED default workflow could not plug in
60
- a formatter at all without copying every state into its `.gtdrc` — and since gtd
61
- ships no formatter (§2.2), that is precisely the common case. The rc layer is
62
- merged into the definition BEFORE `validateDefinition` runs
63
- (`compileWorkflowConfig`'s `rcModes` argument), so a state may name a mode
64
- either layer declares and the load-time name check (§2.3) stays a pure function
65
- of one definition.
66
-
67
- ### 2.2 `qa` and `review` stay built in — as VALIDATORS, and only that
68
-
69
- The two existing modes' parsers are also what `gtd lsp` publishes as live
70
- diagnostics and what its document symbols and code actions are built on —
71
- shelling out per keystroke over an unsaved buffer is a different (and much
72
- worse) design. So gtd keeps them as its `BUILT_IN_MODES`, available in every
73
- workflow without being declared.
74
-
75
- Their FORMATTING half went the other way. gtd used to bundle prettier and expose
76
- it as a `gtd format <file>` subcommand, which the built-in modes then called;
77
- both are gone (the subcommand, and prettier as a runtime dependency — the
78
- single-file bundle drops from ~15 MB to ~10 MB). A project brings its own
79
- formatter and plugs it into whichever mode should use it. The two built-ins
80
- therefore VALIDATE and nothing else, until a `modes:` entry gives them a
81
- `format:`.
82
-
83
- Which forces the resolution rule: the halves resolve INDEPENDENTLY, first layer
84
- wins per half.
85
- `modes: { qa: { format: "npx prettier --write <%= it.file %>" } }` adds
86
- formatting to `qa` and keeps gtd's open-questions validation; declaring
87
- `validate:` instead displaces the parser. (An earlier draft of this design had a
88
- declared entry REPLACE a built-in wholesale — that cannot express "gtd's
89
- validation plus my formatter", which is the common case.)
90
-
91
- ### 2.3 `mode:` stays load-time-validated, against a definition-derived set
92
-
93
- `validateDefinition` no longer checks a hardcoded vocabulary; it checks
94
- `knownModes(def)` — the built-ins plus the declared names — exactly the way the
95
- commit grammar's closed actor set derives from `declaredActors`. A typo'd mode
96
- must not silently disable both the capture gate and the editor's diagnostics, so
97
- this stays an error at config-load time, and its message lists both sets.
98
-
99
- ### 2.4 Commands are Eta templates with `it.file`, executed at the edge
100
-
101
- A command sees the resting state's full template context plus `it.file`, the
102
- already-rendered steering-file path (`PatternTemplates.ModeCommandContext`) — so
103
- a command can read `it.vars`, and a workflow whose `file:` is vars-parameterized
104
- never has to repeat the path. Execution is `bash -c` from the repo root, output
105
- captured (unlike `gtd run`'s scripted actor, whose output is for the human).
106
-
107
- `./scripts/check-adr.sh` in a command is a COMMAND, never a `./`-relative file
108
- reference the way a content string is — inlining it as template text would break
109
- the obvious reading.
110
-
111
- ### 2.5 The pure engine still never validates anything
112
-
113
- Everything above is an edge concern, in one new module, `src/SteeringMode.ts`
114
- (resolution + execution) — the same shape as `src/ReviewWindow.ts`. The engine
115
- carries `modes:` as inert data and validates its shape; `PatternMachine.step`
116
- never sees a mode.
117
-
118
- ## 3. What changed
119
-
120
- | Area | Change |
121
- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
- | `src/PatternMachine.ts` | `ModeDef`; `WorkflowDefinition.modes`; `StateMode` widened from `"qa" \| "review"` to `string`; `BuiltInMode`/`isBuiltInMode`/`knownModes`; `validateModes`; `validateMode` now definition-derived |
123
- | `src/PatternConfig.ts` | `compileModesMap` + `mergeModes` (mirroring `compileVarsMap`), `modes` added to `KNOWN_TOP_KEYS`, `compileWorkflowConfig`'s `rcModes` argument |
124
- | `src/Config.ts` / `src/ConfigSchema.ts` | the third blessed top-level key, `modes:` (`compileRcModes`), layered over the compiled workflow (or the bundled default) per half |
125
- | `src/Format.ts`, `gtd format <file>` | DELETED, and `prettier` dropped from the runtime dependencies — formatting is a mode's own command now |
126
- | `src/PatternTemplates.ts` | `ModeCommandContext` (`TemplateContext` + `file`) and `renderModeCommand` |
127
- | `src/SteeringMode.ts` (new) | `resolveSteeringMode` (per half), `formatSteeringFile`, `validateSteeringFile`, `formatAndValidateSteeringFile`, `unknownModeMessage` |
128
- | `src/program.ts` | `formatAndCheckSteeringFile` routes through the resolved mode (was: a hardcoded `qa`/`review` switch plus `formatFile`); `gtd validate` and the capture gate are otherwise unchanged |
129
- | `src/ConfigSchema.ts` / `schema.json` | the `modes:` object; `mode:`'s `enum` dropped |
130
- | `src/Lsp.ts` | docs only — a custom mode gets no live diagnostics/symbols/code actions (documented limitation) |
131
- | `src/workflows/default.yaml` | unchanged — the bundled default keeps `qa`/`review`, which resolve to the built-in validators |
132
-
133
- One behavior change for existing users: steering files are no longer
134
- auto-formatted, because gtd no longer has a formatter to do it with. Validation
135
- is untouched. A project that wants the old behavior adds four lines to its
136
- `.gtdrc` (the `modes: { qa: { format: … } }` snippet in
137
- [configuration.md](../configuration.md#modes--pluggable-steering-file-modes)).
138
-
139
- ## 4. Known limitations
140
-
141
- - **No live editor support for a custom mode.** `gtd lsp` dispatches on the
142
- built-in names only; a workflow-declared mode's file gets no diagnostics, no
143
- document symbols, no code actions. It is still formatted and validated by
144
- `gtd validate` and the `gtd step` gate.
145
- - **Nothing is formatted out of the box.** The bundled default declares no
146
- `format:` command, so a fresh install validates but never rewrites. This was
147
- the deliberate alternative to shipping an `npx prettier` default, which would
148
- make every gate depend on prettier resolving in the user's project.
149
- - **A command runs on the developer's machine, unsandboxed** — same trust model
150
- as a `script:` state: a `.gtdrc` is project code.
151
- - **`gtd next`'s self-validation instruction** is appended for any `prompt` rest
152
- declaring `file:`+`mode:`, including a mode that declares only `format:`
153
- (where `gtd validate` can only ever pass). Harmless, and keeps the rule "a
154
- state that hands over a steering file tells its agent to check it".