@pmelab/gtd 7.11.2 → 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.
- package/README.md +409 -130
- package/bin/gtd +59 -287
- package/dist/gtd.bundle.mjs +187 -98
- package/package.json +1 -2
- package/schema.json +33 -9
- package/docs/cli.md +0 -747
- package/docs/configuration.md +0 -994
- package/docs/design/configurable-state-machine.md +0 -1439
- package/docs/design/delta-purity-plan.md +0 -283
- package/docs/design/pattern-machine-plan.md +0 -164
- package/docs/design/pluggable-steering-modes.md +0 -154
- package/docs/design/scripted-actor.md +0 -289
- package/docs/design/state-file-association.md +0 -120
- package/docs/design/state-machine-validation.md +0 -437
- package/docs/design/steering-file-loops.md +0 -182
- package/docs/design/steering-file-validation-command.md +0 -385
- package/docs/design/work-packages.md +0 -193
- package/docs/development.md +0 -109
- package/docs/loop.md +0 -289
- package/docs/upgrading.md +0 -293
|
@@ -1,1439 +0,0 @@
|
|
|
1
|
-
# Design exploration: a fully configurable state machine
|
|
2
|
-
|
|
3
|
-
> **Historical design record.** The machinery described here (the v2 model —
|
|
4
|
-
> gates, guard functions, actor kinds, conditions) was deleted by the v3
|
|
5
|
-
> rewrite; superseded by `docs/design/pattern-machine-plan.md` and
|
|
6
|
-
> [STATES.md](../../STATES.md).
|
|
7
|
-
|
|
8
|
-
> Status: exploration, not a commitment. This document maps the building blocks
|
|
9
|
-
> a configuration-driven workflow engine would need so that the current v2
|
|
10
|
-
> machine becomes nothing more than the **default configuration**, and a user
|
|
11
|
-
> can define their own states, edges, conditions, and human gates.
|
|
12
|
-
|
|
13
|
-
## 1. What "the state machine" actually is today
|
|
14
|
-
|
|
15
|
-
Before designing a configurable version, it's worth being precise about what
|
|
16
|
-
would be configured. gtd's machine is **not** a classic FSM with a stored state
|
|
17
|
-
pointer. It is a pure re-derivation:
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
state = resolve( parse(first-parent history) + snapshot(working tree) + config )
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Nothing is persisted except ordinary git commits; every invocation re-derives
|
|
24
|
-
the entire decision from scratch. That property buys crash-safety, human
|
|
25
|
-
tamper-tolerance, and v1 backward compatibility — and any configurable design
|
|
26
|
-
must preserve it, or it isn't the same product.
|
|
27
|
-
|
|
28
|
-
Concretely, the engine has three layers:
|
|
29
|
-
|
|
30
|
-
| Layer | Where | What it does |
|
|
31
|
-
| -------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
32
|
-
| **Fact gathering** | `src/Events.ts` | IO edge: reads git history + working tree into `CommitEvent[]` flags and a `ResolvePayload` fact snapshot |
|
|
33
|
-
| **Pure resolution** | `src/Machine.ts` | folds counters, checks illegal combinations, classifies HEAD (rest vs mid-chain), applies the precedence ladder, layers turn-taking |
|
|
34
|
-
| **Effect execution** | `src/Events.ts` | `perform(EdgeAction)`: a closed set of side-effect opcodes (capture turn, routing commit, run tests, squash, …) |
|
|
35
|
-
|
|
36
|
-
The observation that makes configurability tractable: **most of the middle layer
|
|
37
|
-
is already data, not code.** `counterRules`, the illegal-combination rule lists,
|
|
38
|
-
`ROUTING_SUBJECT`, `TURN_GATES`, `INERT_EMPTY_AGENT_GATES`, `GATE_OVERRIDES`,
|
|
39
|
-
`STATE_TEMPLATE`, `MODEL_STATE`, and the entry-point pick order are all literal
|
|
40
|
-
tables today. `classifyHead` is a hand-unrolled table. What remains as genuine
|
|
41
|
-
code is a handful of precedence exceptions and the turn-taking layer. A
|
|
42
|
-
configurable machine is therefore less "rewrite the engine" and more "promote
|
|
43
|
-
the existing tables into a compiled artifact of a user-facing schema, and find
|
|
44
|
-
declarative homes for the ~20% that isn't table-shaped yet."
|
|
45
|
-
|
|
46
|
-
## 2. The shape of the proposal
|
|
47
|
-
|
|
48
|
-
Users author a **workflow definition** (states + edges + conditions, statechart
|
|
49
|
-
style). A **compile step** turns it into the internal tables the engine already
|
|
50
|
-
runs on:
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
workflow definition (user config)
|
|
54
|
-
│ compile (validate totality, ambiguity, reachability)
|
|
55
|
-
▼
|
|
56
|
-
┌─────────────────────────────────────────────────────────┐
|
|
57
|
-
│ generated subject grammar (turn gates, routing phases) │
|
|
58
|
-
│ head-classification table (classifyHead rows) │
|
|
59
|
-
│ interrupt/precedence rules (steering-file ladder) │
|
|
60
|
-
│ counter fold rules (counterRules) │
|
|
61
|
-
│ illegal-combination rules (assertLegal) │
|
|
62
|
-
│ prompt/model bindings (STATE_TEMPLATE, MODEL_STATE)│
|
|
63
|
-
│ turn-taking parameters (empty-turn policies, entry) │
|
|
64
|
-
└─────────────────────────────────────────────────────────┘
|
|
65
|
-
▼
|
|
66
|
-
same pure resolve() interpreter, now table-driven end to end
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
The commit log stays the only persistence. Turn and routing subjects are
|
|
70
|
-
**generated from the configured state/edge names**, and the boundary rule is
|
|
71
|
-
preserved: any subject outside the configured closed grammar is an inert
|
|
72
|
-
boundary commit. This is load-bearing (see §7 on config-change hazards) — it is
|
|
73
|
-
the same mechanism that already makes v1 history inert under v2.
|
|
74
|
-
|
|
75
|
-
Two design stances worth naming and rejecting:
|
|
76
|
-
|
|
77
|
-
- **Generic statechart runtime (XState-style) with a persisted state pointer.**
|
|
78
|
-
Simple to configure, but abandons derive-from-history: crash recovery, human
|
|
79
|
-
edits landing as ordinary commits, and `gtd next` as a pure query all break.
|
|
80
|
-
Rejected.
|
|
81
|
-
- **Arbitrary user-defined effects (scripts as actions).** Turns the workflow
|
|
82
|
-
into a CI system and destroys the safety analysis (an action's effect on the
|
|
83
|
-
next resolution becomes unknowable). Rejected in favor of a **closed primitive
|
|
84
|
-
toolbox** (§3.7) — like GitHub Actions, where the _graph_ is yours but the
|
|
85
|
-
_runner semantics_ are built in. Custom behavior enters through prompts,
|
|
86
|
-
templates, commands (`testCommand`-style strings), and artifacts — not through
|
|
87
|
-
new opcodes.
|
|
88
|
-
|
|
89
|
-
## 3. The building blocks
|
|
90
|
-
|
|
91
|
-
Eleven blocks. Each lists what it generalizes from the current machine.
|
|
92
|
-
|
|
93
|
-
### 3.1 Actors
|
|
94
|
-
|
|
95
|
-
The turn-taking universe. Today: `human` and `agent`, hard-coded into the
|
|
96
|
-
subject grammar `gtd(<actor>): <gate>`, the invoker commands (`gtd step`,
|
|
97
|
-
`gtd step-agent`), and the refusal logic.
|
|
98
|
-
|
|
99
|
-
```yaml
|
|
100
|
-
actors:
|
|
101
|
-
human: { kind: interactive } # gtd step
|
|
102
|
-
agent: { kind: autonomous } # gtd step-agent
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Keep the set fixed at these two for v1 of configurability. The building block
|
|
106
|
-
exists so a later version can add named roles (e.g. a second autonomous
|
|
107
|
-
`reviewer` actor invoked as `gtd step-agent --as reviewer`), but nothing in the
|
|
108
|
-
current functionality needs it, and multi-actor turn-taking multiplies the
|
|
109
|
-
refusal/fixpoint matrix.
|
|
110
|
-
|
|
111
|
-
### 3.2 Artifacts (steering files)
|
|
112
|
-
|
|
113
|
-
The single richest block. Every steering file becomes a declared **artifact**,
|
|
114
|
-
and almost everything the machine currently knows about files falls out of the
|
|
115
|
-
declaration:
|
|
116
|
-
|
|
117
|
-
```yaml
|
|
118
|
-
artifacts:
|
|
119
|
-
todo:
|
|
120
|
-
path: .gtd/TODO.md
|
|
121
|
-
validator: open-questions # named, built-in validators (see below)
|
|
122
|
-
format: true # run `gtd format` on capture
|
|
123
|
-
architecture:
|
|
124
|
-
path: .gtd/ARCHITECTURE.md
|
|
125
|
-
validator: open-questions
|
|
126
|
-
format: true
|
|
127
|
-
plan:
|
|
128
|
-
path: .gtd/PLAN.md
|
|
129
|
-
entryOnly: true # may never coexist with anything else
|
|
130
|
-
review:
|
|
131
|
-
path: .gtd/REVIEW.md
|
|
132
|
-
validator: review-doc
|
|
133
|
-
probes: [checkbox-only-diff] # content probes this artifact opts into
|
|
134
|
-
feedback:
|
|
135
|
-
path: .gtd/FEEDBACK.md
|
|
136
|
-
probes: [empty]
|
|
137
|
-
consumedByTurn: fixing # precedence exception (see §3.6)
|
|
138
|
-
errors:
|
|
139
|
-
path: .gtd/ERRORS.md
|
|
140
|
-
health:
|
|
141
|
-
path: .gtd/HEALTH.md
|
|
142
|
-
consumedByTurn: health-fixing
|
|
143
|
-
squashMsg:
|
|
144
|
-
path: .gtd/SQUASH_MSG.md
|
|
145
|
-
template: templates/squash-msg.md # written by write-template; probe
|
|
146
|
-
probes: [template-unmodified] # `template-unmodified` guards use it
|
|
147
|
-
learnings:
|
|
148
|
-
path: .gtd/LEARNINGS.md
|
|
149
|
-
template: templates/learnings.md
|
|
150
|
-
probes: [template-unmodified]
|
|
151
|
-
|
|
152
|
-
packages: # the one non-file artifact KIND
|
|
153
|
-
kind: package-set
|
|
154
|
-
path: .gtd/
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Each declared artifact automatically contributes **facts** to the guard
|
|
158
|
-
vocabulary (§3.4): `todo.exists`, `todo.committed`, `feedback.present`,
|
|
159
|
-
`feedback.empty`, `feedback.pendingDeletion`, `review.checkboxOnly`,
|
|
160
|
-
`squashMsg.isTemplate`, `packages.present`, `packages.modified`,
|
|
161
|
-
`packages.remaining`, … — exactly the `ResolvePayload` fields that exist today,
|
|
162
|
-
but _generated per artifact_ instead of hand-listed.
|
|
163
|
-
|
|
164
|
-
Three deliberately closed sub-vocabularies live here:
|
|
165
|
-
|
|
166
|
-
- **Content probes** — the machine inspects file content in exactly three ways
|
|
167
|
-
today (FEEDBACK.md whitespace-emptiness, REVIEW.md checkbox-only diffs,
|
|
168
|
-
template-unmodified checks). These stay a closed, named set. No user
|
|
169
|
-
expression language ever reads file content; "file presence steers, content
|
|
170
|
-
doesn't" survives as a design invariant with three opt-in exceptions.
|
|
171
|
-
- **Validators** — `open-questions` and `review-doc` are named built-ins (backed
|
|
172
|
-
by `src/OpenQuestions.ts` / `src/ReviewDoc.ts`). A plausible later extension
|
|
173
|
-
is a small declarative markdown-structure schema ("required `##` section",
|
|
174
|
-
"first body line matches …"), but named validators reproduce today's behavior.
|
|
175
|
-
- **`package-set`** — packages are not a file; they are an ordered directory
|
|
176
|
-
protocol (`01-*/`, task files, close-lowest-first) with their own facts and
|
|
177
|
-
their own effect primitive (`close-package`). Modeling them as a special
|
|
178
|
-
artifact _kind_ keeps the machine honest about that instead of pretending a
|
|
179
|
-
directory tree is a boolean.
|
|
180
|
-
|
|
181
|
-
Mutual-exclusion ("illegal combinations") is declared as conflict sets, not 30
|
|
182
|
-
hand-written pairs — the compiler expands `entryOnly` and explicit `conflicts`
|
|
183
|
-
groups into the pairwise rule list `assertLegal` runs today:
|
|
184
|
-
|
|
185
|
-
```yaml
|
|
186
|
-
conflicts:
|
|
187
|
-
- [health, packages, review, feedback, errors, todo, architecture, plan] # health vs world
|
|
188
|
-
- [todo, architecture] # lifecycle stages of one document
|
|
189
|
-
- [feedback, review]
|
|
190
|
-
- requires: { feedback: packages } # FEEDBACK.md without packages → error
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### 3.3 Commit-event flags and counters
|
|
194
|
-
|
|
195
|
-
`gatherEvents` currently derives six flags per commit (`isErrors`, `isFeedback`,
|
|
196
|
-
`isPackageStart`, `removedErrors`, `isHealthCheck`, `isTestsGreen`), and
|
|
197
|
-
`foldCounters` folds three counters from them with declarative reset/increment
|
|
198
|
-
rules. Both generalize cleanly:
|
|
199
|
-
|
|
200
|
-
```yaml
|
|
201
|
-
commitFlags:
|
|
202
|
-
packageStart: { routing: [planning, package-done] }
|
|
203
|
-
feedbackRound:
|
|
204
|
-
{ turn: { actor: agent, gate: agentic-review }, diffTouches: feedback }
|
|
205
|
-
errorsRound: { routing: [errors] }
|
|
206
|
-
removedErrors: { diffDeletes: errors }
|
|
207
|
-
healthRound: { routing: [health-check] }
|
|
208
|
-
testsGreen: { routing: [tests-green] }
|
|
209
|
-
|
|
210
|
-
counters:
|
|
211
|
-
testFix:
|
|
212
|
-
{
|
|
213
|
-
resetOn: [packageStart, feedbackRound, removedErrors],
|
|
214
|
-
incrementOn: [errorsRound],
|
|
215
|
-
}
|
|
216
|
-
reviewFix: { resetOn: [packageStart], incrementOn: [feedbackRound] }
|
|
217
|
-
healthFix:
|
|
218
|
-
{
|
|
219
|
-
resetOn: [packageStart, removedErrors, testsGreen],
|
|
220
|
-
incrementOn: [healthRound],
|
|
221
|
-
}
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
A commit-flag predicate needs exactly three matchers to cover everything that
|
|
225
|
-
exists: _subject is routing phase X_, _subject is turn (actor, gate)_, and _diff
|
|
226
|
-
touches / deletes artifact Y_. `counterRules` in `Machine.ts` already has
|
|
227
|
-
precisely this shape — this block is a rename, not an invention. User- defined
|
|
228
|
-
counters plus `counterAtLeast` guards (§3.4) are what make custom
|
|
229
|
-
budget/threshold loops ("escalate to a human after N rounds of X") expressible
|
|
230
|
-
without code.
|
|
231
|
-
|
|
232
|
-
### 3.4 Facts and guard expressions
|
|
233
|
-
|
|
234
|
-
The guard vocabulary is the union of:
|
|
235
|
-
|
|
236
|
-
- artifact-derived facts (§3.2),
|
|
237
|
-
- built-in tree/head facts: `tree.clean`, `code.dirty`, `head.turnEmpty`,
|
|
238
|
-
`head.isTurn(actor, gate)`, `head.isRouting(phase)`, `head.isBoundary`,
|
|
239
|
-
`reviewable` (base + non-empty diff + commits since last done),
|
|
240
|
-
- counters: `counter(name)`,
|
|
241
|
-
- config parameters: `param(name)` — the `agenticReview` / `fixAttemptCap` /
|
|
242
|
-
`reviewThreshold` / `squash` / `learning` values, which stay ordinary scalar
|
|
243
|
-
config and travel to the resolver exactly as they do today (payload fields,
|
|
244
|
-
per AGENTS.md's config-vs-mode-flag rule).
|
|
245
|
-
|
|
246
|
-
Guards are a small structured boolean AST — JSON/YAML, not an embedded
|
|
247
|
-
expression string, in keeping with schema-validated config and mutation-tested
|
|
248
|
-
code:
|
|
249
|
-
|
|
250
|
-
```yaml
|
|
251
|
-
when:
|
|
252
|
-
{
|
|
253
|
-
all: [{ not: { param: agenticReview } }],
|
|
254
|
-
any: [{ counterAtLeast: { counter: reviewFix, param: reviewThreshold } }],
|
|
255
|
-
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
(Sugar for common forms — `when: feedback.empty` — is a parser nicety, not a
|
|
259
|
-
semantic feature.) The force-approve rule, today a code comment thicket, becomes
|
|
260
|
-
one named, reusable guard:
|
|
261
|
-
|
|
262
|
-
```yaml
|
|
263
|
-
guards:
|
|
264
|
-
forceApprove:
|
|
265
|
-
any:
|
|
266
|
-
- { not: { param: agenticReview } }
|
|
267
|
-
- { counterAtLeast: { counter: reviewFix, param: reviewThreshold } }
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
### 3.5 States
|
|
271
|
-
|
|
272
|
-
```yaml
|
|
273
|
-
states:
|
|
274
|
-
grilling:
|
|
275
|
-
awaits: dynamic # resolved per-edge; grilling awaits human OR agent
|
|
276
|
-
prompts:
|
|
277
|
-
agent: prompts/grilling-agent.md
|
|
278
|
-
human: prompts/grilling-answers.md
|
|
279
|
-
model: planning # tier ref; per-state override still possible
|
|
280
|
-
emptyAgentTurn: inert # the INERT_EMPTY_AGENT_GATES membership
|
|
281
|
-
validateAgentTurn: open-questions # refuse agent capture on malformed doc
|
|
282
|
-
await-review:
|
|
283
|
-
awaits: human
|
|
284
|
-
prompts: { human: prompts/await-review.md }
|
|
285
|
-
gate: review # GATE_OVERRIDES: turn authored under another gate name
|
|
286
|
-
squashing:
|
|
287
|
-
awaits: agent
|
|
288
|
-
prompts: { agent: prompts/squashing.md }
|
|
289
|
-
model: planning
|
|
290
|
-
emptyAgentTurn: { inertWhen: squashMsg.isTemplate } # conditional inertness
|
|
291
|
-
health-fixing:
|
|
292
|
-
awaits: agent
|
|
293
|
-
emptyAgentTurn:
|
|
294
|
-
{ signal: true, inertWhen: { head.isTurn: [human, health-fixing] } }
|
|
295
|
-
testing: { kind: edge-only } # never a rest; never renders a prompt
|
|
296
|
-
close-package: { kind: edge-only }
|
|
297
|
-
idle:
|
|
298
|
-
awaits: human
|
|
299
|
-
prompts: { human: prompts/idle.md }
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
Everything `PROMPT_STATES`, `MODEL_STATE`, `STATE_IS_OWN_GATE`,
|
|
303
|
-
`GATE_OVERRIDES`, and `INERT_EMPTY_AGENT_GATES` encode today collapses into
|
|
304
|
-
per-state declarations. The **empty-turn policy** deserves emphasis: the current
|
|
305
|
-
machine's most subtle invariant (AGENTS.md "Turn Capture") is that each gate
|
|
306
|
-
explicitly decides whether a clean-tree capture is a signal or a no-op, guarded
|
|
307
|
-
at BOTH the capture layer and the classification layer. Making it a mandatory
|
|
308
|
-
per-state field — with `inertWhen` guards for the conditional cases — forces
|
|
309
|
-
every custom machine author to make that decision explicitly, and lets the
|
|
310
|
-
compiler enforce that both layers read the same declaration (they already share
|
|
311
|
-
`isInertEmptyAgentRest`; the config keeps them shared).
|
|
312
|
-
|
|
313
|
-
### 3.6 Edges (the transition rules)
|
|
314
|
-
|
|
315
|
-
Resolution today happens in three passes, and the config mirrors them as three
|
|
316
|
-
edge families. Order matters in each family and is preserved as written.
|
|
317
|
-
|
|
318
|
-
**(a) Interrupt rules** — the steering-file precedence ladder: fire on facts
|
|
319
|
-
regardless of HEAD, top priority first. Each rule may carry `unless` exceptions;
|
|
320
|
-
the common one ("the turn that consumes this artifact must not be pre-empted by
|
|
321
|
-
the artifact's own presence") is auto-generated from the artifact's
|
|
322
|
-
`consumedByTurn` declaration:
|
|
323
|
-
|
|
324
|
-
```yaml
|
|
325
|
-
interrupts:
|
|
326
|
-
- when: errors.present
|
|
327
|
-
rest: { state: escalate }
|
|
328
|
-
- when: health.present # unless auto-generated: head is gtd(agent): health-fixing
|
|
329
|
-
rest: { state: health-fixing }
|
|
330
|
-
- when: { any: [feedback.present, feedback.pendingDeletion] }
|
|
331
|
-
unless: { all: [guard: forceApprove, head.isRouting: tests-green] }
|
|
332
|
-
branch:
|
|
333
|
-
- when: { head.isRouting: tests-green } # uncaptured reviewer write
|
|
334
|
-
rest: { state: agentic-review }
|
|
335
|
-
- when: { all: [feedback.emptyEffective, { not: fact: inFixLoop }] }
|
|
336
|
-
chain: { state: close-package, action: close-package }
|
|
337
|
-
- rest: { state: fixing }
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
**(b) Head rules** — the `classifyHead` table: match the parsed HEAD subject,
|
|
341
|
-
branch on guards, land on a **rest** (a state + awaited actor) or a **chain** (a
|
|
342
|
-
state label + one effect the driver performs before re-resolving — "mid-chain"
|
|
343
|
-
today):
|
|
344
|
-
|
|
345
|
-
```yaml
|
|
346
|
-
onTurn:
|
|
347
|
-
- match: { actor: human, gate: grilling }
|
|
348
|
-
branch:
|
|
349
|
-
- when: head.turnEmpty
|
|
350
|
-
chain:
|
|
351
|
-
state: grilling
|
|
352
|
-
action: { route: architecting, seed: { from: todo, to: architecture, banner: seeded-from-todo } }
|
|
353
|
-
- rest: { state: grilling, awaits: agent }
|
|
354
|
-
- match: { actor: agent, gate: grilled }
|
|
355
|
-
branch:
|
|
356
|
-
- when: packages.present
|
|
357
|
-
chain: { state: grilled, action: { route: planning, remove: [architecture] } }
|
|
358
|
-
- rest: { state: grilled, awaits: agent } # guard: no packages → re-emit prompt
|
|
359
|
-
- match: { actor: human, gate: review }
|
|
360
|
-
branch:
|
|
361
|
-
- when: fact: reviewSubstantive
|
|
362
|
-
chain: { state: review, action: { route: review-feedback, remove: [review] } }
|
|
363
|
-
- chain: { state: review, action: { route: done, remove: [review] } }
|
|
364
|
-
|
|
365
|
-
onRouting:
|
|
366
|
-
- match: tests-green
|
|
367
|
-
branch:
|
|
368
|
-
- when: packages.present
|
|
369
|
-
branch:
|
|
370
|
-
- when: { guard: forceApprove }
|
|
371
|
-
chain: { state: close-package, action: close-package }
|
|
372
|
-
- rest: { state: agentic-review, awaits: agent }
|
|
373
|
-
- use: afterCycleSettles # named, reusable rule (learning → squash → idle)
|
|
374
|
-
- match: awaiting-review
|
|
375
|
-
rest: { state: await-review, awaits: human }
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
`use:` names a shared sub-rule — `nextAfterReviewOrLearning` in today's code
|
|
379
|
-
(the learning-then-squash-then-idle decision reached from three different
|
|
380
|
-
routing subjects) is exactly such a reusable fragment.
|
|
381
|
-
|
|
382
|
-
**(c) Fallback rules** — the boundary-HEAD ladder that runs when neither an
|
|
383
|
-
interrupt nor a head rule fired: `packages.modified → planning`,
|
|
384
|
-
`todo.exists → grilling`, `architecture.exists → architecting`,
|
|
385
|
-
`plan.exists → grilled (awaits human)`, the operational-recovery checkpoint
|
|
386
|
-
rule, `reviewable → review`, else `idle`, else **corruption error**. Same
|
|
387
|
-
ordered-rule shape as interrupts. The final "no rule matched → hard error, do
|
|
388
|
-
not guess" is engine behavior, not configurable — a custom machine keeps the
|
|
389
|
-
steering-misuse contract for free.
|
|
390
|
-
|
|
391
|
-
**Entry points** are a fourth, tiny rule list consumed by the turn-taking layer
|
|
392
|
-
(which artifact in a dirty boundary tree selects which entry gate):
|
|
393
|
-
|
|
394
|
-
```yaml
|
|
395
|
-
entry:
|
|
396
|
-
- { when: health.exists, gate: health-fixing }
|
|
397
|
-
- { when: plan.exists, gate: grilled }
|
|
398
|
-
- { when: architecture.exists, gate: architecting }
|
|
399
|
-
- { gate: grilling } # default
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
The compiler verifies the entry artifacts are pairwise-conflicting (§3.2), so
|
|
403
|
-
the pick is provably unambiguous — today that's a prose promise.
|
|
404
|
-
|
|
405
|
-
### 3.7 Effect primitives (the closed toolbox)
|
|
406
|
-
|
|
407
|
-
Config composes effects; it never defines new ones. The current eight
|
|
408
|
-
`EdgeAction` variants generalize into six parameterized opcodes:
|
|
409
|
-
|
|
410
|
-
| Primitive | Parameters | Generalizes |
|
|
411
|
-
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
412
|
-
| `capture-turn` | actor, gate (engine-chosen; not really config-facing) | `captureTurn` |
|
|
413
|
-
| `route` | routing phase; `remove: [artifact…]`; `seed: {from, to, banner}` | `commitRouting` + its five booleans |
|
|
414
|
-
| `run-check` | command ref; `green: route \| stop \| chain`; `red: {write: artifact, cap: {counter, param, write: artifact}, route}`; `removeFirst: [artifact…]` | `runTest` AND `runHealthCheck` |
|
|
415
|
-
| `write-template` | artifact (must declare `template:`); route | `writeSquashTemplate` / `writeLearningTemplate` |
|
|
416
|
-
| `squash` | base anchor (engine-computed cycle base); message-from artifact | `squashCommit` |
|
|
417
|
-
| `close-package` | (package-set artifact) | `closePackage` |
|
|
418
|
-
|
|
419
|
-
`run-check` is the significant unification: `runTest` and `runHealthCheck` are
|
|
420
|
-
already the same shape (run command → green routes / red writes an artifact,
|
|
421
|
-
escalating to a different artifact at a counter cap), differing only in which
|
|
422
|
-
artifacts and which follow-up. Parameterizing it also unlocks the most-wanted
|
|
423
|
-
custom machines for free: **multiple named checks** (lint gate, typecheck gate,
|
|
424
|
-
e2e gate as separate states with separate budgets and separate commands) — today
|
|
425
|
-
impossible without forking the code.
|
|
426
|
-
|
|
427
|
-
Commands are declared alongside `testCommand`:
|
|
428
|
-
|
|
429
|
-
```yaml
|
|
430
|
-
commands:
|
|
431
|
-
test: npm run test
|
|
432
|
-
lint: npm run lint # available to custom run-check edges
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
### 3.8 The turn-taking engine (fixed, parameterized — not configurable)
|
|
436
|
-
|
|
437
|
-
Everything in `applyTurnTaking` stays engine code: invoker-vs-awaited refusal in
|
|
438
|
-
both directions, the fixpoint (idempotent re-invocation), the dirty-boundary
|
|
439
|
-
entry capture, pending-human-edits-ride-along, and `invoker: "none"` purity. It
|
|
440
|
-
is _parameterized_ by config (empty-turn policies, entry rules, per-gate
|
|
441
|
-
validators) but its skeleton is the product's safety core — the analysis in
|
|
442
|
-
AGENTS.md about guarding both layers exists precisely because this logic is easy
|
|
443
|
-
to get fatally wrong. A user configures _which_ gates exist and _who_ they
|
|
444
|
-
await; they do not configure what turn-taking means.
|
|
445
|
-
|
|
446
|
-
Likewise the **review checkout window** stays wired only in `program.ts`
|
|
447
|
-
(program-edge concern, per the architecture rule); config can at most toggle it
|
|
448
|
-
per human gate (`checkoutWindow: true` on `await-review`).
|
|
449
|
-
|
|
450
|
-
### 3.9 Prompts and models
|
|
451
|
-
|
|
452
|
-
Already halfway configurable (model tiers/states in `.gtdrc`). The remaining
|
|
453
|
-
moves:
|
|
454
|
-
|
|
455
|
-
- Per-state template **paths** in config, resolving user files with fallback to
|
|
456
|
-
the built-ins (which ship as the default config's templates).
|
|
457
|
-
- A documented, versioned **view-model contract**: templates receive `context`
|
|
458
|
-
(counters, packages, diffs, feedback content, decision log — the
|
|
459
|
-
`ResolveContext` fields), `model`, `fenceFor`, and the shared partials
|
|
460
|
-
(`@header`, `@diff`, `@package`, `@agent-turn`, …) stay available.
|
|
461
|
-
- Model tiers become config-defined names (`planning`, `execution` today) that
|
|
462
|
-
states reference; the existing `models.states` override mechanism survives
|
|
463
|
-
unchanged.
|
|
464
|
-
|
|
465
|
-
Custom states then get prompts the same way built-in ones do; a state without a
|
|
466
|
-
prompt and without `kind: edge-only` is a compile error.
|
|
467
|
-
|
|
468
|
-
### 3.10 The subject grammar (generated, namespaced)
|
|
469
|
-
|
|
470
|
-
`TurnGate` and `RoutingPhase` closed sets are generated from the config: every
|
|
471
|
-
state with an actor gate contributes a turn subject; every `route:` target
|
|
472
|
-
contributes a routing subject. Two rules preserve today's guarantees:
|
|
473
|
-
|
|
474
|
-
- **Closed-world parsing**: `parseSubject` matches only the configured sets;
|
|
475
|
-
everything else is a boundary commit. v1 compatibility and "inert unknown
|
|
476
|
-
subjects" survive automatically — including subjects from a _previous
|
|
477
|
-
configuration_ (see §7).
|
|
478
|
-
- **Reserved namespace**: generated subjects keep the `gtd(actor): gate` /
|
|
479
|
-
`gtd: phase` shapes. The compiler rejects state/phase names that collide with
|
|
480
|
-
each other or with the parameterized `reviewing <hash>` anchor.
|
|
481
|
-
|
|
482
|
-
### 3.11 The compiler (static validation)
|
|
483
|
-
|
|
484
|
-
The piece that makes user-authored machines survivable. At load time (cached;
|
|
485
|
-
this runs on every invocation), validate:
|
|
486
|
-
|
|
487
|
-
- **Schema** — effect-schema decode with `onExcessProperty: "error"`, exactly
|
|
488
|
-
like `ConfigSchema` today; every referenced artifact/guard/counter/command/
|
|
489
|
-
state exists; no dangling `route:` targets.
|
|
490
|
-
- **Totality** — every routing phase emitted by some edge has an `onRouting`
|
|
491
|
-
classification; every rest state has a prompt for its awaited actor(s); every
|
|
492
|
-
state is reachable from an entry point; every `run-check` red path writes a
|
|
493
|
-
declared artifact.
|
|
494
|
-
- **Ambiguity** — interrupt/fallback rules are ordered (documented semantics),
|
|
495
|
-
but two `onTurn` matches for the same (actor, gate) with non-disjoint guard
|
|
496
|
-
branches get flagged.
|
|
497
|
-
- **Livelock bounds** — full progress proofs are out of reach, but two cheap
|
|
498
|
-
checks catch the classic traps: (1) every agent-awaited rest whose gate's
|
|
499
|
-
empty turn is inert must have _some_ dirty-tree path forward, and (2) every
|
|
500
|
-
counter used in a cap guard must have a reset flag reachable from the capped
|
|
501
|
-
loop. Plus a runtime chain-depth limit (the driver already loops gather →
|
|
502
|
-
resolve → perform; bound it and hard-error past, say, 32 hops).
|
|
503
|
-
- **Entry unambiguity** — entry-rule artifacts pairwise conflict (§3.6).
|
|
504
|
-
|
|
505
|
-
## 4. What the default configuration looks like
|
|
506
|
-
|
|
507
|
-
The full default config reproduces all 21 states / 12 gates / 17 routing phases;
|
|
508
|
-
the sketch below shows one representative slice of each family to make the
|
|
509
|
-
flavor concrete (grilling hand-off, the test loop, the human review gate):
|
|
510
|
-
|
|
511
|
-
```yaml
|
|
512
|
-
# gtd.workflow.yaml — excerpt of the shipped default
|
|
513
|
-
actors: { human: { kind: interactive }, agent: { kind: autonomous } }
|
|
514
|
-
|
|
515
|
-
commands: { test: "npm run test" }
|
|
516
|
-
|
|
517
|
-
params:
|
|
518
|
-
fixAttemptCap: { default: 3 }
|
|
519
|
-
reviewThreshold: { default: 3 }
|
|
520
|
-
agenticReview: { default: true }
|
|
521
|
-
squash: { default: true }
|
|
522
|
-
learning: { default: true }
|
|
523
|
-
|
|
524
|
-
artifacts:
|
|
525
|
-
todo: { path: .gtd/TODO.md, validator: open-questions, format: true }
|
|
526
|
-
architecture:
|
|
527
|
-
{ path: .gtd/ARCHITECTURE.md, validator: open-questions, format: true }
|
|
528
|
-
feedback: { path: .gtd/FEEDBACK.md, probes: [empty], consumedByTurn: fixing }
|
|
529
|
-
errors: { path: .gtd/ERRORS.md }
|
|
530
|
-
review:
|
|
531
|
-
{
|
|
532
|
-
path: .gtd/REVIEW.md,
|
|
533
|
-
validator: review-doc,
|
|
534
|
-
probes: [checkbox-only-diff],
|
|
535
|
-
}
|
|
536
|
-
packages: { kind: package-set, path: .gtd/ }
|
|
537
|
-
|
|
538
|
-
counters:
|
|
539
|
-
testFix:
|
|
540
|
-
{
|
|
541
|
-
resetOn: [packageStart, feedbackRound, removedErrors],
|
|
542
|
-
incrementOn: [errorsRound],
|
|
543
|
-
}
|
|
544
|
-
reviewFix: { resetOn: [packageStart], incrementOn: [feedbackRound] }
|
|
545
|
-
|
|
546
|
-
guards:
|
|
547
|
-
forceApprove:
|
|
548
|
-
any:
|
|
549
|
-
- { not: { param: agenticReview } }
|
|
550
|
-
- { counterAtLeast: { counter: reviewFix, param: reviewThreshold } }
|
|
551
|
-
|
|
552
|
-
states:
|
|
553
|
-
grilling:
|
|
554
|
-
awaits: dynamic
|
|
555
|
-
prompts:
|
|
556
|
-
{ agent: prompts/grilling-agent.md, human: prompts/grilling-answers.md }
|
|
557
|
-
model: planning
|
|
558
|
-
emptyAgentTurn: inert
|
|
559
|
-
validateAgentTurn: open-questions
|
|
560
|
-
building:
|
|
561
|
-
awaits: agent
|
|
562
|
-
prompts: { agent: prompts/building.md }
|
|
563
|
-
model: execution
|
|
564
|
-
emptyAgentTurn: inert
|
|
565
|
-
fixing:
|
|
566
|
-
awaits: agent
|
|
567
|
-
prompts: { agent: prompts/fixing.md }
|
|
568
|
-
model: execution
|
|
569
|
-
emptyAgentTurn: inert
|
|
570
|
-
escalate:
|
|
571
|
-
awaits: human
|
|
572
|
-
prompts: { human: prompts/escalate.md }
|
|
573
|
-
agentic-review:
|
|
574
|
-
awaits: agent
|
|
575
|
-
prompts: { agent: prompts/agentic-review.md }
|
|
576
|
-
model: planning
|
|
577
|
-
emptyAgentTurn: inert
|
|
578
|
-
await-review:
|
|
579
|
-
awaits: human
|
|
580
|
-
prompts: { human: prompts/await-review.md }
|
|
581
|
-
gate: review
|
|
582
|
-
checkoutWindow: true
|
|
583
|
-
testing: { kind: edge-only }
|
|
584
|
-
close-package: { kind: edge-only }
|
|
585
|
-
|
|
586
|
-
interrupts:
|
|
587
|
-
- { when: errors.present, rest: { state: escalate } }
|
|
588
|
-
- when: { any: [feedback.present, feedback.pendingDeletion] }
|
|
589
|
-
unless: { all: [{ guard: forceApprove }, { head.isRouting: tests-green }] }
|
|
590
|
-
branch:
|
|
591
|
-
- {
|
|
592
|
-
when: { head.isRouting: tests-green },
|
|
593
|
-
rest: { state: agentic-review },
|
|
594
|
-
}
|
|
595
|
-
- when: { all: [feedback.emptyEffective, { not: { fact: inFixLoop } }] }
|
|
596
|
-
chain: { state: close-package, action: close-package }
|
|
597
|
-
- rest: { state: fixing }
|
|
598
|
-
|
|
599
|
-
onTurn:
|
|
600
|
-
- match: { actor: agent, gate: building }
|
|
601
|
-
chain:
|
|
602
|
-
state: building
|
|
603
|
-
action:
|
|
604
|
-
run-check:
|
|
605
|
-
command: test
|
|
606
|
-
removeFirst: [feedback]
|
|
607
|
-
green: { route: tests-green }
|
|
608
|
-
red:
|
|
609
|
-
write: feedback
|
|
610
|
-
cap: { counter: testFix, param: fixAttemptCap, write: errors }
|
|
611
|
-
route: errors
|
|
612
|
-
- match: { actor: human, gate: review }
|
|
613
|
-
branch:
|
|
614
|
-
- when: { fact: reviewSubstantive }
|
|
615
|
-
chain:
|
|
616
|
-
{
|
|
617
|
-
state: review,
|
|
618
|
-
action: { route: review-feedback, remove: [review] },
|
|
619
|
-
}
|
|
620
|
-
- chain: { state: review, action: { route: done, remove: [review] } }
|
|
621
|
-
|
|
622
|
-
onRouting:
|
|
623
|
-
- match: planning
|
|
624
|
-
rest: { state: building, awaits: agent }
|
|
625
|
-
- match: errors
|
|
626
|
-
branch:
|
|
627
|
-
- { when: errors.present, rest: { state: escalate } }
|
|
628
|
-
- rest: { state: fixing, awaits: agent }
|
|
629
|
-
- match: tests-green
|
|
630
|
-
branch:
|
|
631
|
-
- when: packages.present
|
|
632
|
-
branch:
|
|
633
|
-
- {
|
|
634
|
-
when: { guard: forceApprove },
|
|
635
|
-
chain: { state: close-package, action: close-package },
|
|
636
|
-
}
|
|
637
|
-
- rest: { state: agentic-review, awaits: agent }
|
|
638
|
-
- use: afterCycleSettles
|
|
639
|
-
- match: awaiting-review
|
|
640
|
-
rest: { state: await-review, awaits: human }
|
|
641
|
-
|
|
642
|
-
fallback:
|
|
643
|
-
- {
|
|
644
|
-
when: { all: [packages.present, packages.modified] },
|
|
645
|
-
rest: { state: planning },
|
|
646
|
-
}
|
|
647
|
-
- { when: todo.exists, rest: { state: grilling, awaits: agent } }
|
|
648
|
-
- { when: architecture.exists, rest: { state: architecting, awaits: agent } }
|
|
649
|
-
- { when: { fact: reviewable }, rest: { state: review, awaits: agent } }
|
|
650
|
-
- rest: { state: idle }
|
|
651
|
-
|
|
652
|
-
entry:
|
|
653
|
-
- { when: health.exists, gate: health-fixing }
|
|
654
|
-
- { when: plan.exists, gate: grilled }
|
|
655
|
-
- { when: architecture.exists, gate: architecting }
|
|
656
|
-
- { gate: grilling }
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
## 5. What a user-built machine looks like
|
|
660
|
-
|
|
661
|
-
A team that wants a minimal loop — no grilling, no learning, no squash, plus a
|
|
662
|
-
**security sign-off** human gate after the automated review — writes roughly:
|
|
663
|
-
|
|
664
|
-
```yaml
|
|
665
|
-
artifacts:
|
|
666
|
-
spec: { path: .gtd/SPEC.md }
|
|
667
|
-
security: { path: .gtd/SECURITY.md, probes: [checkbox-only-diff] }
|
|
668
|
-
feedback: { path: .gtd/FEEDBACK.md, probes: [empty], consumedByTurn: fixing }
|
|
669
|
-
errors: { path: .gtd/ERRORS.md }
|
|
670
|
-
packages: { kind: package-set, path: .gtd/ }
|
|
671
|
-
|
|
672
|
-
states:
|
|
673
|
-
planning:
|
|
674
|
-
{
|
|
675
|
-
awaits: agent,
|
|
676
|
-
prompts: { agent: prompts/plan.md },
|
|
677
|
-
model: planning,
|
|
678
|
-
emptyAgentTurn: inert,
|
|
679
|
-
}
|
|
680
|
-
building:
|
|
681
|
-
{
|
|
682
|
-
awaits: agent,
|
|
683
|
-
prompts: { agent: prompts/build.md },
|
|
684
|
-
model: execution,
|
|
685
|
-
emptyAgentTurn: inert,
|
|
686
|
-
}
|
|
687
|
-
fixing:
|
|
688
|
-
{
|
|
689
|
-
awaits: agent,
|
|
690
|
-
prompts: { agent: prompts/fix.md },
|
|
691
|
-
model: execution,
|
|
692
|
-
emptyAgentTurn: inert,
|
|
693
|
-
}
|
|
694
|
-
security-review:
|
|
695
|
-
awaits: agent
|
|
696
|
-
prompts: { agent: prompts/security-review.md } # agent writes .gtd/SECURITY.md findings
|
|
697
|
-
model: planning
|
|
698
|
-
emptyAgentTurn: inert
|
|
699
|
-
await-signoff:
|
|
700
|
-
awaits: human
|
|
701
|
-
prompts: { human: prompts/await-signoff.md }
|
|
702
|
-
gate: security-review
|
|
703
|
-
checkoutWindow: true
|
|
704
|
-
escalate: { awaits: human, prompts: { human: prompts/escalate.md } }
|
|
705
|
-
idle: { awaits: human, prompts: { human: prompts/idle.md } }
|
|
706
|
-
# … edges: spec.exists → planning; building turn → run-check(test);
|
|
707
|
-
# tests-green → security-review; agent security turn (security.present) →
|
|
708
|
-
# route awaiting-signoff; human signoff turn: checkbox-only/empty → done,
|
|
709
|
-
# substantive → route back to fixing with the human's diff as feedback.
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
The point of the example: the _human gate_ is just a state that `awaits: human`
|
|
713
|
-
with a checkout window and an empty-turn-is-approval convention — the same three
|
|
714
|
-
declarations the built-in review gate uses. Nothing about human gating is
|
|
715
|
-
special-cased anymore.
|
|
716
|
-
|
|
717
|
-
## 6. The hard 20% — honest inventory
|
|
718
|
-
|
|
719
|
-
Things in today's machine that resist naive table-ization, and where each lands:
|
|
720
|
-
|
|
721
|
-
| Today's special case | Home in the design |
|
|
722
|
-
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
723
|
-
| `headIsFixerTurn` / `headIsHealthFixerTurn` precedence exceptions | auto-generated `unless` from artifact `consumedByTurn` (§3.2) |
|
|
724
|
-
| force-approve interplay with a lingering FEEDBACK.md at tests-green | named guard + explicit `unless` on the interrupt rule (§3.6) — stays visibly ugly, which is honest |
|
|
725
|
-
| delete-dispute (`pendingFeedbackDeletion` ≈ emptied file) | artifact-derived fact `*.pendingDeletion` + the `emptyEffective` composite |
|
|
726
|
-
| empty-turn signal vs inert, per gate, guarded at two layers | mandatory per-state `emptyAgentTurn` policy; compiler wires both layers (§3.5) |
|
|
727
|
-
| health-fix same-chain re-test carve-outs in `applyTurnTaking` | the genuinely hardest fit; likely a `run-check` edge annotation (`resumeChain: true`) rather than pure rules — needs a spike |
|
|
728
|
-
| operational-recovery (boundary commit atop a building checkpoint) | engine-level fallback rule parameterized by "checkpoint gates" (states may declare `checkpoint: true`) |
|
|
729
|
-
| squash-base / review-base / healthFixBase anchor computation | stays engine code — cycle-base discovery is history analysis, not policy |
|
|
730
|
-
| review checkout window | program-edge, per-gate boolean only (§3.8) |
|
|
731
|
-
| decision-log accumulation, prompt-cache-friendly concatenation | stays engine code; exposed to templates as a context field |
|
|
732
|
-
|
|
733
|
-
If the health-fix carve-outs prove un-declarable, the fallback position is a
|
|
734
|
-
small set of engine-recognized **loop idioms** (a "check loop" macro that
|
|
735
|
-
expands to state + interrupt + carve-out) rather than leaking special cases into
|
|
736
|
-
user configs.
|
|
737
|
-
|
|
738
|
-
## 7. Hazards specific to configurability
|
|
739
|
-
|
|
740
|
-
**Config changes mid-history.** The machine's reading of history depends on the
|
|
741
|
-
config; edit the config and yesterday's subjects may fall out of the closed
|
|
742
|
-
grammar. The boundary rule makes this _degrade safely_ (unknown subjects go
|
|
743
|
-
inert — same mechanism as v1 compat) but a rename mid-cycle can still strand a
|
|
744
|
-
live cycle at an unresolvable rest. Mitigations, cheapest first: (1) document
|
|
745
|
-
"change workflow config only at idle"; (2) the compiler warns when the current
|
|
746
|
-
repo state resolves differently (or stops resolving) under the new config — it
|
|
747
|
-
can literally run both resolutions and diff them; (3) optionally record a config
|
|
748
|
-
content-hash in routing commits so the engine can _detect_ (not replay)
|
|
749
|
-
cross-config history. Full config-versioned replay is not worth it — squash
|
|
750
|
-
already collapses cycles, so history exposure is short.
|
|
751
|
-
|
|
752
|
-
**Livelocks and junk-commit loops.** Today's guards against
|
|
753
|
-
empty-turn-consumes-state disasters are hand-analyzed. The compiler checks in
|
|
754
|
-
§3.11 catch the mechanical cases; the chain-depth bound catches the rest at
|
|
755
|
-
runtime with a hard error instead of an infinite driver loop.
|
|
756
|
-
|
|
757
|
-
**Testing story.** Three layers: (1) a **golden equivalence suite** — the
|
|
758
|
-
existing `Machine.test.ts` / property tests run against both the legacy
|
|
759
|
-
hardcoded resolver and the interpreter-with-default-config, asserting identical
|
|
760
|
-
`Result`s for the same event streams (this is the migration's safety net, and
|
|
761
|
-
`resolve()`'s purity makes it cheap); (2) the existing cucumber features keep
|
|
762
|
-
passing unchanged against the default config, since the CLI surface doesn't
|
|
763
|
-
move; (3) for _user_ configs, the engine ships generic property checks (every
|
|
764
|
-
generated stream either progresses, rests awaiting an actor, refuses, or
|
|
765
|
-
hard-errors — never silently loops), runnable as `gtd workflow check`.
|
|
766
|
-
|
|
767
|
-
**Complexity budget.** The interpreter + compiler is a real cost; mutation
|
|
768
|
-
testing an interpreter is harder than mutation testing unrolled branches. The
|
|
769
|
-
counterweight: the unrolled branches are _already_ ~1800 lines whose safety
|
|
770
|
-
rests on prose comments, and every new feature (learning, health, PLAN.md entry)
|
|
771
|
-
has grown them by touching seven files (AGENTS.md's own removal checklist is the
|
|
772
|
-
symptom). Table-driving shrinks the change surface of the next feature to "edit
|
|
773
|
-
the default config."
|
|
774
|
-
|
|
775
|
-
## 8. Migration path
|
|
776
|
-
|
|
777
|
-
1. **Internal table extraction (no behavior change).** Finish what's started:
|
|
778
|
-
fold `classifyHead`'s branches into literal rule rows; derive
|
|
779
|
-
`ResolvePayload` artifact facts from an internal artifact table; generate
|
|
780
|
-
`TurnGate`/`RoutingPhase`/illegal-combination lists from it. Golden tests pin
|
|
781
|
-
equivalence. This step is valuable even if configurability never ships — it
|
|
782
|
-
collapses the seven-file change surface.
|
|
783
|
-
2. **Interpreter + shipped default config.** Define the schema, write the
|
|
784
|
-
compiler, express the default machine in it, and switch `resolve()` to the
|
|
785
|
-
compiled tables. The legacy resolver stays as the test oracle for one
|
|
786
|
-
release.
|
|
787
|
-
3. **Read user config.** Allow overrides of the _safe_ subset first: prompts,
|
|
788
|
-
commands, params, model tiers, toggling shipped optional stages (learning/
|
|
789
|
-
squash/health/agentic-review already have kill-switches — they become
|
|
790
|
-
config-graph presence instead).
|
|
791
|
-
4. **Full custom machines.** Open states/edges/artifacts, ship
|
|
792
|
-
`gtd workflow check`, document the view-model and primitive contracts.
|
|
793
|
-
|
|
794
|
-
Each step is independently shippable, and steps 1–2 carry zero user-visible risk
|
|
795
|
-
while retiring most of the implementation risk.
|
|
796
|
-
|
|
797
|
-
## Appendix A: the configuration surface, state by state
|
|
798
|
-
|
|
799
|
-
### A.1 Where the configuration lives
|
|
800
|
-
|
|
801
|
-
The workflow definition is a separate, **committed** file — the machine's
|
|
802
|
-
reading of history depends on it, so it must travel with the repo:
|
|
803
|
-
|
|
804
|
-
```
|
|
805
|
-
gtd.workflow.yaml # the workflow definition (states, edges, artifacts…)
|
|
806
|
-
.gtdrc.json # scalar knobs, unchanged: testCommand, params, models
|
|
807
|
-
prompts/… # user template overrides (optional)
|
|
808
|
-
```
|
|
809
|
-
|
|
810
|
-
`.gtdrc` keeps what it has today (per-user/per-machine layering via the cwd→home
|
|
811
|
-
walk makes sense for models and commands); the workflow file has no layering —
|
|
812
|
-
one repo, one machine. Absent `gtd.workflow.yaml`, the shipped default
|
|
813
|
-
definition applies, which is byte-for-byte the current v2 behavior.
|
|
814
|
-
|
|
815
|
-
Top-level keys of the workflow file:
|
|
816
|
-
|
|
817
|
-
| Key | Contents |
|
|
818
|
-
| ------------- | ---------------------------------------------------------------- |
|
|
819
|
-
| `actors` | the turn-taking universe (fixed: `human`, `agent`) |
|
|
820
|
-
| `commands` | named shell commands `run-check` edges may reference |
|
|
821
|
-
| `params` | typed scalar parameters guards may reference (defaults + bounds) |
|
|
822
|
-
| `artifacts` | steering-file declarations (§3.2) |
|
|
823
|
-
| `commitFlags` | named commit predicates (§3.3) |
|
|
824
|
-
| `counters` | fold rules over commit flags (§3.3) |
|
|
825
|
-
| `guards` | named, reusable guard expressions (§3.4) |
|
|
826
|
-
| `states` | the state declarations (this appendix) |
|
|
827
|
-
| `interrupts` | ordered steering-file precedence rules (§3.6a) |
|
|
828
|
-
| `onTurn` | HEAD-classification rules for turn commits (§3.6b) |
|
|
829
|
-
| `onRouting` | HEAD-classification rules for routing commits (§3.6b) |
|
|
830
|
-
| `fallback` | ordered boundary-HEAD ladder (§3.6c) |
|
|
831
|
-
| `entry` | dirty-boundary entry-gate selection (§3.6) |
|
|
832
|
-
|
|
833
|
-
Note what is **not** under `states`: transitions. Edges live in the four rule
|
|
834
|
-
families, because resolution is history-replay — a state declaration describes
|
|
835
|
-
what a rest _is_ (who's awaited, what prompt renders, what an empty turn means),
|
|
836
|
-
while the rule families describe how HEAD + facts _select_ one.
|
|
837
|
-
|
|
838
|
-
### A.2 State property reference
|
|
839
|
-
|
|
840
|
-
```yaml
|
|
841
|
-
states:
|
|
842
|
-
<name>:
|
|
843
|
-
kind: prompt | label
|
|
844
|
-
awaits: human | agent | dynamic
|
|
845
|
-
gate: <turn-gate label>
|
|
846
|
-
prompts: { agent: <path>, human: <path> }
|
|
847
|
-
model: <model-state name>
|
|
848
|
-
emptyAgentTurn: inert | signal | { inertWhen: <guard> } | { signal: true, inertWhen: <guard> }
|
|
849
|
-
validateAgentTurn: <validator name>
|
|
850
|
-
checkoutWindow: <bool>
|
|
851
|
-
checkpoint: <bool>
|
|
852
|
-
stepAction: { human: <action>, agent: <action> }
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
| Property | Applies to | Meaning / allowed values |
|
|
856
|
-
| ------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
857
|
-
| `kind` | all | `prompt` — a real rest: `gtd next` renders a prompt, an actor is awaited. `label` — a name that only appears as the `state:` on chain (mid-chain) rules, for `gtd status`/`--json` display; never rests, never prompts. Replaces today's `PROMPT_STATES` complement (testing, planning, close-package, done, health-check, learning-applied). |
|
|
858
|
-
| `awaits` | `kind: prompt` | Which actor's `step` is accepted at this rest. `dynamic` — the awaited actor is decided per-rest by the matching rule (`rest: { state, awaits }`), for the states that alternate (grilling/architecting: agent iterates, human answers). Any other invoker is refused. Source of today's `awaitedActor`. |
|
|
859
|
-
| `gate` | `kind: prompt` | The `<gate>` label the captured turn commit carries (`gtd(<actor>): <gate>`). Defaults to the state name. Overrides exist for states that _share_ a gate with another state — `await-review` captures under `review`, `await-learning-review` under `learning`. Replaces `STATE_IS_OWN_GATE` + `GATE_OVERRIDES`. Compiler checks the generated grammar for collisions. |
|
|
860
|
-
| `prompts` | `kind: prompt` | Template path per awaitable actor. A state with `awaits: dynamic` must bind both; others exactly one. Templates get the documented view-model (`context`, `model`, `fenceFor`, shared partials). Replaces `STATE_TEMPLATE` + the grilling/architecting actor split in `buildPrompt`. |
|
|
861
|
-
| `model` | agent-prompting states | Which model-resolution name `{{MODEL}}` resolves against (today's `ModelState`: `grilling`, `architecting`, `decompose`, `building`, `fixing`, `agentic-review`, `clean` — themselves config: each names a tier, overridable per state in `.gtdrc` exactly as now). Human-awaited states carry none. Replaces `MODEL_STATE`. |
|
|
862
|
-
| `emptyAgentTurn` | agent-awaited rests | What a clean-tree `gtd step-agent` means here. `inert` — no-op, zero commits, `gtd next` re-emits (the `INERT_EMPTY_AGENT_GATES` set). `signal` — the empty turn is captured and means something (health-fixing's environmental fix). `inertWhen: <guard>` — conditional (squashing/learning while the template is unmodified; health-fixing while HEAD is the human's entry turn). The compiler wires this into BOTH layers (capture guard and historical-HEAD classification), which today must be kept in sync by hand. **Mandatory** for every agent-awaited rest — the author must decide. |
|
|
863
|
-
| `validateAgentTurn` | agent-awaited rests | Named structural validator run before capturing the agent's turn; malformed → refusal with the error list, zero commits (today: `open-questions` on grilling/architecting, `review-doc` on review). Never applied to human turns. |
|
|
864
|
-
| `checkoutWindow` | human-awaited rests | Hold the review checkout window open while this rest is pending (HEAD/index rewound to the review base so editors surface the diff). Program-edge wiring only; the resolver never sees it. Today: hardcoded to `await-review`. |
|
|
865
|
-
| `checkpoint` | agent-awaited rests | This state's turn commit is an operational-recovery resume point: a boundary commit landing on top of it (config fix after a mid-chain crash) re-resolves to this state's continuation instead of hard-erroring. Today: hardcoded to `building`. |
|
|
866
|
-
| `stepAction` | `kind: prompt` | Escape hatch for the rests where an actor's `step` performs an action _instead of_ capturing a turn. Today's only case: `idle.stepAction.human = run-check` (a human step at idle re-runs the health check, never authoring an empty turn). Kept deliberately rare — most behavior belongs in `onTurn` rules. |
|
|
867
|
-
|
|
868
|
-
What is deliberately **not** a state property: the awaited-human-empty-turn
|
|
869
|
-
meaning (an empty human turn is always captured — whether it means "accept
|
|
870
|
-
defaults", "approve", or "resume" is decided by the `onTurn` branch that
|
|
871
|
-
classifies the captured commit); transitions; and anything that would let a
|
|
872
|
-
state execute arbitrary code.
|
|
873
|
-
|
|
874
|
-
### A.3 The default machine, all 21 states
|
|
875
|
-
|
|
876
|
-
```yaml
|
|
877
|
-
states:
|
|
878
|
-
# ── grilling: product plan ─────────────────────────────────────────────
|
|
879
|
-
grilling:
|
|
880
|
-
kind: prompt
|
|
881
|
-
awaits: dynamic # agent iterates .gtd/TODO.md; human answers open questions
|
|
882
|
-
prompts:
|
|
883
|
-
agent: prompts/grilling-agent.md
|
|
884
|
-
human: prompts/grilling-answers.md
|
|
885
|
-
model: grilling # planning tier
|
|
886
|
-
emptyAgentTurn: inert
|
|
887
|
-
validateAgentTurn: open-questions
|
|
888
|
-
|
|
889
|
-
# ── architecting: technical plan ───────────────────────────────────────
|
|
890
|
-
architecting:
|
|
891
|
-
kind: prompt
|
|
892
|
-
awaits: dynamic
|
|
893
|
-
prompts:
|
|
894
|
-
agent: prompts/architecting-agent.md
|
|
895
|
-
human: prompts/architecting-answers.md
|
|
896
|
-
model: architecting
|
|
897
|
-
emptyAgentTurn: inert
|
|
898
|
-
validateAgentTurn: open-questions
|
|
899
|
-
|
|
900
|
-
# ── grilled: decompose into packages ───────────────────────────────────
|
|
901
|
-
grilled:
|
|
902
|
-
kind: prompt
|
|
903
|
-
awaits: agent # the human's grilled turn exists only as the PLAN.md entry (entry rules)
|
|
904
|
-
prompts: { agent: prompts/decompose.md }
|
|
905
|
-
model: decompose
|
|
906
|
-
emptyAgentTurn: inert # no packages written → re-emit; never consume ARCHITECTURE.md
|
|
907
|
-
|
|
908
|
-
# ── build lifecycle ────────────────────────────────────────────────────
|
|
909
|
-
planning: { kind: label } # transient: packages present + .gtd/ modified (promptless today)
|
|
910
|
-
building:
|
|
911
|
-
kind: prompt
|
|
912
|
-
awaits: agent
|
|
913
|
-
prompts: { agent: prompts/building.md }
|
|
914
|
-
model: building # execution tier
|
|
915
|
-
emptyAgentTurn: inert
|
|
916
|
-
checkpoint: true # boundary commit atop the building turn resumes the test chain
|
|
917
|
-
testing: { kind: label } # the run-check hop
|
|
918
|
-
fixing:
|
|
919
|
-
kind: prompt
|
|
920
|
-
awaits: agent
|
|
921
|
-
prompts: { agent: prompts/fixing.md }
|
|
922
|
-
model: fixing
|
|
923
|
-
emptyAgentTurn: inert
|
|
924
|
-
escalate:
|
|
925
|
-
kind: prompt
|
|
926
|
-
awaits: human # deleting .gtd/ERRORS.md is the move; onTurn rule chains a fresh run-check
|
|
927
|
-
prompts: { human: prompts/escalate.md }
|
|
928
|
-
|
|
929
|
-
# ── per-package review ─────────────────────────────────────────────────
|
|
930
|
-
agentic-review:
|
|
931
|
-
kind: prompt
|
|
932
|
-
awaits: agent # writes .gtd/FEEDBACK.md; empty file = approval (artifact probe)
|
|
933
|
-
prompts: { agent: prompts/agentic-review.md }
|
|
934
|
-
model: agentic-review
|
|
935
|
-
emptyAgentTurn: inert # no FEEDBACK.md written at all is NOT an approval
|
|
936
|
-
close-package: { kind: label }
|
|
937
|
-
|
|
938
|
-
# ── cycle-end human review ─────────────────────────────────────────────
|
|
939
|
-
review:
|
|
940
|
-
kind: prompt
|
|
941
|
-
awaits: agent # writes .gtd/REVIEW.md
|
|
942
|
-
prompts: { agent: prompts/review.md }
|
|
943
|
-
model: clean
|
|
944
|
-
emptyAgentTurn: inert
|
|
945
|
-
validateAgentTurn: review-doc
|
|
946
|
-
await-review:
|
|
947
|
-
kind: prompt
|
|
948
|
-
awaits: human
|
|
949
|
-
gate: review # human turn is captured under the shared "review" gate
|
|
950
|
-
prompts: { human: prompts/await-review.md }
|
|
951
|
-
checkoutWindow: true
|
|
952
|
-
done: { kind: label }
|
|
953
|
-
|
|
954
|
-
# ── learning ───────────────────────────────────────────────────────────
|
|
955
|
-
learning:
|
|
956
|
-
kind: prompt
|
|
957
|
-
awaits: agent
|
|
958
|
-
prompts: { agent: prompts/learning.md }
|
|
959
|
-
model: clean
|
|
960
|
-
emptyAgentTurn: { inertWhen: learnings.isTemplate }
|
|
961
|
-
await-learning-review:
|
|
962
|
-
kind: prompt
|
|
963
|
-
awaits: human # empty turn = accept draft; there is no reject path (onTurn rule)
|
|
964
|
-
gate: learning
|
|
965
|
-
prompts: { human: prompts/await-learning-review.md }
|
|
966
|
-
learning-apply:
|
|
967
|
-
kind: prompt
|
|
968
|
-
awaits: agent
|
|
969
|
-
prompts: { agent: prompts/learning-apply.md }
|
|
970
|
-
model: clean
|
|
971
|
-
emptyAgentTurn: inert # a doc-edit move: clean tree = nothing to apply yet
|
|
972
|
-
learning-applied: { kind: label }
|
|
973
|
-
|
|
974
|
-
# ── squash ─────────────────────────────────────────────────────────────
|
|
975
|
-
squashing:
|
|
976
|
-
kind: prompt
|
|
977
|
-
awaits: agent
|
|
978
|
-
prompts: { agent: prompts/squashing.md }
|
|
979
|
-
model: clean
|
|
980
|
-
emptyAgentTurn: { inertWhen: squashMsg.isTemplate } # never squash the placeholder
|
|
981
|
-
|
|
982
|
-
# ── idle / health ──────────────────────────────────────────────────────
|
|
983
|
-
idle:
|
|
984
|
-
kind: prompt
|
|
985
|
-
awaits: human
|
|
986
|
-
prompts: { human: prompts/idle.md }
|
|
987
|
-
stepAction: # human step at idle re-runs the health check, never an empty turn
|
|
988
|
-
human:
|
|
989
|
-
run-check:
|
|
990
|
-
command: test
|
|
991
|
-
green: { stop: true, chainWhen: { fact: healthFixBasePresent } }
|
|
992
|
-
red:
|
|
993
|
-
write: health
|
|
994
|
-
cap: { counter: healthFix, param: fixAttemptCap, write: errors }
|
|
995
|
-
route: health-check
|
|
996
|
-
health-check: { kind: label }
|
|
997
|
-
health-fixing:
|
|
998
|
-
kind: prompt
|
|
999
|
-
awaits: agent # also the HEALTH.md entry gate for a human (entry rules)
|
|
1000
|
-
prompts: { agent: prompts/health-fixing.md }
|
|
1001
|
-
model: fixing
|
|
1002
|
-
emptyAgentTurn: # empty agent turn = environmental fix (remove HEALTH.md, re-test)…
|
|
1003
|
-
signal: true
|
|
1004
|
-
inertWhen: { head.isTurn: [human, health-fixing] } # …unless the human's entry turn is still unread
|
|
1005
|
-
```
|
|
1006
|
-
|
|
1007
|
-
Reading the table against today's code: `kind` reproduces
|
|
1008
|
-
`PROMPT_STATES`/`isPromptState`; `awaits` reproduces `awaitedActor` plus the
|
|
1009
|
-
dynamic grilling/architecting split; `gate` reproduces `gateForState`;
|
|
1010
|
-
`prompts` + `model` reproduce `STATE_TEMPLATE` + `MODEL_STATE`; `emptyAgentTurn`
|
|
1011
|
-
reproduces `INERT_EMPTY_AGENT_GATES` + `isInertEmptyAgentRest`'s conditional
|
|
1012
|
-
cases; `validateAgentTurn` reproduces the two structural-validation refusals in
|
|
1013
|
-
`applyTurnTaking`; `checkpoint` reproduces the operational-recovery rung;
|
|
1014
|
-
`stepAction` reproduces the idle health-check carve-out. Every hardcoded
|
|
1015
|
-
per-state fact in the engine has exactly one home in the declaration — which is
|
|
1016
|
-
the test the schema was designed against.
|
|
1017
|
-
|
|
1018
|
-
## Appendix B: the diff-driven chain model (a radical simplification)
|
|
1019
|
-
|
|
1020
|
-
> A second-round exploration: can the four rule families of §3.6 collapse into
|
|
1021
|
-
> ONE mechanism? Proposal under test: _each state is defined by its previous
|
|
1022
|
-
> state(s) plus diff-matching rules; matching always commits the new state; a
|
|
1023
|
-
> clean tree means we ARE at the derived state, where exactly one state command
|
|
1024
|
-
> runs (its output selecting what happens next) — otherwise the state just emits
|
|
1025
|
-
> its prompt._
|
|
1026
|
-
|
|
1027
|
-
### B.1 The model
|
|
1028
|
-
|
|
1029
|
-
Three ideas, and everything else falls out:
|
|
1030
|
-
|
|
1031
|
-
1. **State = commit label.** Every workflow commit is labeled with the state it
|
|
1032
|
-
entered: `gtd(human): grilling`, `gtd(agent): fixing`, `gtd: tests-green`
|
|
1033
|
-
(machine-authored). The current state is simply the label of the nearest
|
|
1034
|
-
labeled commit (boundary commits — anything unlabeled — are skipped, as
|
|
1035
|
-
`lastWorkflowTurn` already does).
|
|
1036
|
-
|
|
1037
|
-
2. **Transition = diff classification.** A state declares which predecessor
|
|
1038
|
-
states it can follow (`from`) and what shape of pending changes selects it
|
|
1039
|
-
(`when`: touched/added/deleted paths, plus the closed probe set). When the
|
|
1040
|
-
tree is dirty, the engine finds the matching state for _(previous state,
|
|
1041
|
-
invoker, diff)_ and **commits the changes under that label**. No separate
|
|
1042
|
-
"capture" vs "routing" concepts — the machine's own file writes re-enter the
|
|
1043
|
-
same classifier as everyone else's.
|
|
1044
|
-
|
|
1045
|
-
3. **A state either acts or waits.** On a clean tree at state S: if S declares a
|
|
1046
|
-
`command`, run it — its outcome (exit code, or a built-in primitive's effect)
|
|
1047
|
-
either writes files (which re-enter rule 2 as the machine actor) or stops. If
|
|
1048
|
-
S declares no command, emit S's `prompt`. Command XOR prompt.
|
|
1049
|
-
|
|
1050
|
-
The driver loop becomes:
|
|
1051
|
-
|
|
1052
|
-
```
|
|
1053
|
-
loop:
|
|
1054
|
-
prev ← nearest labeled commit (skip boundary commits)
|
|
1055
|
-
if tree is dirty:
|
|
1056
|
-
rule ← first rule in states[*] with prev ∈ from, actor = invoker, when ⊨ diff
|
|
1057
|
-
none matched → refuse (wrong actor's turn, or unrecognized changes)
|
|
1058
|
-
git commit -am "gtd(<actor>): <rule.state>" # ← the ONLY mutation
|
|
1059
|
-
else if invoker is stepping and some rule has when: empty:
|
|
1060
|
-
git commit --allow-empty under that rule's state # empty turn as signal
|
|
1061
|
-
else:
|
|
1062
|
-
S ← states[prev.label]
|
|
1063
|
-
S.command? → run it; primitive writes/deletes files → loop (as machine actor)
|
|
1064
|
-
→ or terminal outcome → stop
|
|
1065
|
-
S.prompt? → emit prompt; stop
|
|
1066
|
-
```
|
|
1067
|
-
|
|
1068
|
-
Every iteration either commits (progress) or stops (prompt / refusal / done), so
|
|
1069
|
-
termination is the chain-depth bound for free.
|
|
1070
|
-
|
|
1071
|
-
### B.2 What this dissolves
|
|
1072
|
-
|
|
1073
|
-
Checking the model against every mechanism in the current engine:
|
|
1074
|
-
|
|
1075
|
-
| Current mechanism | Fate in the diff-driven model |
|
|
1076
|
-
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1077
|
-
| Turn commits vs routing commits (two namespaces) | **Unified.** One label grammar; the machine is just a third actor whose writes classify like anyone's |
|
|
1078
|
-
| `classifyHead` table (§3.6b) | **Dissolved.** HEAD's label IS the state — no re-derivation of "what did this subject mean" |
|
|
1079
|
-
| Steering-file precedence ladder (§3.6a) | **Mostly dissolved.** It exists today because subjects under-determine state (a fresh FEEDBACK.md is newer than HEAD). Here every write is committed-with-label at once, so the file/HEAD divergence it compensates for cannot arise |
|
|
1080
|
-
| Boundary fallback ladder (§3.6c) | **Dissolved into `from`.** `todo.exists → grilling` etc. were re-anchoring rules; with labels authoritative, only "skip boundary commits" remains |
|
|
1081
|
-
| Entry points (dirty tree at boundary → which gate) | **Just rules from `start`.** `from: [start], when: { adds: plan } → decompose-entry` — entry stops being special |
|
|
1082
|
-
| `INERT_EMPTY_AGENT_GATES` + `isInertEmptyAgentRest` | **Inert-by-default.** A clean tree matches nothing unless a state explicitly declares `when: empty` — the entire bug class ("does this gate consume state on an empty turn?") becomes opt-in instead of curated |
|
|
1083
|
-
| Empty human turn = signal (accept/approve/resume) | An explicit `when: { empty: true }, actor: human` rule per gate that wants it |
|
|
1084
|
-
| Counter flags derived from subjects + diffs | **Simplified.** Counters fold directly over state labels in history (`count("test-failed") since last "package-start"-class label`) — no diff inspection needed, the label already encodes it |
|
|
1085
|
-
| Crash recovery (write landed, commit didn't) | **Free.** The orphaned write is a dirty tree that reclassifies identically on the next invocation — the write path and the recovery path are the same code |
|
|
1086
|
-
| `applyTurnTaking` refusals | Kept, but shrunk: "no rule for this (state, invoker, diff)" is the refusal |
|
|
1087
|
-
| `predictTurn` / `gtd status` | Trivial: report which rule the current diff would match |
|
|
1088
|
-
|
|
1089
|
-
### B.3 What must be layered back (the honest list)
|
|
1090
|
-
|
|
1091
|
-
The one-sentence model needs eight amendments to reproduce current behavior —
|
|
1092
|
-
all small, none breaking the single-mechanism shape:
|
|
1093
|
-
|
|
1094
|
-
1. **Actor on rules.** Diff shape can't distinguish the human's TODO.md answers
|
|
1095
|
-
from the agent's TODO.md iteration; the invoker can. Rules match _(from,
|
|
1096
|
-
actor, diff)_, not _(from, diff)_.
|
|
1097
|
-
2. **`from` is a list.** Fixing follows building-red AND escalate-resume;
|
|
1098
|
-
grilling follows start AND review-feedback. Strict single-predecessor is too
|
|
1099
|
-
narrow; lists preserve the shape.
|
|
1100
|
-
3. **Ordered rules per (from, actor).** At awaiting-review, human:
|
|
1101
|
-
checkbox-only/deletes-review/empty → approve must be tried before
|
|
1102
|
-
anything-else → feedback. First match wins, declaration order.
|
|
1103
|
-
4. **The closed probe set rides on matchers.** `adds: feedback` splits into
|
|
1104
|
-
`{ adds: feedback, probe: empty }` vs non-empty;
|
|
1105
|
-
`{ touches: review, probe: checkbox-only }`;
|
|
1106
|
-
`{ touches: squashMsg, probe: template-modified }`. Same three probes as §3.2
|
|
1107
|
-
— content still never steers beyond them.
|
|
1108
|
-
5. **Commands are the closed primitive set, with guarded outcomes.** A state
|
|
1109
|
-
command is a shell command (exit-code branch) or a built-in
|
|
1110
|
-
(`write-template`, `seed`, `close-package`, `squash`, `stop`). Outcomes may
|
|
1111
|
-
carry guards over counters/params
|
|
1112
|
-
(`red below cap → write output to feedback; red at cap → write to errors`) —
|
|
1113
|
-
this is where `fixAttemptCap`, `reviewThreshold`, `learning`/`squash` toggles
|
|
1114
|
-
live.
|
|
1115
|
-
6. **Machine-labeled marker commits.** Some outcomes change no files (tests
|
|
1116
|
-
green) but must be observable in history for the fold — `--allow-empty`
|
|
1117
|
-
machine commits (`gtd: tests-green`). Today's routing commits survive only in
|
|
1118
|
-
this reduced role.
|
|
1119
|
-
7. **Guarded prompt/command split.** `tests-green` is a prompt state
|
|
1120
|
-
(agentic-review) _unless_ force-approve holds, where it acts (close-package).
|
|
1121
|
-
Either allow `command: { when: <guard>, … }` falling back to the prompt, or
|
|
1122
|
-
accept an extra marker commit. The former is one wrinkle; the latter is
|
|
1123
|
-
noisier history (squash collapses it anyway).
|
|
1124
|
-
8. **Refusal semantics.** Dirty tree matching no rule = hard refusal (today's
|
|
1125
|
-
out-of-turn error + illegal-combination error, unified). The
|
|
1126
|
-
corrupt-repo-refuses-to-guess contract survives as "no rule matched."
|
|
1127
|
-
|
|
1128
|
-
### B.4 The default machine, sketched in this model
|
|
1129
|
-
|
|
1130
|
-
A representative slice (entry → grilling → build/test loop → review), showing
|
|
1131
|
-
that the 21 states redraw naturally as ~a-couple-dozen labeled states:
|
|
1132
|
-
|
|
1133
|
-
```yaml
|
|
1134
|
-
states:
|
|
1135
|
-
# ── entry (from `start` = boundary HEAD; replaces the entry table) ──────
|
|
1136
|
-
grilling-entry:
|
|
1137
|
-
from: [start, done]
|
|
1138
|
-
actor: human
|
|
1139
|
-
when: { any: true } # lowest-priority catch-all: any sketch/notes
|
|
1140
|
-
prompt: grilling-agent.md
|
|
1141
|
-
architecting-entry:
|
|
1142
|
-
from: [start, done]
|
|
1143
|
-
actor: human
|
|
1144
|
-
when: { adds: architecture }
|
|
1145
|
-
prompt: architecting-agent.md
|
|
1146
|
-
plan-entry:
|
|
1147
|
-
from: [start, done]
|
|
1148
|
-
actor: human
|
|
1149
|
-
when: { adds: plan }
|
|
1150
|
-
command: { seed: { from: plan, to: architecture } } # writes → re-classify
|
|
1151
|
-
health-entry:
|
|
1152
|
-
from: [start, done]
|
|
1153
|
-
actor: human
|
|
1154
|
-
when: { adds: health }
|
|
1155
|
-
prompt: health-fixing.md
|
|
1156
|
-
|
|
1157
|
-
# ── grilling (alternating turns, one label) ─────────────────────────────
|
|
1158
|
-
grilling-draft:
|
|
1159
|
-
from: [grilling-entry, grilling-answer, review-feedback]
|
|
1160
|
-
actor: agent
|
|
1161
|
-
when: { touches: todo }
|
|
1162
|
-
validate: open-questions
|
|
1163
|
-
prompt: grilling-answers.md # awaiting the human
|
|
1164
|
-
grilling-answer:
|
|
1165
|
-
from: [grilling-draft]
|
|
1166
|
-
actor: human
|
|
1167
|
-
when: { touches: any }
|
|
1168
|
-
prompt: grilling-agent.md # back to the agent
|
|
1169
|
-
grilling-accept:
|
|
1170
|
-
from: [grilling-draft]
|
|
1171
|
-
actor: human
|
|
1172
|
-
when: { empty: true } # accept-defaults signal
|
|
1173
|
-
command: { seed: { from: todo, to: architecture } }
|
|
1174
|
-
architecture-seeded:
|
|
1175
|
-
from: [plan-entry, grilling-accept]
|
|
1176
|
-
actor: gtd # the machine's own seed write, classified like any diff
|
|
1177
|
-
when: { adds: architecture, deletes: [todo, plan] }
|
|
1178
|
-
prompt: architecting-agent.md # (decompose.md when from plan-entry)
|
|
1179
|
-
|
|
1180
|
-
# ── build / test loop ───────────────────────────────────────────────────
|
|
1181
|
-
building:
|
|
1182
|
-
from: [planning, package-done]
|
|
1183
|
-
actor: agent
|
|
1184
|
-
when: { touches: code } # code = everything outside .gtd/
|
|
1185
|
-
command:
|
|
1186
|
-
run: test
|
|
1187
|
-
removeFirst: [feedback]
|
|
1188
|
-
green: { markerState: tests-green }
|
|
1189
|
-
red:
|
|
1190
|
-
write: { output-to: feedback }
|
|
1191
|
-
atCap:
|
|
1192
|
-
{
|
|
1193
|
-
counter: testFix,
|
|
1194
|
-
param: fixAttemptCap,
|
|
1195
|
-
write: { output-to: errors },
|
|
1196
|
-
}
|
|
1197
|
-
test-failed:
|
|
1198
|
-
from: [building, fixing, escalate-resume]
|
|
1199
|
-
actor: gtd
|
|
1200
|
-
when: { adds: feedback }
|
|
1201
|
-
prompt: fixing.md
|
|
1202
|
-
test-failed-cap:
|
|
1203
|
-
from: [building, fixing, escalate-resume]
|
|
1204
|
-
actor: gtd
|
|
1205
|
-
when: { adds: errors }
|
|
1206
|
-
prompt: escalate.md
|
|
1207
|
-
fixing:
|
|
1208
|
-
from: [test-failed, review-findings]
|
|
1209
|
-
actor: agent
|
|
1210
|
-
when: { touches: any }
|
|
1211
|
-
command: { run: test, removeFirst: [feedback], green: …, red: … }
|
|
1212
|
-
escalate-resume:
|
|
1213
|
-
from: [test-failed-cap]
|
|
1214
|
-
actor: human
|
|
1215
|
-
when: { deletes: errors } # deletion resets the budget
|
|
1216
|
-
command: { run: test, … }
|
|
1217
|
-
tests-green:
|
|
1218
|
-
from: [building, fixing]
|
|
1219
|
-
actor: gtd
|
|
1220
|
-
when: { marker: true } # --allow-empty machine commit
|
|
1221
|
-
command: # guarded split (amendment 7)
|
|
1222
|
-
when: { any: [{ not: { param: agenticReview } }, { counterAtLeast: … }] }
|
|
1223
|
-
then: close-package
|
|
1224
|
-
prompt: agentic-review.md # otherwise: rest for the reviewer
|
|
1225
|
-
|
|
1226
|
-
# ── agentic review verdict: two rules on one file, split by probe ──────
|
|
1227
|
-
review-approve:
|
|
1228
|
-
from: [tests-green]
|
|
1229
|
-
actor: agent
|
|
1230
|
-
when: { adds: feedback, probe: empty }
|
|
1231
|
-
command: close-package
|
|
1232
|
-
review-findings:
|
|
1233
|
-
from: [tests-green]
|
|
1234
|
-
actor: agent
|
|
1235
|
-
when: { adds: feedback }
|
|
1236
|
-
prompt: fixing.md
|
|
1237
|
-
|
|
1238
|
-
# ── human review gate ───────────────────────────────────────────────────
|
|
1239
|
-
review-written:
|
|
1240
|
-
from: [package-done] # via a remaining-packages guard on package-done
|
|
1241
|
-
actor: agent
|
|
1242
|
-
when: { adds: review }
|
|
1243
|
-
validate: review-doc
|
|
1244
|
-
prompt: await-review.md
|
|
1245
|
-
checkoutWindow: true
|
|
1246
|
-
review-approved:
|
|
1247
|
-
from: [review-written]
|
|
1248
|
-
actor: human
|
|
1249
|
-
when:
|
|
1250
|
-
any:
|
|
1251
|
-
[
|
|
1252
|
-
{ empty: true },
|
|
1253
|
-
{ touches: review, probe: checkbox-only },
|
|
1254
|
-
{ deletes: review },
|
|
1255
|
-
]
|
|
1256
|
-
command: { remove: [review], markerState: done }
|
|
1257
|
-
review-feedback:
|
|
1258
|
-
from: [review-written]
|
|
1259
|
-
actor: human
|
|
1260
|
-
when: { touches: any } # ordered after review-approved
|
|
1261
|
-
command: { remove: [review], markerState: … }
|
|
1262
|
-
prompt: grilling-agent.md # re-grill with the human's diff
|
|
1263
|
-
```
|
|
1264
|
-
|
|
1265
|
-
Readability bonus: `git log --oneline` becomes a legible state trace — every
|
|
1266
|
-
commit names the state it entered, with no second grammar to decode.
|
|
1267
|
-
|
|
1268
|
-
### B.5 Assessment
|
|
1269
|
-
|
|
1270
|
-
**This is a better core model than §3.6.** It replaces four rule families, two
|
|
1271
|
-
commit namespaces, and three hand-curated guard sets with one classifier, and
|
|
1272
|
-
its default failure mode is the safe one (unmatched = inert or refused, never
|
|
1273
|
-
state-consuming). The earlier building blocks don't disappear — actors,
|
|
1274
|
-
artifacts, probes, counters, params, prompts, the primitive toolbox, and the
|
|
1275
|
-
compiler all survive unchanged — but §3.5's state schema and §3.6's four rule
|
|
1276
|
-
families merge into a single `states` block whose entries are _(from, actor,
|
|
1277
|
-
when) → command | prompt_. Roughly: eleven building blocks become seven.
|
|
1278
|
-
|
|
1279
|
-
What it trades away, named plainly:
|
|
1280
|
-
|
|
1281
|
-
- **File presence stops being a second source of truth.** Today a committed
|
|
1282
|
-
ERRORS.md forces escalate no matter what HEAD says — belt and suspenders. Here
|
|
1283
|
-
the label is the single truth; a repo whose labels are wrong (manual history
|
|
1284
|
-
surgery) degrades differently (boundary-skip to an older label) rather than
|
|
1285
|
-
being caught by a file check. The compiler can keep conflict declarations as
|
|
1286
|
-
_lint_ (refuse a commit that would create HEALTH.md alongside packages)
|
|
1287
|
-
without them steering resolution.
|
|
1288
|
-
- **More states.** Alternating-actor gates split into explicit per-actor states
|
|
1289
|
-
(`grilling-draft` / `grilling-answer`), and marker states appear. The graph is
|
|
1290
|
-
longer but each node is smaller and self-describing — a good trade for a
|
|
1291
|
-
_configurable_ machine, where node count is cheap and implicit semantics are
|
|
1292
|
-
expensive.
|
|
1293
|
-
- **A subtler `from`-set discipline.** Mislabeling a predecessor list produces a
|
|
1294
|
-
dead rule; totality/reachability checking (§3.11) stops being nice-to-have and
|
|
1295
|
-
becomes the product.
|
|
1296
|
-
|
|
1297
|
-
Migration consequence: this model is what the **compiler should target** —
|
|
1298
|
-
§3.6's families were designed to mirror the existing engine's passes, but the
|
|
1299
|
-
existing passes are the accident, not the spec. Phase 1 (table extraction,
|
|
1300
|
-
golden equivalence tests) is unchanged; phase 2's interpreter should implement
|
|
1301
|
-
the chain model directly, with the current 21-state behavior expressed in it as
|
|
1302
|
-
the shipped default and the equivalence suite pinning the semantics.
|
|
1303
|
-
|
|
1304
|
-
## Appendix C: the purity test — `next = δ(label, diff)` and nothing else
|
|
1305
|
-
|
|
1306
|
-
> Third-round exploration: what if the next state were **only** a function of
|
|
1307
|
-
> the last commit label and the current diff? What has to change, and where does
|
|
1308
|
-
> it fall down? (Reference point: the guard-input taxonomy — HEAD subject,
|
|
1309
|
-
> HEAD's own diff, folded history, config, content probes, invoker — of which
|
|
1310
|
-
> only tree/diff facts are δ-compatible today.)
|
|
1311
|
-
|
|
1312
|
-
### C.1 The target
|
|
1313
|
-
|
|
1314
|
-
```
|
|
1315
|
-
δ(label(nearest labeled commit), pending diff) → next label // then commit it
|
|
1316
|
-
```
|
|
1317
|
-
|
|
1318
|
-
One reading choice up front: "last commit label" means the nearest **labeled**
|
|
1319
|
-
commit, skipping unlabeled (boundary) commits — the same walk `lastWorkflowTurn`
|
|
1320
|
-
does today. This single concession keeps rider commits (config fixes,
|
|
1321
|
-
operational recovery) survivable; without it, any manual commit mid-cycle
|
|
1322
|
-
destroys the state.
|
|
1323
|
-
|
|
1324
|
-
### C.2 What has to change — five moves
|
|
1325
|
-
|
|
1326
|
-
**Move 1: split states until the label carries every read-time branch.** Today
|
|
1327
|
-
`gtd(human): grilling` means accept-defaults OR another round, decided by
|
|
1328
|
-
inspecting the turn's own diff at the NEXT resolution (a "past diff" —
|
|
1329
|
-
inadmissible). Under δ, the decision must be made at capture time, when the
|
|
1330
|
-
empty diff IS the current diff, and encoded in the label:
|
|
1331
|
-
`gtd(human): grilling-answered` vs `gtd(human): grilling-accepted`. Same for the
|
|
1332
|
-
human review turn (`review-approved` / `review-feedback` at capture, from the
|
|
1333
|
-
checkbox-only/deletion diff shape). The ~21 states become ~30, each label
|
|
1334
|
-
unambiguous.
|
|
1335
|
-
|
|
1336
|
-
**Move 2: config decisions move to write time.** `agenticReview` off, or the
|
|
1337
|
-
review-fix threshold reached, currently changes what `gtd: tests-green` _means_
|
|
1338
|
-
at read time. Under δ, config can't feed the transition — so the check action
|
|
1339
|
-
(which runs with config in hand) writes the decided label directly: green →
|
|
1340
|
-
`gtd: agentic-review` or `gtd: close-package`, chosen when acting. Semantic
|
|
1341
|
-
shift: flipping a kill-switch no longer affects an in-flight rest (the label
|
|
1342
|
-
already committed the decision); config is read at decision points, not on every
|
|
1343
|
-
resolution. Arguably more predictable — but a behavior change.
|
|
1344
|
-
|
|
1345
|
-
**Move 3: counters move into the labels.** `test-failed` below vs at cap is a
|
|
1346
|
-
fold over history — inadmissible. Under δ the label carries the counter:
|
|
1347
|
-
`gtd: test-failed 2/3`. The writer computes it from the PREVIOUS label's counter
|
|
1348
|
-
(+1), so the value is derivable from `(label, diff)` alone and
|
|
1349
|
-
crash-recomputable. Cost: budgets become wire format (changing `fixAttemptCap`
|
|
1350
|
-
semantics is a grammar change), and any counter that spans several states
|
|
1351
|
-
(reviewFixCount survives fixing → building → tests-green) must be carried
|
|
1352
|
-
through every intermediate label — a state vector in every subject line.
|
|
1353
|
-
|
|
1354
|
-
**Move 4: content probes become diff matchers — and mostly improve.** The three
|
|
1355
|
-
probes are largely diff-expressible already: FEEDBACK.md-empty is "adds the file
|
|
1356
|
-
with zero content lines"; checkbox-only is literally a diff shape;
|
|
1357
|
-
template-unmodified becomes "the pending diff does not touch SQUASH_MSG.md"
|
|
1358
|
-
(inert) vs "touches it" (proceed) — arguably _cleaner_ than content comparison.
|
|
1359
|
-
Narrow loss: a rider-committed real squash message can no longer be consumed by
|
|
1360
|
-
an empty turn (today the content check allows it); under δ an untouched file is
|
|
1361
|
-
indistinguishable from an untouched template.
|
|
1362
|
-
|
|
1363
|
-
**Move 5: the external world enters only through diffs.** A test run's outcome
|
|
1364
|
-
is neither label nor diff — but its EFFECT is: the check writes
|
|
1365
|
-
FEEDBACK.md/ERRORS.md/nothing, and that machine-authored write re-enters δ as
|
|
1366
|
-
the next transition's diff (plus the outcome label from move 2). This
|
|
1367
|
-
generalizes to an invariant the engine must enforce: **every effect must
|
|
1368
|
-
materialize as a label or a file change, or it is invisible to the machine.**
|
|
1369
|
-
|
|
1370
|
-
With those five moves, the interrupt and fallback ladders genuinely dissolve:
|
|
1371
|
-
they exist today because labels under-determine state (a fresh FEEDBACK.md is
|
|
1372
|
-
newer than HEAD; ERRORS.md outlives its commit's subject). When every write
|
|
1373
|
-
lands with its decided label, file presence never disagrees with the label.
|
|
1374
|
-
Entry points survive as δ's rules from the `start` pseudo-label (no labeled
|
|
1375
|
-
commit found): `(start, adds .gtd/HEALTH.md) → health-entry`, etc.
|
|
1376
|
-
|
|
1377
|
-
### C.3 A traced scenario
|
|
1378
|
-
|
|
1379
|
-
```
|
|
1380
|
-
label (nearest) pending diff at invocation δ → committed label
|
|
1381
|
-
─────────────────────── ───────────────────────────────── ─────────────────────────────
|
|
1382
|
-
(start) adds notes.md, .gtd/TODO.md gtd(human): grilling-entry
|
|
1383
|
-
grilling-entry touches .gtd/TODO.md gtd(agent): grilling-draft
|
|
1384
|
-
grilling-draft (empty — accept defaults) gtd(human): grilling-accepted
|
|
1385
|
-
grilling-accepted machine seed: +ARCH.md −TODO.md gtd: architecting
|
|
1386
|
-
… … …
|
|
1387
|
-
building (pkg 01) code changes gtd(agent): building
|
|
1388
|
-
building turn check writes FEEDBACK.md gtd: test-failed 1/3
|
|
1389
|
-
test-failed 1/3 code changes (the fix) gtd(agent): fixing
|
|
1390
|
-
fixing turn check writes nothing (green) gtd: agentic-review ← config+count decided HERE
|
|
1391
|
-
agentic-review adds empty FEEDBACK.md gtd: close-package ← emptiness read from the DIFF
|
|
1392
|
-
close-package machine rm: pkg dir, FEEDBACK.md gtd: review ← reviewable decided at write time
|
|
1393
|
-
```
|
|
1394
|
-
|
|
1395
|
-
Every row is `(label, diff) → label`. The history reads as a complete replay
|
|
1396
|
-
log: anyone (or any tool) can re-derive every state with no counters, no config,
|
|
1397
|
-
no file inspection.
|
|
1398
|
-
|
|
1399
|
-
### C.4 Where it falls down
|
|
1400
|
-
|
|
1401
|
-
1. **Turn attribution.** The human's answer and the agent's iteration can be
|
|
1402
|
-
byte-identical diffs. δ survives ONLY because state-splitting makes every
|
|
1403
|
-
state await exactly one actor — so the actor adds no information to the
|
|
1404
|
-
transition. But the invoker is still required as **authentication** (refuse
|
|
1405
|
-
the wrong actor); δ can be pure while the CLI cannot. And no diff carries
|
|
1406
|
-
authorship: two actors editing before a capture are indistinguishable,
|
|
1407
|
-
exactly as today (pending edits ride along).
|
|
1408
|
-
2. **Action parameters and prompts stay impure.** The squash needs the cycle
|
|
1409
|
-
base SHA; the review gate needs `diff(reviewBase..HEAD)`; grilling inlines
|
|
1410
|
-
the decision log. The _transition_ is δ-pure, but `perform()` and
|
|
1411
|
-
`buildPrompt()` must read history — the machine is pure, the product around
|
|
1412
|
-
it is not. This is a scoping line, not a flaw, but it must be drawn
|
|
1413
|
-
explicitly.
|
|
1414
|
-
3. **Counter-bearing labels are a real wire-format tax.** `gtd: test-failed 2/3`
|
|
1415
|
-
is tolerable; carrying reviewFixCount through five unrelated labels is not
|
|
1416
|
-
pretty, and every budget-semantics tweak becomes a breaking-history change.
|
|
1417
|
-
This is the single strongest argument that the current design (counters
|
|
1418
|
-
folded at read time) is the better trade.
|
|
1419
|
-
4. **Write-time config freezes in-flight decisions.** The kill-switch flip that
|
|
1420
|
-
today force-approves a package resting at agentic-review would do nothing
|
|
1421
|
-
until the next decision point. Defensible, but different.
|
|
1422
|
-
5. **`start` must absorb everything unlabeled.** Post-squash HEADs, fresh repos,
|
|
1423
|
-
and pre-gtd history all resolve to `start` — fine. But a _mid-cycle_ history
|
|
1424
|
-
whose labels are gone (rebase, squash-by-hand) silently restarts instead of
|
|
1425
|
-
erroring; today's corruption checks catch some of this. Strict δ trades
|
|
1426
|
-
"refuse to guess" for "quietly re-enter."
|
|
1427
|
-
|
|
1428
|
-
### C.5 Verdict
|
|
1429
|
-
|
|
1430
|
-
δ-purity is ~90% achievable and the exercise is clarifying: moves 1, 4, and 5
|
|
1431
|
-
(state splitting, probes-as-diff-matchers, effects-as-diffs) would improve the
|
|
1432
|
-
current design and are adoptable incrementally without the rest. Moves 2–3
|
|
1433
|
-
(config and counters into write-time labels) are where purity starts charging
|
|
1434
|
-
rent — wire-format coupling and frozen decisions — and the current
|
|
1435
|
-
read-time-fold design is the deliberate impurity that keeps budgets and
|
|
1436
|
-
kill-switches out of the commit grammar. The irreducible residue is turn
|
|
1437
|
-
authentication (the invoker) and history-reading actions/prompts, which no
|
|
1438
|
-
labeling scheme can absorb because neither authorship nor anchors exist in any
|
|
1439
|
-
single diff.
|