@pmelab/gtd 3.0.0 → 3.2.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/dist/gtd.bundle.mjs +427 -26
- package/docs/cli.md +44 -2
- package/docs/configuration.md +58 -0
- package/docs/design/state-machine-validation.md +437 -0
- package/docs/upgrading.md +8 -2
- package/package.json +1 -1
- package/schema.json +103 -4
package/docs/cli.md
CHANGED
|
@@ -13,6 +13,8 @@ Commands:
|
|
|
13
13
|
actor (the built-in script driver)
|
|
14
14
|
status Print the resolved rest's state/actor and which declared
|
|
15
15
|
pattern (if any) each pending change matches (no mutation)
|
|
16
|
+
mermaid Print the active workflow's shape as Mermaid
|
|
17
|
+
stateDiagram-v2 source (no mutation)
|
|
16
18
|
format <file> Format a markdown file in place
|
|
17
19
|
lsp Start the LSP server for .gtd/ steering files (stdio)
|
|
18
20
|
|
|
@@ -175,6 +177,46 @@ never `null`, otherwise):
|
|
|
175
177
|
|
|
176
178
|
`gtd status` takes no arguments — extra positional args are rejected.
|
|
177
179
|
|
|
180
|
+
## `gtd mermaid`
|
|
181
|
+
|
|
182
|
+
Pure emitter of the active workflow's **shape** — not the resolved rest — as
|
|
183
|
+
Mermaid [`stateDiagram-v2`](https://mermaid.js.org/syntax/stateDiagram.html)
|
|
184
|
+
source (see `src/Mermaid.ts`): one node per declared state, the `[*] -->`
|
|
185
|
+
initial-state marker, one edge per declared `on` row labeled with its raw
|
|
186
|
+
pattern string (same declaration order the engine itself evaluates), a `--> [*]`
|
|
187
|
+
edge for every commit state (final, no outgoing edges — see
|
|
188
|
+
[STATES.md §8](../STATES.md#8-the-squash-lifecycle)), and one `note right of`
|
|
189
|
+
per rest naming its actor, content kind, and retry cap (e.g.
|
|
190
|
+
`agent · prompt · retry 3→escalate`). No git, no HEAD resolution, no template
|
|
191
|
+
rendering — purely a function of the compiled `WorkflowDefinition`, so its
|
|
192
|
+
output is identical regardless of the current process/branch state.
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
$ gtd mermaid
|
|
196
|
+
stateDiagram-v2
|
|
197
|
+
state "idle" as idle
|
|
198
|
+
state "grilling" as grilling
|
|
199
|
+
...
|
|
200
|
+
[*] --> idle
|
|
201
|
+
idle --> grilling : * **
|
|
202
|
+
...
|
|
203
|
+
note right of idle : human · message
|
|
204
|
+
...
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Pipe it straight into a `.md`/`.mmd` file, a GitHub issue/PR description, or any
|
|
208
|
+
Mermaid-aware renderer (GitHub, GitLab, VS Code, Obsidian, the
|
|
209
|
+
[Mermaid Live Editor](https://mermaid.live)) to get a diagram of a custom
|
|
210
|
+
`.gtdrc` `workflow:` with no hand-maintained docs required.
|
|
211
|
+
|
|
212
|
+
State names are aliased to Mermaid-safe identifiers (non-word characters fold to
|
|
213
|
+
`_`; a digit-led name gets an `s_` prefix) via a `state "<name>" as <alias>`
|
|
214
|
+
declaration up front, so a hyphenated name like `todo-validating` still displays
|
|
215
|
+
with its exact declared spelling. Rejects `--json` (exit 1,
|
|
216
|
+
`gtd mermaid does not accept --json`) — there is no structured shape to emit
|
|
217
|
+
beyond the Mermaid source itself — and takes no arguments (extra positional args
|
|
218
|
+
are rejected).
|
|
219
|
+
|
|
178
220
|
## `gtd format <file>`
|
|
179
221
|
|
|
180
222
|
Formats a markdown file in place with a bundled prettier (`parser: "markdown"`,
|
|
@@ -249,7 +291,7 @@ the plain-text one.
|
|
|
249
291
|
green run, and the check's `"C"` pattern never fires. Gitignore every path
|
|
250
292
|
your scripts write before wiring gtd into a repo.
|
|
251
293
|
- **Repository root invocation.** Every state subcommand (`step`/`next`/`run`/
|
|
252
|
-
`status`) must run from the git repository root — the workflow,
|
|
253
|
-
changes, and process history are resolved against the process cwd.
|
|
294
|
+
`status`/`mermaid`) must run from the git repository root — the workflow,
|
|
295
|
+
pending changes, and process history are resolved against the process cwd.
|
|
254
296
|
`--help`/`--version`, `format`, and `lsp` skip this guard entirely (and any
|
|
255
297
|
git/`.gtdrc` dependency along with it).
|
package/docs/configuration.md
CHANGED
|
@@ -172,6 +172,49 @@ change to that path MEANS keeps matching the old literal path. The vars are a
|
|
|
172
172
|
DRY mechanism inside templates and the state↔file association, not a rename
|
|
173
173
|
switch. (Making pattern keys var-aware at compile time is possible future work.)
|
|
174
174
|
|
|
175
|
+
### `reviewWindow:`/`reviewBase:` — the review checkout window
|
|
176
|
+
|
|
177
|
+
A state may declare `reviewWindow: true`. While the machine RESTS there, gtd
|
|
178
|
+
opens a **review checkout window**: it rewinds HEAD and the index to the review
|
|
179
|
+
base (`git reset --mixed`) with the working tree untouched, so the whole
|
|
180
|
+
`base..HEAD` diff surfaces as ordinary uncommitted changes in the editor's git
|
|
181
|
+
integration (SCM panel, gutters, per-file diff, discard-hunk). The window closes
|
|
182
|
+
automatically on the next invocation, once the machine rests anywhere else — and
|
|
183
|
+
a reviewer's own edits, made while it was open, become the resting state's
|
|
184
|
+
ordinary pending changes, captured by its `on` patterns like any other diff.
|
|
185
|
+
Forbidden on a commit state (never at rest).
|
|
186
|
+
|
|
187
|
+
The diff base defaults to the current process's start. To narrow it, mark an
|
|
188
|
+
earlier state `reviewBase: true`: the most-recent in-process commit that entered
|
|
189
|
+
such a state becomes the base, so only work committed after that milestone
|
|
190
|
+
surfaces.
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
workflow:
|
|
194
|
+
states:
|
|
195
|
+
# …
|
|
196
|
+
building:
|
|
197
|
+
actor: agent
|
|
198
|
+
reviewBase: true # the diff shown at review starts here…
|
|
199
|
+
prompt: implement the plan
|
|
200
|
+
on:
|
|
201
|
+
"* **": review
|
|
202
|
+
review:
|
|
203
|
+
actor: human
|
|
204
|
+
reviewWindow: true # …and is surfaced while resting here
|
|
205
|
+
message: review the diff in your editor, then `gtd step human`
|
|
206
|
+
on:
|
|
207
|
+
"C": done
|
|
208
|
+
"* **": building
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The bundled default enables `reviewWindow: true` on `await-review` (no
|
|
212
|
+
`reviewBase` state, so the base is the whole cycle). The pure engine never
|
|
213
|
+
observes an open window — it is opened/closed entirely at the edge; the real
|
|
214
|
+
head is preserved under `refs/gtd/review-head` (the base under
|
|
215
|
+
`refs/gtd/review-base`) for the window's lifetime. See
|
|
216
|
+
[STATES.md §11](../STATES.md) for the full lifecycle.
|
|
217
|
+
|
|
175
218
|
### Template variables
|
|
176
219
|
|
|
177
220
|
Every `script`/`prompt`/`message`/`commit`/`model` template is rendered as an
|
|
@@ -261,6 +304,21 @@ workflow config:
|
|
|
261
304
|
- state "idle": "on" target "nowhere" is not a defined state
|
|
262
305
|
```
|
|
263
306
|
|
|
307
|
+
`validateDefinition`'s findings include the **semantic graph checks**: every
|
|
308
|
+
`on` target and `retry.otherwise` must name a defined state, and every state
|
|
309
|
+
must be **reachable** from the initial state by walking `on` targets and
|
|
310
|
+
`retry.otherwise` redirects — an unreachable state (typically a typo'd rename or
|
|
311
|
+
a leftover) is a load error like any other, since a workflow is bound to a
|
|
312
|
+
project and edited as a project-wide change.
|
|
313
|
+
|
|
314
|
+
Many of these problems never reach gtd at all if your editor validates against
|
|
315
|
+
the published schema: `schema.json` fully types the `workflow:` key (state
|
|
316
|
+
shape, content kinds, `on`/`retry` structure, the `mode` vocabulary), so a
|
|
317
|
+
yaml-language-server-style editor flags unknown keys and wrong types as you
|
|
318
|
+
type. The rules JSON Schema cannot express — exactly one content kind, exactly
|
|
319
|
+
one `initial: true`, targets naming defined states, reachability — remain the
|
|
320
|
+
compiler's job at load time.
|
|
321
|
+
|
|
264
322
|
Other load failures:
|
|
265
323
|
|
|
266
324
|
- **Parse errors** (malformed YAML/JSON) — message includes the offending
|
|
@@ -0,0 +1,437 @@
|
|
|
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/FORMAT.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_VAR_*` env vars)
|
|
194
|
+
can legitimately supply a name at run time, an unknown name is a **warning**,
|
|
195
|
+
not an error. (A full "render with a tracking proxy and record misses" approach
|
|
196
|
+
also 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 `default.yaml` and `docs/examples/advanced-workflow.md`
|
|
310
|
+
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/FORMAT.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.
|
package/docs/upgrading.md
CHANGED
|
@@ -49,8 +49,14 @@ upgrading.
|
|
|
49
49
|
tiering — a workflow author is free to build any of that shape back with
|
|
50
50
|
states of their own, but gtd no longer bakes it in.
|
|
51
51
|
- **The review checkout window.** v2 rewound HEAD/index to the review base while
|
|
52
|
-
a human review rested, so editors would show the diff directly.
|
|
53
|
-
|
|
52
|
+
a human review rested, so editors would show the diff directly. Dropped in the
|
|
53
|
+
v3.0 rewrite, then RE-INTRODUCED as the explicit state property it was always
|
|
54
|
+
meant to become: a state declaring `reviewWindow: true` opens the window while
|
|
55
|
+
the machine rests there (base = the process start, or a state marked
|
|
56
|
+
`reviewBase: true`). The bundled default enables it on `await-review`. Unlike
|
|
57
|
+
v2's hardwired gate, it is pure workflow DATA and the engine stays oblivious —
|
|
58
|
+
see [STATES.md §11](../STATES.md) and the `file:`/`mode:` neighbours in
|
|
59
|
+
[configuration.md](configuration.md).
|
|
54
60
|
- **`forceApprove`, content-inspection verdicts.** FEEDBACK.md emptiness,
|
|
55
61
|
checkbox-only REVIEW.md diffs, and doc-structure validation are gone as ENGINE
|
|
56
62
|
mechanisms — verdicts are now expressed purely by which file a turn writes or
|