@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,437 +0,0 @@
1
- # Research: static validation of workflow definitions
2
-
3
- > Status: PARTIALLY IMPLEMENTED. Scope decision: a workflow is bound to a
4
- > project and edited as a project-wide change (never mid-process), so the
5
- > mid-flight/backward-compat concerns in §6 don't apply, and no warning channel
6
- > is needed — implemented checks are plain errors. Landed: the full `workflow:`
7
- > typing in `schema.json` (§6.1, via `jsonSchema` annotations in
8
- > `src/ConfigSchema.ts`) and the unreachable-state error (§3.1, as
9
- > `validateReachability` in `src/PatternMachine.ts` — invalid `on`/
10
- > `retry.otherwise` targets were already errors). Everything else in this
11
- > document is deliberately on hold.
12
-
13
- ## 1. What is validated today (baseline)
14
-
15
- Two layers run at config-load time, both aggregate-all-findings-then-throw
16
- (`compileWorkflowConfig` merges its shape errors with `validateDefinition`'s
17
- findings into one error — a bad config fails loudly at load, never at step
18
- time):
19
-
20
- - **Config shape** (`src/PatternConfig.ts`): unknown top-level/state/retry keys,
21
- wrong types, unreadable `./` file references, non-scalar `vars:` values.
22
- - **Definition rules** (`src/PatternMachine.ts` `validateDefinition`): at least
23
- one state; exactly one non-commit `initial`; exactly one content kind per
24
- state; commit states carry no `actor`/`on`/`model`/`file`/`mode`; non-commit
25
- states carry an `actor`; every `on` pattern parses; every `on` target and
26
- `retry.otherwise` names a defined state; `retry.max` is a non-negative
27
- integer; `file`/`model` non-empty; `mode` ∈ {qa, review} and requires `file`.
28
-
29
- Every one of these checks is **local** — a rule over one state (or one field) at
30
- a time. Nothing today looks at the definition as a _graph_. The v2 design doc
31
- (`configurable-state-machine.md` §3.11) planned exactly that class of checks
32
- (totality, reachability, ambiguity, livelock bounds); the v3 rewrite shipped
33
- without them.
34
-
35
- ## 2. The gap: definitions that validate clean but are broken
36
-
37
- Each of these passes today's validation:
38
-
39
- 1. **Unreachable state** — defined, valid, but no `on` target or
40
- `retry.otherwise` ever names it (typically a rename that missed the edges).
41
- It silently never runs.
42
- 2. **Dead-end state** — a non-commit state with no `on` map (or an empty one).
43
- Once entered, every step is a no-op (clean) or refusal (dirty) forever; the
44
- process can never leave.
45
- 3. **Trap cycle** — a group of states that reach each other but from which no
46
- terminal (a commit state, or re-entry into the initial state — the bundled
47
- default's only "end") is reachable. The process runs but can never complete.
48
- 4. **Unbounded agent loop (livelock)** — a cycle consisting only of agent/check
49
- states with no `retry` cap on any member. The default's `fixing ↔ checking`
50
- loop is safe only because `fixing` carries
51
- `retry: {max: 3, otherwise: escalate}`; delete that key and the machine
52
- validates clean but can ping-pong forever with no human in the loop.
53
- 5. **Shadowed `on` row** — `on` is first-match-wins, so
54
- `{"* **": a, "A .gtd/FEEDBACK.md": b}` never routes to `b`: the catch-all
55
- subsumes the specific row. Dead rows are silent today.
56
- 6. **`retry.otherwise` cycle / self-reference** — `applyRetry` terminates at
57
- runtime via its `visited` guard, but the guard's "accept the repeated target
58
- as final" behavior is almost certainly not what the author meant; the config
59
- is accepted silently.
60
- 7. **Pointless `retry`** — retry redirection applies when a state is entered _as
61
- a transition target_; a `retry` on a state nothing targets never fires.
62
- 8. **Template errors discovered at step time** — Eta syntax errors in
63
- `prompt`/`script`/`message`/`commit`/`file` templates surface only when that
64
- state is first rendered, contradicting the "fails at load, never at step
65
- time" contract everything else honors.
66
- 9. **The documented `vars` ↔ `on` desync** (`default.yaml`'s KNOWN LIMITATION) —
67
- `on` patterns are literal strings while `file:` and content templates read
68
- `it.vars.todoFile` etc.; repointing a var moves what templates read/write
69
- without moving what patterns match.
70
-
71
- ## 3. Algorithm survey
72
-
73
- The good news: a gtd workflow is a small, explicit, finite directed graph (the
74
- bundled default: 12 nodes, ~25 edges; a custom one is the same order of
75
- magnitude). Every algorithm below is textbook material and costs microseconds at
76
- this size — there is no need for a model checker, SAT solver, or symbolic
77
- anything. The edge set for all graph checks is `on` targets ∪ `retry.otherwise`
78
- targets.
79
-
80
- ### 3.1 Reachability — accessible states (BFS/DFS from the initial state)
81
-
82
- The classic "useless states" removal from automata theory, first half: a state
83
- is _accessible_ iff some path from the start state reaches it. One BFS/DFS from
84
- the `initial` state over the edge set, O(V+E). Anything not visited is finding
85
- #1 (unreachable).
86
-
87
- **gtd nuance:** history is an attributed state trace, and `resolveState` accepts
88
- _any_ hand-authored `gtd(<actor>): <state>` subject naming a defined non-commit
89
- state and a declared actor — so an edge-unreachable state is still _enterable_
90
- by writing a commit by hand. That makes unreachability a **warning**, not a hard
91
- error: it is almost always a typo'd rename, but a deliberate "manual entry
92
- point" state is expressible and legal. (It also means an unreachable state's
93
- `actor` still widens the closed-world actor vocabulary — one more reason the
94
- finding deserves surfacing.)
95
-
96
- ### 3.2 Co-reachability — can-complete (reverse BFS from terminals)
97
-
98
- The second half of the same textbook construction: a state is _co-accessible_
99
- iff a terminal is reachable from it; an automaton restricted to accessible ∩
100
- co-accessible states is called _trim_. Reverse the edges, BFS from the terminal
101
- set, flag unvisited states.
102
-
103
- **gtd's terminal set** is `commit` states ∪ the `initial` state — the bundled
104
- default has no commit state at all; its cycle "ends" by re-entering `idle` (the
105
- process-boundary rule in `computeProcessRun`). A reachable state that is not
106
- co-accessible is a trap (finding #3); a state with no outgoing edges at all
107
- (finding #2) is the degenerate case but deserves its own, more specific message.
108
-
109
- This is also exactly what **workflow-net soundness** (van der Aalst; the Woflan
110
- validator) checks in the Petri-net world: _option to complete_ (from every
111
- reachable marking, the end is reachable) plus _no dead transitions_. gtd's
112
- machine has no concurrency/tokens, so full soundness collapses to the two
113
- reachability passes above — the whole "soundness" property is two BFS runs here.
114
-
115
- ### 3.3 Livelock heuristic — SCC analysis (Tarjan)
116
-
117
- Full progress proofs are out of reach (whether a pattern fires depends on what
118
- an agent writes), but the v2 plan's "livelock bounds" idea translates cleanly:
119
- compute strongly connected components (Tarjan, O(V+E)), and for each cyclic SCC
120
- check that it contains an _escape guarantee_:
121
-
122
- - a state whose `actor` is a human-ish rest point (the machine stops and waits —
123
- a human can always steer out), **or**
124
- - a member with a `retry` cap whose `otherwise` leaves the SCC (bounded
125
- redirection out).
126
-
127
- An SCC with neither — e.g. `building → checking → fixing → checking` with no cap
128
- — is a machine that can loop autonomously forever. Since "human-ish" is not
129
- knowable (actors are open-vocabulary strings; the engine deliberately blesses no
130
- names), the practical formulation is: **flag any cycle in which no member state
131
- either awaits a different actor than the cycle's dominant one or carries a retry
132
- cap escaping the cycle** — or, simpler and honest, make it a warning listing the
133
- cycle and what would bound it. This mirrors how linters handle undecidable
134
- properties: name the risk, don't pretend to prove it.
135
-
136
- ### 3.4 Shadowed/dead `on` rows — pattern subsumption (match-arm usefulness)
137
-
138
- `on` is an ordered first-match-wins list — semantically identical to ML/Rust
139
- `match` arms, where compilers warn on unreachable arms using the _usefulness_
140
- algorithm (Maranget, "Warnings for pattern matching"). gtd's pattern grammar is
141
- far simpler than ML patterns, so full usefulness reduces to pairwise
142
- subsumption:
143
-
144
- - Row _j_ is dead iff some earlier row _i_ fires on every diff row _j_ fires on.
145
- Matching is existential over the change set, so language inclusion is
146
- sufficient: if row _i_'s (status, glob) language ⊇ row _j_'s, any change
147
- matching _j_ also matches _i_, hence _i_ fires first.
148
- - `C` is disjoint from every diff row (clean vs. dirty), so it can only be
149
- shadowed by an earlier duplicate `C`.
150
- - Status subsumption is trivial (`*` ⊇ `A`/`M`/`D`; equal otherwise). Glob
151
- inclusion over the grammar {literal chars, `*`, `**`} is decidable — both
152
- compile to regular languages (`globToRegExp` already builds the regexes), and
153
- regular-language inclusion is decidable via product construction. In practice
154
- a direct segment-wise structural check (the approach gitignore/ file-watcher
155
- tooling uses) covers it without regex machinery, and even the cheap sufficient
156
- subset catches the real-world cases:
157
- - an exact duplicate pattern string earlier in the list (see also §4.1 — YAML
158
- may reject or silently drop these before the compiler sees them),
159
- - `* **` (or `* <glob>` with the identical glob) declared before a
160
- more-specific row,
161
- - same glob, earlier row's status `*` vs. later row's specific letter.
162
-
163
- This one is a hard **error** candidate: a dead row is never intentional, and the
164
- fix (reorder) is mechanical.
165
-
166
- Deliberately **not** worth adopting from the match-checking world:
167
- _exhaustiveness_. A dirty tree matching no row is a refusal **by design**
168
- (AGENTS.md: "something happened that nothing recognizes" is a signal distinct
169
- from a no-op), and a clean step with no `C` row is the documented default no-op.
170
- The event space is intentionally non-total; a totality lint would fight the
171
- semantics.
172
-
173
- ### 3.5 Retry-chain analysis — cycle detection on a functional graph
174
-
175
- `retry.otherwise` edges form a functional graph (≤1 outgoing edge per state), so
176
- cycle detection is a trivial colored DFS (or Floyd's, but at this size DFS with
177
- a visited set is clearer — `applyRetry` already contains the runtime version of
178
- it). Findings: `otherwise` chains that cycle (error — the runtime guard's
179
- "accept the repeated target" fallback is a documented we-must-terminate
180
- behavior, not a meaning), `otherwise: <self>` (degenerate case of the same), and
181
- a `retry` on a state that no `on` edge targets (warning — it can never fire, per
182
- `applyRetry`'s entry-as-target semantics).
183
-
184
- ### 3.6 Template validation — compile at load time
185
-
186
- Eta exposes compilation separately from rendering: `compile()` every
187
- `prompt`/`script`/`message`/`commit`/`file` string at load and report syntax
188
- errors as load errors, closing gap #8 without executing anything impure
189
- (rendering stays an edge concern; compiling is pure).
190
-
191
- One step further, the **undefined-variable lint**: scan template source for
192
- `it.vars.<name>` references and compare against the merged
193
- workflow-`vars:`/rc-`vars:` keys. Because a third layer (`GTD_*` env vars) can
194
- legitimately supply a name at run time, an unknown name is a **warning**, not an
195
- error. (A full "render with a tracking proxy and record misses" approach also
196
- works but executes template code at load — scanning the source is safer and
197
- catches the common typo just as well.)
198
-
199
- ### 3.7 Domain cross-checks (gtd-specific lints)
200
-
201
- Not from the literature — from this codebase's own documented failure modes:
202
-
203
- - **`file:` ↔ `on` consistency** (gap #9): render each state's `file:` template
204
- with the workflow's _declared default_ vars (pure — the defaults are right
205
- there in the definition) and warn when the rendered path is matched by none of
206
- the workflow's `on` globs. This turns `default.yaml`'s KNOWN LIMITATION
207
- comment into a machine-checked finding whenever a var repoint desyncs
208
- templates from patterns. The same check re-run at `gtd status` time with the
209
- _actual_ merged vars (env layer included) closes the runtime half of the gap.
210
- - **Actor typo proximity**: the actor vocabulary is open by design, so `agnet`
211
- is a legal actor — but a state awaiting it will never be stepped by the loop
212
- driving `agent`. A Levenshtein "did you mean" warning when one declared actor
213
- is within edit distance 1 of another (a pattern lifted from CLI arg parsers
214
- and `git`'s command suggestions). Warning-only; false positives are possible
215
- (`qa1`/`qa2`), so never an error.
216
-
217
- ### 3.8 Dynamic validation — model-based testing (complement, not substitute)
218
-
219
- Since `step` is a pure function, the graph can be _executed_ exhaustively in
220
- tests: for every state and every `on` row, synthesize a minimal diff that the
221
- row's pattern matches (invert the glob: replace `*`/`**` with a literal segment)
222
- and assert `step` routes there — i.e., every declared edge is exercisable. This
223
- is the state-machine-testing pattern XState ships as `@xstate/graph`
224
- (`getShortestPaths`/`getSimplePaths` for path coverage) and what the
225
- model-based-testing literature calls transition coverage. It overlaps §3.4 (a
226
- shadowed row shows up as an unexercisable edge) but from the behavioral side,
227
- and it doubles as a regression harness for the pattern grammar itself. Fits
228
- naturally as a property test over `defaultWorkflowDefinition` rather than a
229
- load-time check.
230
-
231
- ## 4. Patterns from comparable systems
232
-
233
- - **Automata theory** — _trim_ construction (accessible ∩ co-accessible),
234
- §3.1/§3.2. The canonical "early problem detection" for FSMs.
235
- - **Workflow nets** — soundness (option to complete, no dead transitions);
236
- degenerates to the same two BFS passes for token-free graphs.
237
- - **SCXML / W3C statecharts** — conformance validation is exactly this flavor:
238
- every `target` IDREF resolves, `initial` names a real child, etc. gtd already
239
- has the local half; the graph half is what's missing.
240
- - **XState** — beyond TS-level typegen, its ecosystem does reachability via path
241
- enumeration (`@xstate/graph`) and visual inspection (Stately inspector) rather
242
- than a lint pass — an argument for also shipping a human-readable graph dump
243
- (see §5, `gtd lint`).
244
- - **ML/Rust match checking** — usefulness/unreachable-arm warnings, §3.4; also
245
- the precedent for making dead-arm a _warning_ in some compilers and an _error_
246
- in none... but gtd's rows are config, not code paths, and a dead row means a
247
- whole state may be unreachable — error is defensible.
248
- - **Model checking (CTL/LTL)** — names the property taxonomy worth keeping:
249
- today's checks are all _safety_ ("nothing malformed"); §3.2/§3.3 add the cheap
250
- _liveness_ half ("something good stays reachable"). At 12 nodes,
251
- `AG EF terminal` is just reverse-BFS — no checker needed.
252
- - **Linter architecture (ESLint et al.)** — severity tiers (error/warning),
253
- collect-everything-then-report (gtd already does this for errors), stable
254
- finding identifiers, and machine-readable output. The missing piece in gtd is
255
- a **warning channel**: `compileWorkflowConfig` can only throw-or-pass, so
256
- warning-tier findings (§3.1's manual-entry nuance, §3.3's heuristic, §3.6's
257
- env-supplied vars, §3.7) currently have nowhere to go.
258
-
259
- ## 5. Recommendation
260
-
261
- Tier the findings and keep every new check a pure function over
262
- `WorkflowDefinition` (they need nothing but the definition — they belong in
263
- `PatternMachine.ts` next to `validateDefinition`, except the Eta compile check,
264
- which imports Eta and belongs in `PatternTemplates.ts`):
265
-
266
- **Errors** (extend `validateDefinition`'s existing thrown aggregate; all
267
- unambiguous, mechanical fixes):
268
-
269
- 1. Non-commit state with no `on` rows (dead end, §3.2's degenerate case).
270
- 2. Reachable state from which no terminal (commit state or initial) is reachable
271
- (trap, §3.2).
272
- 3. `on` row strictly subsumed by an earlier row (§3.4).
273
- 4. `retry.otherwise` self-reference or cycle (§3.5).
274
- 5. Eta template that does not compile (§3.6).
275
-
276
- **Warnings** (requires adding a `{ severity, message }` shape and a channel —
277
- print to stderr at load and/or surface under `gtd status`; a `gtd lint`
278
- subcommand that prints all findings plus the state graph would follow the
279
- XState-inspector precedent and cost little):
280
-
281
- 6. State unreachable via edges from `initial` (§3.1 — legal via hand-authored
282
- subjects, almost always a typo).
283
- 7. Autonomous cycle with no retry-capped escape (§3.3 heuristic).
284
- 8. `retry` on a state no edge targets (§3.5).
285
- 9. `it.vars.<name>` reference with no declared default (§3.6 — env layer may
286
- supply it).
287
- 10. `file:` rendering (with declared-default vars) to a path no `on` glob
288
- matches (§3.7).
289
- 11. Declared actor within edit distance 1 of another (§3.7).
290
-
291
- **Test-side** (not load-time): transition-coverage property test over the
292
- bundled default — synthesize a matching diff per `on` row, assert `step` takes
293
- the edge (§3.8).
294
-
295
- Sizing check: all graph passes are O(V+E) (two BFS + one Tarjan), subsumption is
296
- O(rows² per state) over a trivial grammar, and template compilation is the only
297
- cost that scales with content size. On workflows of tens of states this is
298
- sub-millisecond — safe to run on every invocation, exactly like today's
299
- `validateDefinition`.
300
-
301
- ### Interactions to respect
302
-
303
- - **Severity of unreachability** hinges on `resolveState`'s hand-authored
304
- subject entry (§3.1) — do not promote finding 6 to an error without also
305
- deciding that manual-entry states are unsupported.
306
- - **The bundled default must stay finding-free** — findings 7 and 10 are
307
- calibrated against it (`fixing`'s cap bounds the check loop; the literal
308
- `.gtd/...` patterns match the declared default vars). Any new check must be
309
- validated against the bundled unified template (`src/workflows/unified.yaml`,
310
- covering both its simple and advanced flows) before landing.
311
- - **Warning channel is the only architectural change** — everything else slots
312
- into the existing collect-and-merge flow in `compileWorkflowConfig`.
313
-
314
- ## 6. Reframe: safeguards for USER-authored machines
315
-
316
- §1–§5 read as "validate the definition harder." The actual product goal is
317
- narrower and different: the bundled default is already covered by e2e; what
318
- needs protecting is a **user hand-writing a `workflow:` key in `.gtdrc`** — and
319
- that shifts the design in three ways:
320
-
321
- 1. **Static analysis is structurally insufficient.** Whether an `on` pattern
322
- ever fires depends on what an agent writes into the tree at run time. No
323
- load-time check can prove a user's machine makes progress — it can only catch
324
- the mechanically-dead shapes (§2's findings 1–8). The worst user-facing
325
- failures — an agent↔check loop burning tokens all night, a process resting in
326
- a state whose patterns never match what its own prompt asks the agent to
327
- produce — need **runtime bounds**, not better proofs.
328
- 2. **Severity must be recalibrated for other people's configs.** A new hard
329
- error is a breaking change: a config that ran yesterday must not brick the
330
- repo after `npm update`, _especially_ mid-process. Checks that are
331
- "error-worthy" for a definition we author (§5's shadowed-row, trap-state)
332
- default to **warnings** for user configs; hard errors stay reserved for
333
- definitions that cannot execute at all (today's structural rules, plus
334
- templates that don't compile).
335
- 3. **A broken config must never take reporting down with it.** Today
336
- `compileWorkflowConfig` throws, and everything — including `gtd status` —
337
- dies with it. For a user mid-flight that's the difference between "gtd tells
338
- me what's wrong" and "gtd won't even tell me where I am."
339
-
340
- The safeguards form four layers, ordered by when they catch the problem:
341
-
342
- ### 6.1 Authoring time — before the machine ever runs
343
-
344
- - **A real JSON Schema.** `schema.json` is generated from `ConfigSchema.ts`,
345
- where `workflow`/`vars` are `Schema.Unknown` — so the published schema says
346
- `"title": "unknown"` and editors validate NOTHING about the one key users
347
- hand-write. Hand-authoring the workflow shape into the schema (states map,
348
- actor, the four content kinds, `on` as string→string, `retry`
349
- `{max, otherwise}`, `model`/`file`/`mode`) gives every yaml-language-server
350
- editor squiggles and autocomplete for free, with zero gtd code. The compiler
351
- stays the source of truth (the schema can't express "exactly one content kind"
352
- or cross-state rules); the schema is the fast first net, not a replacement.
353
- Cheapest, highest-leverage single item in this document.
354
- - **`gtd lint` (a.k.a. "check my machine before I trust it").** Compile the
355
- config, run every §5 check, print ALL findings (errors + warnings) plus a
356
- human-readable dump of the graph — states, actors, edges, retry caps, and the
357
- reachability/terminal classification per state — so an author can _see_ the
358
- machine they wrote (mermaid `stateDiagram-v2` output fits: renderable in any
359
- markdown viewer, no new dependency to emit text). Also dry-compiles every Eta
360
- template. This is where warnings can be exhaustive and loud, because the user
361
- explicitly asked.
362
-
363
- ### 6.2 Load time — every invocation, quiet by default
364
-
365
- - **The §5 checks with user-config severities**: structural rules and
366
- template-compile failures stay throwing errors; all graph findings
367
- (unreachable, trap, shadowed row, retry cycle, autonomous cycle) surface as
368
- stderr warnings — one line each, prefixed (`gtd: workflow warning: ...`),
369
- never fatal. Errors keep the current all-findings-in-one-throw behavior.
370
- - **Degrade reporting gracefully**: `gtd status` should still answer with the
371
- findings when the config is broken (resolve HEAD against nothing, print the
372
- compile errors as the status) rather than crashing with a stack trace.
373
- `step`/`run`/`next` keep refusing — acting on a broken machine is worse than
374
- stopping — but the _reporting_ path must survive any config.
375
-
376
- ### 6.3 Run time — the bounds static analysis cannot provide
377
-
378
- These are the actual safeguards, in the circuit-breaker sense. All three are
379
- pure functions of data the engine already has (`processTrace`, the pending diff,
380
- the definition) — they fit `PatternMachine.step`'s purity discipline.
381
-
382
- - **A per-process autonomous-turn budget.** The v2 plan's "chain-depth limit"
383
- (§3.11: "bound it and hard-error past, say, 32 hops"), reborn for v3: count
384
- process-trace entries into states whose actor differs from the workflow's
385
- human-facing rest points — concretely, entries since the last turn authored by
386
- a DIFFERENT actor than the loop's autonomous pair, or simplest and honest:
387
- total trace length. Past a cap (workflow-overridable via a reserved-ish var or
388
- top-level config key, default generous — e.g. 50), `step` refuses with a new
389
- refusal reason (`"budget"`) naming the count and how to raise the cap. This is
390
- the one safeguard that holds NO MATTER WHAT the user's graph looks like — the
391
- livelock heuristic (§3.3) can then stay a soft warning without leaving a hole.
392
- - **Stall detection in the engine, not the skill.** `skills/loop/SKILL.md`
393
- currently tells the driver, in prose, to stop when the same state+content
394
- repeats with no new commits. Promote the signal: `gtd next --json` (and
395
- `status --json`) can report `"revisits": <n>` — how many times the current
396
- state already appears in the process trace. Any driver (not just one that read
397
- the skill carefully) can then apply its own stop rule; the skill's prose
398
- heuristic becomes one consumer of a first-class field. No behavior change in
399
- the engine's own decisions.
400
- - **Self-explaining refusals.** A no-match refusal already lists the state's
401
- declared patterns; for a user debugging their own machine, also list the
402
- pending changes that failed to match (status + path, first N). "Your tree has
403
- `M src/x.ts` but this state only matches `A .gtd/FEEDBACK.md`" turns the most
404
- common custom-workflow dead end from a mystery into a diff-vs-pattern
405
- diagnosis the user can act on immediately.
406
-
407
- ### 6.4 Recovery — no config may trap the repository
408
-
409
- Already true, worth asserting and documenting as a guarantee: the machine's
410
- whole state is HEAD's commit subject, so a hand-authored commit
411
- (`git commit --allow-empty -m "gtd(human): <state>"` for any defined non-commit
412
- state, or any non-gtd subject to fall back to the initial state) exits ANY state
413
- a broken config strands a process in. A user's workflow can waste turns; it
414
- cannot brick a repo. An e2e scenario pinning this (hand-commit out of an
415
- intentionally-trapped custom workflow) makes the guarantee load-bearing instead
416
- of incidental — and it is the reason every graph finding in §6.2 can afford to
417
- be a warning.
418
-
419
- ### What deliberately stays out
420
-
421
- - No sandboxing or vetting of `script:` content — the workflow author is
422
- trusted; scripts already run verbatim by design (`gtd run`).
423
- - No engine interpretation of actor names for the budget/livelock rules beyond
424
- counting — the actor vocabulary stays unblessed.
425
- - No auto-repair ("we removed your unreachable state") — findings report; the
426
- user decides.
427
-
428
- ### Revised priority (user-safeguard lens)
429
-
430
- 1. JSON Schema for the `workflow:` shape (§6.1) — editor-time, zero engine code.
431
- 2. Warning channel + graceful `gtd status` degradation (§6.2) — the
432
- architectural enabler.
433
- 3. Autonomous-turn budget refusal (§6.3) — the unconditional backstop.
434
- 4. Graph warnings from §5, demoted to warning severity (§6.2).
435
- 5. Refusal diagnostics + `revisits` in `--json` (§6.3).
436
- 6. `gtd lint` with graph dump (§6.1) — subsumes nothing above, packages all of
437
- it for the authoring moment.
@@ -1,182 +0,0 @@
1
- # Plan: steering-file validation loops in the default workflow
2
-
3
- > SUPERSEDED (§2–§4): the in-machine validation-loop states this plan added —
4
- > `todo-validating` and `review-validating` — and their `.gtd/FORMAT.md`
5
- > steering file have all been DELETED. Deterministic format-checking now lives
6
- > in the `gtd validate` command (which runs the same pure `parseOpenQuestions` /
7
- > `parseReviewDoc` parsers the state's `mode:` selects) plus producing-agent
8
- > self-validation, rather than in bash-port `check`/`script` states. The default
9
- > workflow is now 10 states, not 12. See
10
- > [docs/design/steering-file-validation-command.md](steering-file-validation-command.md)
11
- > and [STATES.md §12](../STATES.md). §1 (the file formats) and §5 (the LSP
12
- > resurrection) below remain valid and current.
13
-
14
- > Status: §2–§4 (the two validation loops in the default workflow) AND §5 (the
15
- > LSP resurrection) both LANDED 2026-07-22. Goal: the bundled default workflow
16
- > completely maps the functionality the deleted v2 LSP server (`src/Lsp.ts`,
17
- > removed in the v3 rewrite) provided over the two steering files: the
18
- > **open-questions format** in `.gtd/TODO.md` and the **checkbox review format**
19
- > in `.gtd/REVIEW.md` — including a deterministic validation loop for each.
20
- > Everything is pure workflow configuration; the engine is untouched.
21
-
22
- ## 1. The formats (authoritative spec = the deleted parsers)
23
-
24
- Extract the exact rules from git history —
25
- `git show 60c7490^:src/OpenQuestions.ts` and
26
- `git show 60c7490^:src/ReviewDoc.ts` — and embed them BOTH in the agent prompts
27
- (so drafts are born valid) and in the validator scripts (so malformed drafts
28
- bounce):
29
-
30
- **`.gtd/TODO.md` open questions** (`OpenQuestions.ts`):
31
-
32
- - Free-form prose, plus an OPTIONAL `## Open Questions` section (near the top)
33
- and an OPTIONAL `## Answered Questions` section (at the bottom) — either
34
- absent = zero questions of that status, valid.
35
- - Every `###` sub-heading directly under one of those sections is one question;
36
- its STATUS is POSITIONAL (under `## Open Questions` → open, under
37
- `## Answered Questions` → answered). The body is free-form (the agent's
38
- suggested answer for an open one, the settled resolution for an answered one)
39
- — there is no `Suggested default:`/`Answer:` marker line. The only structural
40
- error is a `###` heading with no question text.
41
- - A question is answered/accepted by MOVING its `###` block down into
42
- `## Answered Questions` (the agent does this on the next `grilling` lap; a
43
- human leaving a suggestion untouched IS acceptance). Nothing enforces the move
44
- or the section order — that is the producing agent's prompt contract.
45
- - Each section ends at the next level-1/2 heading or EOF.
46
-
47
- **`.gtd/REVIEW.md`** (`ReviewDoc.ts`):
48
-
49
- - First non-blank line: `# Review: <short-hash>`.
50
- - A `<!-- base: <hash> -->` comment somewhere in the document.
51
- - At least one `## <Chunk Title>` (non-empty title), each with at least one file
52
- pointer matching `- [ ] ./path/to/file.ts#42 — note` / `- [x] …` (checkbox,
53
- `./`-relative path, optional `#line`, optional `—`/`-` note) — the deleted
54
- `FILE_POINTER_RE` is the shape to port.
55
-
56
- ## 2. The new default workflow (12 states)
57
-
58
- Current 7 states, plus the two loops. `smart` on `grilling` and `reviewing`.
59
-
60
- | State | Actor | Content | `on` (declaration order matters) |
61
- | ------------------- | ----- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
62
- | `idle` (initial) | human | message | `* **` → `grilling` |
63
- | `grilling` | agent | prompt | `* **` → `todo-validating` |
64
- | `todo-validating` | check | script | `A/M .gtd/FORMAT.md` → `grilling`; `D .gtd/FORMAT.md` → `grilling-answer`; `C` → `grilling-answer` |
65
- | `grilling-answer` | human | message | `C` → `building`; `* **` → `grilling` |
66
- | `building` | agent | prompt | `* **` → `checking` |
67
- | `checking` | check | script | `A/M .gtd/FEEDBACK.md` → `fixing`; `D .gtd/FEEDBACK.md` → `reviewing`; `C` → `reviewing` |
68
- | `fixing` (retry 3) | agent | prompt | `* **` → `checking` |
69
- | `escalate` | human | message | `* **` → `checking` |
70
- | `reviewing` | agent | prompt | `* **` → `review-validating` |
71
- | `review-validating` | check | script | `A/M .gtd/FORMAT.md` → `reviewing`; `D .gtd/FORMAT.md` → `await-review`; `C` → `await-review` |
72
- | `await-review` | human | message | `D .gtd/REVIEW.md` → `idle`; `M .gtd/REVIEW.md` → `review-deciding`; `* **` → `grilling`; (no `C` row — a clean step is a no-op, still awaiting) |
73
- | `review-deciding` | check | script | `A/M .gtd/TODO.md` → `grilling`; `D .gtd/REVIEW.md` → `idle`; `C` → `await-review` (defensive) |
74
-
75
- **Loop 1 — TODO.md questions (maps the LSP's question symbols/format):**
76
-
77
- - `grilling` develops `.gtd/TODO.md` with the exact Open Questions format; if
78
- `.gtd/FORMAT.md` exists, its prompt says to fix those findings first.
79
- - `todo-validating` (deterministic script): parse-check TODO.md against §1's
80
- rules. Malformed → write findings to `.gtd/FORMAT.md` (one line each) → back
81
- to `grilling`. Valid → `rm -f .gtd/FORMAT.md` → `grilling-answer`.
82
- - `grilling-answer` (human): answer by editing a question's body in place, or
83
- leave a suggestion untouched to accept it; accept all remaining suggestions
84
- with a clean step (`C` → `building`); any edit loops back to `grilling` (the
85
- agent folds the whole answered batch into the plan, moving each resolved block
86
- down to `## Answered Questions`, possibly asks follow-ups, and re-validates).
87
-
88
- **Loop 2 — REVIEW.md checkboxes (maps the LSP's review chunks/check actions):**
89
-
90
- - Green `checking` now goes to `reviewing` (not straight to `await-review`).
91
- - `reviewing` (agent, `model: smart`) writes `.gtd/REVIEW.md` in §1's exact
92
- format — header hash from `<%= it.currentCommit %>` (short slice), base
93
- comment from `<%= it.startCommit %>`, chunks grouped semantically over the
94
- embedded `it.processDiff` (reuse the advanced example's reviewing prompt as
95
- the base, tightened to the format spec).
96
- - `review-validating` (deterministic script): structure check (header, base
97
- comment, ≥1 chunk, every pointer matches the shape). Malformed →
98
- `.gtd/FORMAT.md` → `reviewing`; valid → `await-review`.
99
- - `await-review` (human): tick a pointer's checkbox to approve that item; when
100
- EVERY box is ticked the cycle is approved. Add notes / untick / edit code for
101
- feedback. Deleting `.gtd/REVIEW.md` outright is the power-user approve
102
- shortcut. Any `M .gtd/REVIEW.md` step routes to the decider (declared before
103
- `* **`, so a step that also touches code still goes to the decider); code-only
104
- edits are feedback straight to `grilling`.
105
- - `review-deciding` (deterministic script): if NO unticked `- [ ]` pointer
106
- remains → approve: `rm .gtd/REVIEW.md` (→ `idle`). Otherwise → extract the
107
- unticked pointers and their notes into a fresh `.gtd/TODO.md` (the next lap's
108
- input) AND `rm .gtd/REVIEW.md` — the diff then contains both `A .gtd/TODO.md`
109
- and `D .gtd/REVIEW.md`, and the `A/M .gtd/TODO.md` row is declared FIRST so
110
- feedback wins (first-match declaration order is load-bearing here; pin it with
111
- a test).
112
-
113
- **Hygiene invariant (assert in e2e):** an approved cycle leaves `.gtd/` empty —
114
- FEEDBACK.md is cleaned by green `checking`, FORMAT.md by a valid validation run,
115
- REVIEW.md by the approval, TODO.md by `building`.
116
-
117
- ## 3. What the validators look like
118
-
119
- Same discipline as `checking`/the old `picking`: mechanics-only bash, verdict =
120
- file op, semantics = the `on` map. Grep/awk ports of the parser rules
121
- (pragmatic, not a full markdown parser): e.g. a `###` question heading with no
122
- question text is a finding; a pointer line failing the
123
- `- [(x| )] ./path(#N)?( — note)?` shape is a finding; `- [ ]` presence is the
124
- review-deciding branch condition. Findings go into `.gtd/FORMAT.md` verbatim
125
- (file + line + rule), so the fixing agent gets actionable input.
126
-
127
- ## 4. Change chart
128
-
129
- | Area | Change |
130
- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
131
- | `src/workflows/default.yaml` | +5 states (`todo-validating`, `grilling-answer`, `reviewing`, `review-validating`, `review-deciding`); `checking`'s green rows retarget `reviewing`; `await-review` rewritten (checkbox semantics, decider routing); prompts gain the format specs; header comment updated. |
132
- | Engine / TypeScript | **No changes.** Everything is workflow data. |
133
- | `STATES.md` §10 | New table + walkthrough (both loops, the declaration-order subtleties, the hygiene invariant). |
134
- | `tests/integration/features/default-workflow.feature` | Rewritten: malformed-TODO lap (simulate `todo-validating` by writing `.gtd/FORMAT.md` + `gtd step check` — @inmem never executes scripts), valid draft → answer lap → accept; malformed-REVIEW lap; tick-all approve → `idle` with empty `.gtd/`; partial-tick feedback → TODO.md extraction → `grilling`; delete-shortcut approve; retry/escalation kept. |
135
- | `smoke.feature` | Hop updates (`grilling` now routes to `todo-validating`). |
136
- | Docs | `configuration.md` / `README.md` walkthrough mentions; the advanced flow is now part of the bundled unified template (see STATES.md §10) rather than a standalone example. |
137
- | Live verification | Real-script drive (`gtd run` at each validator) covering: agent writes malformed TODO → FORMAT.md → fixed lap; answers folded; malformed REVIEW lap; tick-all approve; partial-tick feedback extracting TODO.md; final tree `.gtd`-free at idle. |
138
-
139
- ## 5. The LSP server comes back (v3-shaped)
140
-
141
- The v2 LSP (`src/Lsp.ts`, deleted at the v3 rewrite) is resurrected, scoped to
142
- what is workflow-agnostic — the FILE FORMATS, not any particular state machine:
143
-
144
- - **Restore the pure parsers verbatim from history** —
145
- `git show 60c7490^:src/OpenQuestions.ts` / `:src/ReviewDoc.ts` (+ their unit
146
- tests) — they are pure (no git/fs/Effect) and restore cleanly. They are the
147
- executable spec of §1; the bash validators in §3 implement the SAME rules
148
- (note this dual-implementation contract in both places — the parsers' unit
149
- tests are the format's spec tests).
150
- - **New `src/Lsp.ts`** (recover from history, then STRIP the v2-model
151
- dependencies — Events/Machine/ReviewWindow/STATE_FILE are gone): keyed on FILE
152
- NAME, not state. For `.gtd/TODO.md`: document symbols for open questions
153
- ([open]/[answered]). For `.gtd/REVIEW.md`: chunk/hunk symbols and the
154
- check/uncheck code actions (hunk and whole-chunk). BOTH files: publish the
155
- parsers' `errors` as diagnostics — the same findings the workflow's
156
- `.gtd/FORMAT.md` validators produce, live in the editor. The v2
157
- `gtd.openSteeringFile` command and its hardcoded state→file map are NOT
158
- restored (a v3 workflow is arbitrary data; a state names no file) — noted as a
159
- possible future addition driven by workflow config.
160
- - **CLI**: re-add the `gtd lsp` subcommand (stdio transport, as before) to
161
- `program.ts` dispatch + help + `docs/cli.md`; re-add the
162
- `vscode-languageserver` / `vscode-languageserver-textdocument` dependencies;
163
- confirm the tsdown single-file bundle still builds and the subcommand starts
164
- from `dist/gtd.bundle.mjs`.
165
- - **Tests**: parser unit tests restored; the LSP's pure helpers
166
- (symbol/edit/diagnostic builders) unit-tested as in v2 (recover `Lsp.test.ts`
167
- from history and adapt); restore/adapt the old protocol-level e2e
168
- (`lsp.steps.ts` + feature) if it adapts cleanly, otherwise a minimal @live
169
- smoke that `gtd lsp` starts and answers `initialize`.
170
- - **Housekeeping**: add the restored pure modules to `stryker.config.json`'s
171
- mutate list; `docs/development.md` + `docs/upgrading.md` notes (the LSP is
172
- back, file-format-keyed); README one-liner on editor integration.
173
-
174
- ## 6. Explicitly out of scope
175
-
176
- - `ARCHITECTURE.md` (the old second question phase) — the default keeps a single
177
- grilling step; the advanced example still shows the two-phase shape.
178
- - Engine-level validation of steering files — validation stays a workflow
179
- concern, per the v3 charter; the LSP is editor tooling over the same formats,
180
- not an engine hook.
181
- - The `gtd.openSteeringFile` command (needs a state→file mapping that v3
182
- workflows don't declare — future work, possibly a per-state `file:` hint).