@pmelab/gtd 8.0.0 → 8.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/README.md +490 -110
- package/bin/gtd +104 -20
- package/dist/gtd.bundle.mjs +214 -102
- package/package.json +1 -2
- package/schema.json +33 -9
- package/docs/cli.md +0 -675
- 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 -219
- package/docs/upgrading.md +0 -293
package/docs/configuration.md
DELETED
|
@@ -1,994 +0,0 @@
|
|
|
1
|
-
# Configuration
|
|
2
|
-
|
|
3
|
-
gtd reads an optional `.gtdrc` config file via
|
|
4
|
-
[cosmiconfig](https://github.com/cosmiconfig/cosmiconfig). gtd ships the bundled
|
|
5
|
-
**unified** workflow as its **built-in default**: with no `workflow:` configured
|
|
6
|
-
anywhere in the cwd→home config chain, that default is used automatically, so a
|
|
7
|
-
state command works out of the box with no config at all.
|
|
8
|
-
[`gtd init`](#gtd-init) is optional — it seeds a minimal `.gtdrc.json` with the
|
|
9
|
-
two things most projects tune (a `testCommand` var and a Prettier formatting
|
|
10
|
-
suggestion), not the workflow. To CUSTOMIZE the machine itself, add your own
|
|
11
|
-
[`workflow:`](#the-workflow-key) key. Supported filenames (searched in this
|
|
12
|
-
order):
|
|
13
|
-
|
|
14
|
-
- `.gtdrc`
|
|
15
|
-
- `.gtdrc.json`
|
|
16
|
-
- `.gtdrc.yaml`
|
|
17
|
-
- `.gtdrc.yml`
|
|
18
|
-
- `gtd.config.json`
|
|
19
|
-
- `gtd.config.yaml`
|
|
20
|
-
|
|
21
|
-
## Schema
|
|
22
|
-
|
|
23
|
-
v3's `.gtdrc` has exactly three blessed top-level keys:
|
|
24
|
-
|
|
25
|
-
- **`workflow`** (object, optional) — the whole machine definition (its states,
|
|
26
|
-
plus its own `vars:` defaults and `modes:`), compiled by
|
|
27
|
-
`src/PatternConfig.ts`. Absent = gtd's built-in default (the bundled unified
|
|
28
|
-
workflow) is used. Declare it to fully REPLACE that default with your own
|
|
29
|
-
machine (there is no `extends`/merge). See
|
|
30
|
-
["The `workflow:` key" below](#the-workflow-key) for its schema.
|
|
31
|
-
- **`vars`** (object, optional) — a flat `name -> scalar` map, one layer of the
|
|
32
|
-
merged `it.vars` every template sees — see ["Variables"](#variables) below.
|
|
33
|
-
- **`modes`** (object, optional) — steering-file modes (`format:`/`validate:`
|
|
34
|
-
shell commands), layered over the active workflow's own `modes:` and gtd's
|
|
35
|
-
built-in validators, so a project can plug its formatter or linter into the
|
|
36
|
-
configured workflow without re-declaring that mode on the workflow itself. See
|
|
37
|
-
["`modes:`"](#modes--pluggable-steering-file-modes) below.
|
|
38
|
-
- **`$schema`** (string, optional) — stripped before validation, so it never
|
|
39
|
-
counts as an unknown key. Point it at the published schema for editor-backed
|
|
40
|
-
autocompletion. A `schema.json` is generated from `src/ConfigSchema.ts` at
|
|
41
|
-
build time and ships with the package.
|
|
42
|
-
|
|
43
|
-
Any other top-level key is **rejected** (`onExcessProperty: "error"`) — v3 has
|
|
44
|
-
no `testCommand`, `fixAttemptCap`, `reviewThreshold`, `agenticReview`, `squash`,
|
|
45
|
-
`learning`, `decisionLog`, or `models` keys; all of that machinery is gone (see
|
|
46
|
-
[Upgrading](upgrading.md)). The engine blesses no VARIABLE NAMES either —
|
|
47
|
-
`testCommand` (the bundled template's own var, see ["Variables"](#variables)) is
|
|
48
|
-
workflow-authored data like any other `it.vars` entry, not a special key gtd
|
|
49
|
-
interprets.
|
|
50
|
-
|
|
51
|
-
## The `workflow:` key
|
|
52
|
-
|
|
53
|
-
> Authoring or editing a workflow with a coding agent?
|
|
54
|
-
> `skills/authoring/SKILL.md` is the agent-facing contract for producing a valid
|
|
55
|
-
> `workflow:` — the state model, pattern grammar, load-time rules, and how to
|
|
56
|
-
> verify a change compiles. This section is the exhaustive schema reference
|
|
57
|
-
> behind it.
|
|
58
|
-
|
|
59
|
-
A declared `workflow:` key fully REPLACES gtd's built-in default — there is no
|
|
60
|
-
`extends`, no merge-over-a-built-in. The built-in default is itself a YAML asset
|
|
61
|
-
(`src/workflows/unified.yaml`) compiled through the exact same compiler
|
|
62
|
-
(`compileWorkflowConfig`) your own `workflow:` value goes through — no
|
|
63
|
-
privileged code path — so you can materialize it into config form as a starting
|
|
64
|
-
point to edit (`renderInitConfig` in `src/workflows/templates.ts`). Its shape:
|
|
65
|
-
|
|
66
|
-
```yaml
|
|
67
|
-
workflow:
|
|
68
|
-
vars: # optional — the workflow's own declared `it.vars` defaults (see "Variables" below)
|
|
69
|
-
anyKey: anyScalarValue
|
|
70
|
-
modes: # optional — steering-file modes a state's `mode:` may name (see "modes:" below)
|
|
71
|
-
<name>:
|
|
72
|
-
format: <shell command> # at least one of format/validate
|
|
73
|
-
validate: <shell command>
|
|
74
|
-
states:
|
|
75
|
-
<name>:
|
|
76
|
-
actor: <string> # forbidden on a commit state, required otherwise
|
|
77
|
-
script: <string> # exactly one of script/prompt/message/commit
|
|
78
|
-
prompt: <string>
|
|
79
|
-
message: <string>
|
|
80
|
-
commit: <string>
|
|
81
|
-
on: # a mapping, DECLARATION ORDER PRESERVED
|
|
82
|
-
"<pattern>": <targetState> # short form
|
|
83
|
-
"<pattern>": { to: <targetState>, describe: <sentence> } # with a human-readable route description
|
|
84
|
-
initial: true # exactly one state across the whole workflow
|
|
85
|
-
retry:
|
|
86
|
-
max: <number>
|
|
87
|
-
otherwise: <targetState>
|
|
88
|
-
model: <string> # optional, opaque harness hint — forbidden on a commit state
|
|
89
|
-
memory: <string> # optional, opaque memory-scope label — forbidden on a commit state
|
|
90
|
-
file: <string> # optional, an Eta template naming the state's steering file — forbidden on a commit state
|
|
91
|
-
mode: <modeName> # optional, requires "file" — a built-in (qa/review/prose) or a `modes:` entry; forbidden on a commit state
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
See [STATES.md](../STATES.md#1-the-model) for what each field means to the
|
|
95
|
-
engine, and [§3](../STATES.md#3-pattern-grammar) for the pattern grammar.
|
|
96
|
-
|
|
97
|
-
### Content values: inline or a file reference
|
|
98
|
-
|
|
99
|
-
A `script`/`prompt`/`message`/`commit` value starting with `./` or `../` is a
|
|
100
|
-
**file reference** — resolved relative to **the directory of the `.gtdrc` that
|
|
101
|
-
declared it** (not the directory `gtd` runs from) and read at load time. A
|
|
102
|
-
missing or unreadable file reference is a **load error**, collected and thrown
|
|
103
|
-
with every other config problem — never silently treated as inline text. Any
|
|
104
|
-
other string (including one that merely contains a `/`, or an absolute path) is
|
|
105
|
-
inline Eta template source, used verbatim.
|
|
106
|
-
|
|
107
|
-
Because references anchor to the declaring file, a `.gtdrc` and its
|
|
108
|
-
`gtd-prompts/` (or `prompts/`) directory can live **outside** the repo — e.g. in
|
|
109
|
-
a parent directory shared across several repos. gtd still runs from the repo
|
|
110
|
-
root, discovers the parent `.gtdrc` by walking up the directory chain (see
|
|
111
|
-
["Lookup and precedence"](#lookup-and-precedence) — cwd→home), and inlines each
|
|
112
|
-
level's `./`-relative references against that level's own directory. When
|
|
113
|
-
workflows are layered across levels, every reference resolves against the file
|
|
114
|
-
that wrote it, so a parent's `./gtd-prompts/x.md` and a child's never collide.
|
|
115
|
-
(This is a capability of a `.gtdrc` you write yourself — `gtd init` seeds only
|
|
116
|
-
`vars:`/`modes:`, not a `workflow:` with file references.)
|
|
117
|
-
|
|
118
|
-
```yaml
|
|
119
|
-
workflow:
|
|
120
|
-
states:
|
|
121
|
-
working:
|
|
122
|
-
actor: agent
|
|
123
|
-
prompt: ./prompts/working.md # read from alongside this .gtdrc
|
|
124
|
-
on:
|
|
125
|
-
"* **": done
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
A `vars:` key sibling to `states:` inside `workflow:` declares the workflow's
|
|
129
|
-
own defaults for `it.vars` — see ["Variables"](#variables) below for the full
|
|
130
|
-
three-layer picture.
|
|
131
|
-
|
|
132
|
-
### `on:` values — a target, or a `{ to, describe }` route description
|
|
133
|
-
|
|
134
|
-
An `on` row's value is normally the target state name. It may instead be an
|
|
135
|
-
object `{ to: <target>, describe: <sentence> }`, attaching a human-readable
|
|
136
|
-
`describe` — a plain sentence explaining what making that kind of change does
|
|
137
|
-
next. `describe` is **inert to the engine**: it plays no part in matching, is
|
|
138
|
-
never Eta-rendered — UNLIKE the pattern key itself, which IS an Eta template
|
|
139
|
-
(see below) — and never affects a decision. It exists only to be surfaced. A
|
|
140
|
-
state's own edges are handed to its content template as `it.edges` (an array of
|
|
141
|
-
`{ pattern, target, describe? }`, `pattern` already rendered, in declaration
|
|
142
|
-
order), so a human gate's `message:` can render a "what each change does next"
|
|
143
|
-
list from the same routing the engine uses — one source of truth:
|
|
144
|
-
|
|
145
|
-
```yaml
|
|
146
|
-
workflow:
|
|
147
|
-
states:
|
|
148
|
-
await-review:
|
|
149
|
-
actor: human
|
|
150
|
-
message: |
|
|
151
|
-
Review the diff, then run `gtd step human`.
|
|
152
|
-
|
|
153
|
-
What each change does next:
|
|
154
|
-
<% it.edges.forEach(function (e) { if (e.describe) { %>
|
|
155
|
-
<%~ "- " + e.describe + "\n" %>
|
|
156
|
-
<% } }) %>
|
|
157
|
-
on:
|
|
158
|
-
"D .gtd/REVIEW.md":
|
|
159
|
-
to: idle
|
|
160
|
-
describe:
|
|
161
|
-
"Delete `.gtd/REVIEW.md` to approve the whole cycle and rest at
|
|
162
|
-
idle."
|
|
163
|
-
"* **":
|
|
164
|
-
to: planning
|
|
165
|
-
describe:
|
|
166
|
-
"Change any source file to leave feedback and start another round."
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
The `<%~ "- " + e.describe + "\n" %>` idiom keeps each bullet's own newline: Eta
|
|
170
|
-
strips the template's line breaks around `<% %>` control tags (its `autoTrim`
|
|
171
|
-
default), not text inside an interpolation. Edges without a `describe` are
|
|
172
|
-
skipped by the `if (e.describe)` guard. `gtd next --json` and
|
|
173
|
-
`gtd status --json` also emit the `edges` array; both omit it when the state has
|
|
174
|
-
no `on` (a commit state), and omit a per-edge `describe` when that edge declares
|
|
175
|
-
none.
|
|
176
|
-
|
|
177
|
-
**The pattern key (everything before the value) is an Eta template, rendered
|
|
178
|
-
against `it.vars` alone** — the same vars-only slice `on` templates could ever
|
|
179
|
-
need, since a pattern only ever names a path, never a diff or commit hash. A
|
|
180
|
-
literal path (`"A .gtd/FEEDBACK.md": fix`) still works unchanged; naming a var
|
|
181
|
-
instead (`"A <%= it.vars.feedbackFile %>": fix`) makes that row track a
|
|
182
|
-
repointed `feedbackFile` the same way `file:` does — see
|
|
183
|
-
["Variables"](#variables).
|
|
184
|
-
|
|
185
|
-
### `model:` — the opaque harness hint, template-rendered
|
|
186
|
-
|
|
187
|
-
A state may declare `model: <string>` — an OPAQUE label (e.g. `smart`, `fast`,
|
|
188
|
-
or a concrete model id) gtd never interprets; it is only passed through so the
|
|
189
|
-
driving loop can map it onto whatever models its agent harness provides. Unset
|
|
190
|
-
means "use the harness's default." Forbidden on a commit state (never at rest,
|
|
191
|
-
emits nothing):
|
|
192
|
-
|
|
193
|
-
```yaml
|
|
194
|
-
workflow:
|
|
195
|
-
states:
|
|
196
|
-
working:
|
|
197
|
-
actor: agent
|
|
198
|
-
model: smart
|
|
199
|
-
prompt: do the thing
|
|
200
|
-
on:
|
|
201
|
-
"* **": done
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
Like every content string, `model:` is rendered as an Eta template through the
|
|
205
|
-
exact same context as the state's content — a plain string with no Eta tags
|
|
206
|
-
(`smart` above) passes through unchanged, but
|
|
207
|
-
`model: "<%= it.vars.reviewModel %>"` resolves against the merged `it.vars` (see
|
|
208
|
-
["Variables"](#variables)). A render failure behaves exactly like a content
|
|
209
|
-
render failure at the same call site: `gtd next`/`gtd status` error out, nothing
|
|
210
|
-
committed.
|
|
211
|
-
|
|
212
|
-
`gtd next --json` and `gtd status --json` include a `"model"` key (the RENDERED
|
|
213
|
-
value) only when the resolved state declares one — it is **omitted entirely**,
|
|
214
|
-
never emitted as `null`, when unset.
|
|
215
|
-
|
|
216
|
-
### `memory:` — the memory-scope label, template-rendered
|
|
217
|
-
|
|
218
|
-
A state may declare `memory: <string>` — an OPAQUE label gtd never interprets;
|
|
219
|
-
it is passed through so a memory-aware driving loop can decide when an agent
|
|
220
|
-
turn continues from the previous turn's memory and when it starts fresh. The
|
|
221
|
-
contract is a **comparison, not a command**: two agent turns that emit the SAME
|
|
222
|
-
`memory` value belong to the same memory scope (the driver retains the agent's
|
|
223
|
-
memory across them), and a change in value — or the very first agent turn — is
|
|
224
|
-
where the driver starts fresh. Unset means "use the harness's default."
|
|
225
|
-
Forbidden on a commit state (never at rest):
|
|
226
|
-
|
|
227
|
-
```yaml
|
|
228
|
-
workflow:
|
|
229
|
-
states:
|
|
230
|
-
planning:
|
|
231
|
-
actor: agent
|
|
232
|
-
memory: plan
|
|
233
|
-
prompt: develop the sketch into a concrete plan
|
|
234
|
-
on:
|
|
235
|
-
"* **": plan-review
|
|
236
|
-
plan-review:
|
|
237
|
-
actor: human
|
|
238
|
-
message: accept the plan, or edit it to iterate
|
|
239
|
-
on:
|
|
240
|
-
"C": building
|
|
241
|
-
"* **": planning
|
|
242
|
-
building:
|
|
243
|
-
actor: agent
|
|
244
|
-
memory: build # a different scope — implementation starts fresh
|
|
245
|
-
prompt: implement the settled plan
|
|
246
|
-
on:
|
|
247
|
-
"* **": done
|
|
248
|
-
done:
|
|
249
|
-
commit: "feat: done"
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
Because `planning` re-enters itself around the review loop, every lap emits
|
|
253
|
-
`memory: plan` and the planner keeps its accumulated exploration; the move to
|
|
254
|
-
`building` emits a different label (`build`), which is where the driver clears
|
|
255
|
-
memory for a fresh implementation turn. A loop of same-labelled turns retains; a
|
|
256
|
-
differently-labelled state at a phase boundary clears. This is exactly how the
|
|
257
|
-
unified template scopes its agent states across the four `plan`/`build`/`fix`/
|
|
258
|
-
`review` memory tiers (see
|
|
259
|
-
[STATES.md §10](../STATES.md#10-the-bundled-workflow-templates)), and how the
|
|
260
|
-
loop driver reads it is spelled out in `skills/loop/SKILL.md`.
|
|
261
|
-
|
|
262
|
-
Like `model`, `memory:` is rendered as an Eta template through the same context
|
|
263
|
-
as the state's content — a plain label (`plan`) passes through unchanged, while
|
|
264
|
-
`memory: "<%= it.vars.planScope %>"` resolves against the merged `it.vars`. A
|
|
265
|
-
render failure behaves exactly like a content render failure:
|
|
266
|
-
`gtd next`/`gtd status` error out, nothing committed.
|
|
267
|
-
`gtd next --json`/`gtd status --json` include a `"memory"` key (the RENDERED
|
|
268
|
-
value) only when the resolved state declares one — **omitted entirely**, never
|
|
269
|
-
`null`, when unset. Plain `gtd status` prints a `Memory:` line (right after
|
|
270
|
-
`Model:`, when present).
|
|
271
|
-
|
|
272
|
-
### `label:` — the opaque display name, template-rendered
|
|
273
|
-
|
|
274
|
-
A state may declare `label: <string>` — an OPAQUE display name gtd never
|
|
275
|
-
interprets; it is only passed through so a driver/viewer can show something
|
|
276
|
-
friendlier than the raw state name. Unset means the state has no display name.
|
|
277
|
-
Forbidden on a commit state (never at rest, emits nothing):
|
|
278
|
-
|
|
279
|
-
```yaml
|
|
280
|
-
workflow:
|
|
281
|
-
states:
|
|
282
|
-
working:
|
|
283
|
-
actor: agent
|
|
284
|
-
label: Implement the feature
|
|
285
|
-
prompt: do the thing
|
|
286
|
-
on:
|
|
287
|
-
"* **": done
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
Like `model`/`memory`, `label:` is rendered as an Eta template through the exact
|
|
291
|
-
same context as the state's content — a plain string with no Eta tags
|
|
292
|
-
(`Implement the feature` above) passes through unchanged, but
|
|
293
|
-
`label: "<%= it.vars.stateLabel %>"` resolves against the merged `it.vars` (see
|
|
294
|
-
["Variables"](#variables)). A render failure behaves exactly like a content
|
|
295
|
-
render failure at the same call site: `gtd next`/`gtd status` error out, nothing
|
|
296
|
-
committed.
|
|
297
|
-
|
|
298
|
-
`gtd next --json` and `gtd status --json` include a `"label"` key (the RENDERED
|
|
299
|
-
value) only when the resolved state declares one — it is **omitted entirely**,
|
|
300
|
-
never emitted as `null`, when unset. A driver/viewer choosing to fall back to
|
|
301
|
-
the raw state name when `label` is absent is that consumer's own fallback, not
|
|
302
|
-
something gtd itself does.
|
|
303
|
-
|
|
304
|
-
### `file:`/`mode:` — the steering-file association
|
|
305
|
-
|
|
306
|
-
A state may additionally declare `file:` — an Eta template naming THE steering
|
|
307
|
-
file this state is about: the file a human/editor should look at while the
|
|
308
|
-
machine rests here (rendered through the same `it.vars`-carrying context as
|
|
309
|
-
content/`model`; must render non-empty). Forbidden on a commit state (never at
|
|
310
|
-
rest). Multiple states may share one `file:` (and, in the unified template, do):
|
|
311
|
-
|
|
312
|
-
```yaml
|
|
313
|
-
workflow:
|
|
314
|
-
vars:
|
|
315
|
-
planFile: .gtd/PLAN.md
|
|
316
|
-
states:
|
|
317
|
-
working:
|
|
318
|
-
actor: agent
|
|
319
|
-
file: <%= it.vars.planFile %>
|
|
320
|
-
mode: qa
|
|
321
|
-
prompt: develop the plan
|
|
322
|
-
on:
|
|
323
|
-
"* **": done
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
`mode:` requires a sibling `file:` and names the file's FORMAT — a
|
|
327
|
-
**steering-file mode**. Two names are BUILT IN, each carrying a VALIDATOR gtd
|
|
328
|
-
implements itself:
|
|
329
|
-
|
|
330
|
-
| `mode` | Format |
|
|
331
|
-
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
332
|
-
| `qa` | The open-questions format (`## Open Questions` near the top and `## Answered Questions` at the bottom, one `###` sub-heading per question with a free-form body; status is positional, no marker line). The bundled advanced flow additionally writes each open question as a checkbox list (candidate answers + a `- [ ] _your answer_` slot) and gates the human's answer step with `answerGate` (exactly one tick per question) — but that convention + gate live in the workflow/edge, not in this validator, which only checks the structure above. |
|
|
333
|
-
| `review` | The checkbox review format (`# Review: <hash>` header, `<!-- base: <hash> -->` comment, `##` chunks, `- [ ]` pointers). |
|
|
334
|
-
|
|
335
|
-
A THIRD name, `prose`, is also built in but ships **no validator at all** — a
|
|
336
|
-
known mode name a state's `mode:` may use with no `modes:` declaration, that
|
|
337
|
-
formats (once a `modes:` layer gives it a `format:` command) but never
|
|
338
|
-
validates. It is for a curated, free-form document with no gtd-side schema — the
|
|
339
|
-
bundled template's simple-flow plan file (`.gtd/TODO.md` at
|
|
340
|
-
`planning`/`plan-review`) uses it.
|
|
341
|
-
|
|
342
|
-
Any other name must be declared in a `modes:` map (next section) — as must a
|
|
343
|
-
`format:` command for any of the three built-ins, since gtd ships no formatter.
|
|
344
|
-
A `mode:` naming neither a built-in nor a declared mode is a load error listing
|
|
345
|
-
both sets (typos must not silently disable the format gate or editor support).
|
|
346
|
-
|
|
347
|
-
What a mode buys the state: before capturing a turn out of it, `gtd step`
|
|
348
|
-
formats that file in place and validates it, refusing the step when it is
|
|
349
|
-
invalid (`gtd validate` runs the same check on demand — see
|
|
350
|
-
[STATES.md §12](../STATES.md) and [docs/cli.md](cli.md#gtd-validate)). The
|
|
351
|
-
ENGINE itself never branches on `mode`; the edge does, and `gtd lsp` (see
|
|
352
|
-
[docs/design/state-file-association.md](design/state-file-association.md)) reads
|
|
353
|
-
it to decide which document symbols/code actions/diagnostics a file gets — for
|
|
354
|
-
the built-in names only.
|
|
355
|
-
|
|
356
|
-
`gtd next --json`/`gtd status --json` gain `"file"` (the RENDERED path) and
|
|
357
|
-
`"mode"` (verbatim) keys, each **omitted entirely** (never `null`) when the
|
|
358
|
-
resolved state declares none — exactly like `model`. Plain `gtd status` prints
|
|
359
|
-
`File:`/`Mode:` lines (right after `Model:`, when present) when set.
|
|
360
|
-
|
|
361
|
-
**`on` pattern keys are Eta templates too.** A workflow's `on` patterns may name
|
|
362
|
-
a filename var the same way `file:`/content does — e.g.
|
|
363
|
-
`"A <%= it.vars.feedbackFile %>": fix` instead of a literal
|
|
364
|
-
`"A .gtd/FEEDBACK.md": fix`. The edge (`src/Edge.ts`'s `renderOnEdges`) renders
|
|
365
|
-
every pattern's key against `it.vars` ALONE (no diffs/commit hashes — a pattern
|
|
366
|
-
only ever needs to name a path) before handing the edges to the pure engine,
|
|
367
|
-
which only ever matches plain, already-rendered strings. This is what lets a
|
|
368
|
-
repointed filename var (`.gtdrc`'s top-level `vars:`, or a `GTD_` override)
|
|
369
|
-
reroute the machine consistently: `file:` (and any template reading/ writing
|
|
370
|
-
that path) and the `on` map that decides what a change to it MEANS both follow
|
|
371
|
-
the same var. A pattern written with a literal `.gtd/…` path instead of the var
|
|
372
|
-
still works exactly as before — only a workflow that templates its patterns
|
|
373
|
-
benefits from the repoint.
|
|
374
|
-
|
|
375
|
-
### `modes:` — pluggable steering-file modes
|
|
376
|
-
|
|
377
|
-
A mode is a pair of SHELL COMMANDS over one file: `format:` rewrites it in
|
|
378
|
-
place, `validate:` decides whether it is well-formed. Declare them beside
|
|
379
|
-
`states:` (the workflow's own modes) and/or under a top-level `modes:` key (the
|
|
380
|
-
project's layer over them), and point any state's `mode:` at the name:
|
|
381
|
-
|
|
382
|
-
```yaml
|
|
383
|
-
workflow:
|
|
384
|
-
modes:
|
|
385
|
-
adr:
|
|
386
|
-
format: "npx prettier --write <%= it.file %>"
|
|
387
|
-
validate: "./scripts/check-adr.sh <%= it.file %>"
|
|
388
|
-
states:
|
|
389
|
-
drafting:
|
|
390
|
-
actor: agent
|
|
391
|
-
prompt: "Write the ADR."
|
|
392
|
-
file: docs/adr/0001.md
|
|
393
|
-
mode: adr
|
|
394
|
-
on:
|
|
395
|
-
"* **": reviewing
|
|
396
|
-
# ...
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
The contract:
|
|
400
|
-
|
|
401
|
-
- **`it.file`** is the already-rendered `file:` path. A command is an Eta
|
|
402
|
-
template over the same context every other template sees (so `it.vars` works
|
|
403
|
-
too) plus that one extra key. A `./`-prefixed command is a COMMAND, never a
|
|
404
|
-
`./`-relative file reference the way a content string is.
|
|
405
|
-
- **`validate:` exits 0 for valid.** A non-zero exit means invalid, and its
|
|
406
|
-
output (stdout then stderr) becomes the findings, one per line — that is what
|
|
407
|
-
`gtd validate` prints and what the `gtd step` gate refuses with. A failing
|
|
408
|
-
command that prints nothing still refuses, with a synthesized finding.
|
|
409
|
-
- **`format:` rewrites the file in place** and runs FIRST, so validation always
|
|
410
|
-
judges the formatted file. A non-zero exit is a HARD error (broken tooling,
|
|
411
|
-
not a malformed file): nothing is validated and nothing is committed.
|
|
412
|
-
- **At least one of the two is required** in any single entry; the half it
|
|
413
|
-
leaves out falls through to the layer beneath (below), or is a no-op.
|
|
414
|
-
- Commands run via `bash -c` from the repository root — the only place gtd
|
|
415
|
-
itself spawns a subprocess (a `script:` state is run by the loop driver, not
|
|
416
|
-
by gtd). A `.gtdrc` is project code: only add commands you would run yourself.
|
|
417
|
-
|
|
418
|
-
#### The layers, half by half
|
|
419
|
-
|
|
420
|
-
`format:` and `validate:` resolve INDEPENDENTLY, each from the first layer that
|
|
421
|
-
declares it:
|
|
422
|
-
|
|
423
|
-
1. the top-level `.gtdrc` `modes:` key (the project's own layer, cwd→home
|
|
424
|
-
deep-merged like any other config key);
|
|
425
|
-
2. the active workflow's own `modes:` map;
|
|
426
|
-
3. for the two VALIDATOR built-in names only, gtd's in-process validator — `qa`
|
|
427
|
-
→ `src/OpenQuestions.ts`, `review` → `src/ReviewDoc.ts`. The third built-in,
|
|
428
|
-
`prose`, has no in-process validator to fall back to, so it validates nothing
|
|
429
|
-
unless a `modes:` layer above declares its own `validate:`. There is no
|
|
430
|
-
built-in formatter at this layer for any of the three: **gtd ships no
|
|
431
|
-
formatter**, so bring your own.
|
|
432
|
-
|
|
433
|
-
So for a state declaring `mode: qa`:
|
|
434
|
-
|
|
435
|
-
| `modes: { qa: … }` | formats with | validates with |
|
|
436
|
-
| -------------------------------- | ------------- | --------------------------- |
|
|
437
|
-
| (nothing declared) | nothing | gtd's open-questions parser |
|
|
438
|
-
| `format: npx prettier --write …` | that command | gtd's open-questions parser |
|
|
439
|
-
| `validate: my-linter …` | nothing | that command |
|
|
440
|
-
| both | its `format:` | its `validate:` |
|
|
441
|
-
|
|
442
|
-
That is what makes the built-in modes extensible rather than all-or-nothing —
|
|
443
|
-
and the top-level key means a project can plug its formatter into the built-in
|
|
444
|
-
default workflow (or its own) without re-declaring a `modes:` block on the
|
|
445
|
-
workflow itself. **`gtd init` seeds exactly this block by default** (the
|
|
446
|
-
Prettier suggestion below), so a fresh repo already formats its steering files —
|
|
447
|
-
edit or drop it to taste. It is what a scaffolded `.gtdrc.json` contains
|
|
448
|
-
(`gtd init` writes no `workflow:` key — the built-in default is used):
|
|
449
|
-
|
|
450
|
-
```jsonc
|
|
451
|
-
// .gtdrc.json — as scaffolded by `gtd init`: a testCommand var and the markdown formatter
|
|
452
|
-
{
|
|
453
|
-
"vars": { "testCommand": "npm test" },
|
|
454
|
-
"modes": {
|
|
455
|
-
"qa": { "format": "npx prettier --write <%= it.file %>" },
|
|
456
|
-
"review": { "format": "npx prettier --write <%= it.file %>" },
|
|
457
|
-
"prose": { "format": "npx prettier --write <%= it.file %>" },
|
|
458
|
-
},
|
|
459
|
-
}
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
**Known limitation — no live editor support for a custom mode.** `gtd lsp`
|
|
463
|
-
publishes diagnostics, document symbols and code actions for the built-in
|
|
464
|
-
`qa`/`review` formats only (gtd never runs a mode command per keystroke over an
|
|
465
|
-
unsaved buffer). A file whose validation is a command — and a `prose` file,
|
|
466
|
-
which has no validator to publish diagnostics for — is still formatted and
|
|
467
|
-
validated by `gtd validate` and the `gtd step` capture gate.
|
|
468
|
-
|
|
469
|
-
### `reviewWindow:`/`reviewBase:` — the review checkout window
|
|
470
|
-
|
|
471
|
-
A state may declare `reviewWindow: true`. While the machine RESTS there, gtd
|
|
472
|
-
opens a **review checkout window**: it rewinds HEAD and the index to the review
|
|
473
|
-
base (`git reset --mixed`) with the working tree untouched, so the whole
|
|
474
|
-
`base..HEAD` diff surfaces as ordinary uncommitted changes in the editor's git
|
|
475
|
-
integration (SCM panel, gutters, per-file diff, discard-hunk). The window closes
|
|
476
|
-
automatically on the next invocation, once the machine rests anywhere else — and
|
|
477
|
-
a reviewer's own edits, made while it was open, become the resting state's
|
|
478
|
-
ordinary pending changes, captured by its `on` patterns like any other diff.
|
|
479
|
-
Forbidden on a commit state (never at rest).
|
|
480
|
-
|
|
481
|
-
The diff base defaults to the current process's start. To narrow it, mark an
|
|
482
|
-
earlier state `reviewBase: true`: the most-recent in-process commit that entered
|
|
483
|
-
such a state becomes the base, so only work committed after that milestone
|
|
484
|
-
surfaces.
|
|
485
|
-
|
|
486
|
-
```yaml
|
|
487
|
-
workflow:
|
|
488
|
-
states:
|
|
489
|
-
# …
|
|
490
|
-
building:
|
|
491
|
-
actor: agent
|
|
492
|
-
reviewBase: true # the diff shown at review starts here…
|
|
493
|
-
prompt: implement the plan
|
|
494
|
-
on:
|
|
495
|
-
"* **": review
|
|
496
|
-
review:
|
|
497
|
-
actor: human
|
|
498
|
-
reviewWindow: true # …and is surfaced while resting here
|
|
499
|
-
message: review the diff in your editor, then `gtd step human`
|
|
500
|
-
on:
|
|
501
|
-
"C": done
|
|
502
|
-
"* **": building
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
The unified template enables `reviewWindow: true` on `await-review` and marks
|
|
506
|
-
`review-deciding` `reviewBase: true`, so the first review covers the whole cycle
|
|
507
|
-
but each subsequent feedback round's window narrows to `last-review..HEAD`. The
|
|
508
|
-
pure engine never observes an open window — it is opened/closed entirely at the
|
|
509
|
-
edge; the real head is preserved under `refs/worktree/gtd/review-head` (the base
|
|
510
|
-
under `refs/worktree/gtd/review-base`) for the window's lifetime — git's
|
|
511
|
-
per-worktree ref namespace, so linked worktrees of one repository each get their
|
|
512
|
-
own window. See [STATES.md §11](../STATES.md) for the full lifecycle.
|
|
513
|
-
|
|
514
|
-
### `reviewEntry:` — the review entry point (`gtd review <commitish>`)
|
|
515
|
-
|
|
516
|
-
A state may separately declare `reviewEntry: true` (at most one across the whole
|
|
517
|
-
workflow; forbidden on a commit state and on the initial state). It marks the
|
|
518
|
-
state [`gtd review <commitish>`](cli.md#gtd-review-commitish---json) enters to
|
|
519
|
-
start a BRAND NEW process reviewing `<commitish>..HEAD` — e.g. a colleague's PR
|
|
520
|
-
branch with no gtd process of its own — reusing whatever `on`/
|
|
521
|
-
`reviewWindow`/`reviewBase` machinery that state and its downstream states
|
|
522
|
-
already declare, with zero duplicated logic.
|
|
523
|
-
|
|
524
|
-
```yaml
|
|
525
|
-
workflow:
|
|
526
|
-
states:
|
|
527
|
-
# …
|
|
528
|
-
review:
|
|
529
|
-
actor: agent
|
|
530
|
-
reviewEntry: true # `gtd review <commitish>` starts a new process here
|
|
531
|
-
prompt: summarize <%~ it.processDiff %> for a human to check
|
|
532
|
-
on:
|
|
533
|
-
"* **": await-review
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
`gtd review <commitish>` requires resting at the workflow's initial state with a
|
|
537
|
-
clean tree, and requires the active workflow to declare a `reviewEntry` state.
|
|
538
|
-
It resolves `<commitish>` (which must name an ancestor of HEAD other than HEAD
|
|
539
|
-
itself) and writes one empty `gtd(human): <review-entry-state>` turn commit
|
|
540
|
-
carrying the resolved hash as a `Gtd-Review-Base:` trailer — read back by
|
|
541
|
-
`computeProcessRun` to override the new process's diff base (`it.startCommit`
|
|
542
|
-
and `it.processDiff` then cover `<commitish>..HEAD`, and a downstream
|
|
543
|
-
`reviewWindow: true` state's checkout window opens over that same range). The
|
|
544
|
-
process's `retry`/squash trace boundary is untouched: the entry commit's own
|
|
545
|
-
parent is a non-workflow commit, so the ordinary boundary rule already stops
|
|
546
|
-
there. See [STATES.md §11](../STATES.md#11-the-review-checkout-window) for the
|
|
547
|
-
full mechanism.
|
|
548
|
-
|
|
549
|
-
The bundled default enables `reviewEntry: true` on `review-start-check` — the
|
|
550
|
-
green-baseline gate in front of `reviewing`.
|
|
551
|
-
|
|
552
|
-
### `fixEntry:` — the fix entry point (`gtd fix`)
|
|
553
|
-
|
|
554
|
-
A state may declare `fixEntry: true` (at most one across the whole workflow;
|
|
555
|
-
forbidden on a commit state and on the initial state — the same rule family as
|
|
556
|
-
`reviewEntry`). It marks the state [`gtd fix`](cli.md#gtd-fix---json) enters to
|
|
557
|
-
start a BRAND NEW process that goes straight into repairing the current failing
|
|
558
|
-
tests.
|
|
559
|
-
|
|
560
|
-
```yaml
|
|
561
|
-
workflow:
|
|
562
|
-
states:
|
|
563
|
-
# …
|
|
564
|
-
fix-check:
|
|
565
|
-
actor: check
|
|
566
|
-
fixEntry: true # `gtd fix` starts a new process here
|
|
567
|
-
script: <%~ it.vars.testCommand %> || echo fail > .gtd/FEEDBACK.md
|
|
568
|
-
on:
|
|
569
|
-
"A .gtd/FEEDBACK.md": fixing # red -> repair loop
|
|
570
|
-
"C": idle # green -> nothing to fix
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
`gtd fix` requires resting at the workflow's initial state with a clean tree,
|
|
574
|
-
and requires the active workflow to declare a `fixEntry` state. Unlike
|
|
575
|
-
`gtd review`, it writes NO `Gtd-Review-Base:` trailer — a fix reviews its own
|
|
576
|
-
fixes from the ordinary process start — so it simply writes one empty
|
|
577
|
-
`gtd(human): <fix-entry-state>` turn commit and lets the workflow's own `on`
|
|
578
|
-
routing take over. The bundled default enables `fixEntry: true` on `fix-check`:
|
|
579
|
-
a red suite drops into the shared `fixing` loop and out through the review +
|
|
580
|
-
squash tail; a green suite is a no-op back to `idle`. See
|
|
581
|
-
[STATES.md §10](../STATES.md#10-the-bundled-workflow-template).
|
|
582
|
-
|
|
583
|
-
### Template variables
|
|
584
|
-
|
|
585
|
-
Every `script`/`prompt`/`message`/`commit`/`model`/`memory`/`file` template is
|
|
586
|
-
rendered as an Eta template (`it.<name>`) with:
|
|
587
|
-
|
|
588
|
-
| Variable | Meaning |
|
|
589
|
-
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
590
|
-
| `startCommit` | The current process's diff base — normally the process start, but re-pointed to `<commitish>`'s hash for a process `gtd review <commitish>` started (see ["`reviewEntry:`"](#reviewentry--the-review-entry-point-gtd-review-commitish)). |
|
|
591
|
-
| `currentCommit` | HEAD's hash at render time. |
|
|
592
|
-
| `previousCommit` | The hash before the last transition (HEAD's parent, in-process). |
|
|
593
|
-
| `state` | The state whose content is being rendered. |
|
|
594
|
-
| `actor` | The actor this render is for. |
|
|
595
|
-
| `processDiff` | `startCommit..HEAD` plus the pending working-tree diff. |
|
|
596
|
-
| `lastDiff` | The diff of the last transition alone. |
|
|
597
|
-
| `processCost` | The accumulated token cost of the current process — the sum of every `Gtd-Cost:` trailer recorded by `gtd step <actor> --cost=<n>` across its turn commits, plus the in-flight step's own `--cost` (so a `commit:` squash template sees the whole-process total). A number, `0` when none recorded. See ["Token cost"](#token-cost). |
|
|
598
|
-
| `processCostByModel` | The same accumulated cost broken down per model: an array of `{ model, cost }`, highest-cost first, one entry per distinct `--model` tag (costs recorded with no `--model` group under `"unspecified"`). A squash template iterates it to itemize the feature's cost per model. Empty when none recorded. See ["Token cost"](#token-cost). |
|
|
599
|
-
| `read(path)` | Reads a working-tree file (pending contents, not HEAD's) by repo-relative path. Throws for a missing/unreadable path — for a `commit:` template, that throw refuses the step (see [STATES.md §8](../STATES.md#8-the-squash-lifecycle)). |
|
|
600
|
-
| `vars` | The merged three-layer variable map, always a flat `Record<string, string>` — see ["Variables"](#variables) below. |
|
|
601
|
-
|
|
602
|
-
### Token cost
|
|
603
|
-
|
|
604
|
-
A loop driver that knows how many tokens the invocation it just drove cost — and
|
|
605
|
-
on which model — can record both with
|
|
606
|
-
`gtd step <actor> --cost=<n> [--model=<name>]` (`<n>` a non-negative number).
|
|
607
|
-
gtd appends them to that turn's commit as a `Gtd-Cost: <n> <model>` trailer — a
|
|
608
|
-
blank line then the trailer, below the untouched `gtd(<actor>): <from> → <to>`
|
|
609
|
-
subject — so the per-turn cost and model are persisted in the git log:
|
|
610
|
-
|
|
611
|
-
```
|
|
612
|
-
gtd(agent): building → reviewing
|
|
613
|
-
|
|
614
|
-
Gtd-Cost: 1450 claude-opus-4-8
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
`--model` is optional and requires `--cost` (a model tag with no token count
|
|
618
|
-
records nothing to sum); a cost recorded without a model is grouped under
|
|
619
|
-
`"unspecified"`. `gtd status` shows the process's running total and, when the
|
|
620
|
-
breakdown adds information, a per-model split (a `Cost:` line with indented
|
|
621
|
-
`<model>: <cost>` lines / a `cost` number and a `costByModel` array, omitted
|
|
622
|
-
when nothing has been recorded). `gtd step --json` echoes what it recorded
|
|
623
|
-
(`{ "state": …, "subject": …, "cost": 1450, "model": "claude-opus-4-8" }`).
|
|
624
|
-
|
|
625
|
-
Every template sees the running total as `it.processCost` and the per-model
|
|
626
|
-
breakdown as `it.processCostByModel` (the table above): the current process's
|
|
627
|
-
turn-commit entries, plus the in-flight step's own `--cost`/`--model`. Their
|
|
628
|
-
intended use is a `commit:` squash template, which renders against the pending
|
|
629
|
-
tree as the process collapses — so the whole feature's total AND its per-model
|
|
630
|
-
itemization (including the squashing step itself) land in the squash message
|
|
631
|
-
even though the intermediate per-turn trailers are discarded with the turns they
|
|
632
|
-
rode on:
|
|
633
|
-
|
|
634
|
-
```yaml
|
|
635
|
-
done:
|
|
636
|
-
commit: |
|
|
637
|
-
feat: ship it
|
|
638
|
-
|
|
639
|
-
Total token cost: <%= it.processCost %>
|
|
640
|
-
<% it.processCostByModel.forEach(function(m){ %>
|
|
641
|
-
- <%= m.model %>: <%= m.cost %>
|
|
642
|
-
<% }) %>
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
`--cost`/`--model` are accepted only by `gtd step` (a usage error on any other
|
|
646
|
-
command), and gtd never interprets the number's unit (tokens, cents, whatever
|
|
647
|
-
the driver records) or the model name — it only records, sums, and groups them.
|
|
648
|
-
|
|
649
|
-
## Sub-machines (`submachines:` / `use:`)
|
|
650
|
-
|
|
651
|
-
Two OPTIONAL top-level keys let you factor repeated or complex clusters of
|
|
652
|
-
states into reusable **sub-machines** that are expanded into ordinary concrete
|
|
653
|
-
states at load time (see [STATES.md §13](../STATES.md) and
|
|
654
|
-
`src/Submachines.ts`). They add NO runtime behavior — the engine only ever sees
|
|
655
|
-
the expanded states — so a workflow authored with them is exactly a workflow
|
|
656
|
-
written flat.
|
|
657
|
-
|
|
658
|
-
```yaml
|
|
659
|
-
workflow:
|
|
660
|
-
submachines:
|
|
661
|
-
gate: # authored once
|
|
662
|
-
params: [onGreen] # names referenced as `$onGreen` below
|
|
663
|
-
states:
|
|
664
|
-
check:
|
|
665
|
-
actor: check
|
|
666
|
-
script: <%~ it.vars.testCommand %>
|
|
667
|
-
on:
|
|
668
|
-
"A .gtd/FEEDBACK.md": blocked # a local state -> renamed by `as:`
|
|
669
|
-
"C": "$onGreen" # a param -> bound by `with:`
|
|
670
|
-
blocked:
|
|
671
|
-
actor: human
|
|
672
|
-
file: <%= it.vars.feedbackFile %>
|
|
673
|
-
message: "fix the baseline"
|
|
674
|
-
on: { "* **": check }
|
|
675
|
-
use:
|
|
676
|
-
- submachine: gate
|
|
677
|
-
as: { check: start-check, blocked: start-blocked } # local -> concrete (omit for identity)
|
|
678
|
-
with: { onGreen: planning } # bind each `$param`
|
|
679
|
-
set: { check: { reviewEntry: true } } # optional extra fields on a renamed state
|
|
680
|
-
states:
|
|
681
|
-
idle:
|
|
682
|
-
{ actor: human, initial: true, message: "…", on: { "* **": start-check } }
|
|
683
|
-
planning: { actor: agent, prompt: "…", on: { "* **": start-check } }
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
Rules:
|
|
687
|
-
|
|
688
|
-
- **`submachines: { <name>: { params, states } }`** — `states` are ordinary
|
|
689
|
-
state definitions, except an `on`/`retry.otherwise` target or any whole field
|
|
690
|
-
value may be a `$param` placeholder.
|
|
691
|
-
- **`use:`** is an ordered list. Each entry names a `submachine:` and may set:
|
|
692
|
-
- **`as:`** — a `local -> concreteName` rename map. Omit it (or a given local)
|
|
693
|
-
for identity. Alias to the names a flat workflow would use to keep history
|
|
694
|
-
and `git log` unchanged.
|
|
695
|
-
- **`with:`** — bindings for the sub-machine's `$param`s.
|
|
696
|
-
- **`set:`** — extra fields merged onto a renamed state (e.g. `reviewEntry`).
|
|
697
|
-
- **Substitution is WHOLE-VALUE only.** `$name` resolves only when it is the
|
|
698
|
-
entire value of a field or target — never a substring — so bash `$x`/`${x}`
|
|
699
|
-
and Eta `<%= it.vars.x %>` inside content are untouched. Pass per-instance
|
|
700
|
-
text (a whole message/prompt/`describe`) as a whole `$name` binding.
|
|
701
|
-
- A target naming one of the sub-machine's own locals is rewritten via `as:`;
|
|
702
|
-
any other target passes through verbatim (a concrete top-level state). `on`
|
|
703
|
-
PATTERN keys are never substituted.
|
|
704
|
-
- Expansion strips `submachines:`/`use:`; the merged `states` are then compiled
|
|
705
|
-
and `validateDefinition`'d normally. Unknown sub-machine, unbound `$param`,
|
|
706
|
-
and generated-name collisions are load-time errors, aggregated with the rest.
|
|
707
|
-
|
|
708
|
-
`gtd init` scaffolds the EXPANDED flat config, so a fresh project never sees
|
|
709
|
-
`submachines:`/`use:` unless it adds them itself. The bundled template is
|
|
710
|
-
authored with them (dedup: `assertGreen`/`makeGreen`/`qaLoop`; comprehension:
|
|
711
|
-
`planLoop`/`humanReview`/`specGate`/`packageLoop`) and its golden test pins that
|
|
712
|
-
expansion is byte-identical to the flat form.
|
|
713
|
-
|
|
714
|
-
## Variables
|
|
715
|
-
|
|
716
|
-
Every template — `script`/`prompt`/`message`/`commit`, and `model`/`memory`/
|
|
717
|
-
`file` — sees `it.vars`: a flat `Record<string, string>` assembled from three
|
|
718
|
-
layers, **later wins**:
|
|
719
|
-
|
|
720
|
-
1. **The workflow's own `vars:` key** (sibling to `states:`, shown above) — the
|
|
721
|
-
workflow author's declared defaults. The unified template declares
|
|
722
|
-
`vars: { testCommand: "npm test" }`, read by `checking`'s script as
|
|
723
|
-
`<%~ it.vars.testCommand %>` (see
|
|
724
|
-
[STATES.md §10](../STATES.md#10-the-bundled-workflow-templates)). The bundled
|
|
725
|
-
template also declares two model-tier vars — `plannerModel` (default `smart`)
|
|
726
|
-
and `coderModel` (default `base`) — that every agent state's `model:`
|
|
727
|
-
resolves through (`model: <%= it.vars.plannerModel %>`), so repointing one
|
|
728
|
-
var here (or via layer 2/3 below) changes every state on that tier at once.
|
|
729
|
-
2. **A top-level `.gtdrc` `vars:` key** (a sibling of `workflow:`, NOT nested
|
|
730
|
-
inside it) — per-repo tuning without redefining the whole workflow. Subject
|
|
731
|
-
to the same cwd→home deep merge as everything else in `.gtdrc` (innermost
|
|
732
|
-
wins per-key).
|
|
733
|
-
3. **`GTD_<UPPERCASE-name>` environment variables** — highest precedence,
|
|
734
|
-
checked at every invocation, case-insensitively against each name already
|
|
735
|
-
declared by layer 1 or 2: `GTD_TESTCOMMAND` overrides `testCommand`. Unlike
|
|
736
|
-
the other two layers, the environment can only OVERRIDE a name a config layer
|
|
737
|
-
already declared — an uppercased env key can't round-trip back to an
|
|
738
|
-
arbitrary camelCase name, so a `GTD_*` var matching no declared name is
|
|
739
|
-
silently ignored.
|
|
740
|
-
|
|
741
|
-
Values in layers 1–2 must be YAML scalars (string/number/boolean) — coerced to
|
|
742
|
-
strings at load time; an object or array value is a load error, collected
|
|
743
|
-
alongside every other config-shape finding (see
|
|
744
|
-
["Validation and errors"](#validation-and-errors)). Environment values are
|
|
745
|
-
already strings.
|
|
746
|
-
|
|
747
|
-
The bundled template also declares `stateDir: .gtd` — where its check scripts
|
|
748
|
-
keep their own scratch/bookkeeping (the `.check-output` temp file,
|
|
749
|
-
`review-deciding`'s exclusion of gtd's other state files when deciding whether
|
|
750
|
-
the human hand-edited code this round). It is independent of the per-file vars
|
|
751
|
-
(`todoFile`, `feedbackFile`, `reviewFile`, …): relocating one steering file
|
|
752
|
-
outside `stateDir` needs no `stateDir` change (each check script creates that
|
|
753
|
-
file's own parent directory as needed), and a project relocating its state
|
|
754
|
-
wholesale sets both `stateDir` and the file vars it cares about.
|
|
755
|
-
|
|
756
|
-
```yaml
|
|
757
|
-
# .gtdrc — overriding the unified template's testCommand
|
|
758
|
-
vars:
|
|
759
|
-
testCommand: npm run test:ci
|
|
760
|
-
```
|
|
761
|
-
|
|
762
|
-
```bash
|
|
763
|
-
# highest precedence — beats both the workflow default and the .gtdrc value above.
|
|
764
|
-
# The var is read at render time, so it shows up in the script `gtd next` prints
|
|
765
|
-
# (and in what the loop driver then executes).
|
|
766
|
-
GTD_TESTCOMMAND="npm run test -- --bail" gtd next
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
A template reads any of it as `it.vars.<name>`:
|
|
770
|
-
|
|
771
|
-
```yaml
|
|
772
|
-
workflow:
|
|
773
|
-
vars:
|
|
774
|
-
reviewer: alice
|
|
775
|
-
states:
|
|
776
|
-
working:
|
|
777
|
-
actor: agent
|
|
778
|
-
prompt: "Assigned reviewer: <%= it.vars.reviewer %>"
|
|
779
|
-
model: "<%= it.vars.reviewModel %>" # a GTD_REVIEWMODEL override, or a .gtdrc vars: entry
|
|
780
|
-
on:
|
|
781
|
-
"* **": done
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
No variable name is blessed by the engine — `testCommand` is workflow-authored
|
|
785
|
-
data like any other `it.vars` entry, not a special key gtd interprets; a custom
|
|
786
|
-
workflow is free to declare and read any names it likes.
|
|
787
|
-
|
|
788
|
-
## Validation and errors
|
|
789
|
-
|
|
790
|
-
Config-shape problems (unknown keys, wrong types, unreadable file references)
|
|
791
|
-
are collected together; if the shape is clean, the assembled definition is
|
|
792
|
-
additionally run through the engine's own `validateDefinition` (see
|
|
793
|
-
[STATES.md §9](../STATES.md#9-validation)). A bad config throws **one** error
|
|
794
|
-
listing every finding, at load time — before anything touches the repository —
|
|
795
|
-
never partially, and never deferred to step time:
|
|
796
|
-
|
|
797
|
-
```
|
|
798
|
-
workflow config:
|
|
799
|
-
- state "idle": must declare exactly one of script/prompt/message/commit (found 2)
|
|
800
|
-
- state "idle": "on" target "nowhere" is not a defined state
|
|
801
|
-
```
|
|
802
|
-
|
|
803
|
-
`validateDefinition`'s findings include the **semantic graph checks**: every
|
|
804
|
-
`on` target and `retry.otherwise` must name a defined state, and every state
|
|
805
|
-
must be **reachable** from the initial state by walking `on` targets and
|
|
806
|
-
`retry.otherwise` redirects — an unreachable state (typically a typo'd rename or
|
|
807
|
-
a leftover) is a load error like any other, since a workflow is bound to a
|
|
808
|
-
project and edited as a project-wide change.
|
|
809
|
-
|
|
810
|
-
Many of these problems never reach gtd at all if your editor validates against
|
|
811
|
-
the published schema: `schema.json` fully types the `workflow:` key (state
|
|
812
|
-
shape, content kinds, `on`/`retry` structure, the `modes:` map), so a
|
|
813
|
-
yaml-language-server-style editor flags unknown keys and wrong types as you
|
|
814
|
-
type. The rules JSON Schema cannot express — exactly one content kind, exactly
|
|
815
|
-
one `initial: true`, targets naming defined states, reachability — remain the
|
|
816
|
-
compiler's job at load time.
|
|
817
|
-
|
|
818
|
-
Other load failures:
|
|
819
|
-
|
|
820
|
-
- **Parse errors** (malformed YAML/JSON) — message includes the offending
|
|
821
|
-
filename.
|
|
822
|
-
- **Non-object top-level** — a YAML list or `null` at the root is rejected with
|
|
823
|
-
the filename in the message.
|
|
824
|
-
- **Unknown top-level key** — anything besides
|
|
825
|
-
`workflow`/`vars`/`modes`/`$schema` emits
|
|
826
|
-
`Invalid gtd config: <field>: <reason>`.
|
|
827
|
-
- **A bad top-level `vars:` entry** — an object/array value fails the same way
|
|
828
|
-
as a bad workflow-level `vars:` entry (see ["Variables"](#variables)), one
|
|
829
|
-
aggregated error:
|
|
830
|
-
`gtd config:\n - "vars.<name>" must be a string, number, or boolean, got <type>`.
|
|
831
|
-
|
|
832
|
-
All of these exit **1** and write to **stderr**, never stdout.
|
|
833
|
-
|
|
834
|
-
## Lookup and precedence
|
|
835
|
-
|
|
836
|
-
gtd walks from the current working directory **up to your home directory** (or
|
|
837
|
-
to the filesystem root when cwd is outside home), collecting every `.gtdrc` it
|
|
838
|
-
finds along the way. All found levels are **deep-merged**, with the **innermost
|
|
839
|
-
(cwd) config winning** on conflicts — so a shared `.gtdrc` in a worktree-parent
|
|
840
|
-
directory cascades to every checkout beneath it, while any individual checkout
|
|
841
|
-
can still override with its own `.gtdrc`.
|
|
842
|
-
|
|
843
|
-
## `gtd init`
|
|
844
|
-
|
|
845
|
-
gtd ships the bundled unified workflow as its **built-in default**, so a state
|
|
846
|
-
command works out of the box with **no** config and no init at all. `gtd init`
|
|
847
|
-
is therefore optional — it just seeds the two things most projects tune:
|
|
848
|
-
|
|
849
|
-
```bash
|
|
850
|
-
gtd init # takes no argument
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
`gtd init` writes a **minimal** `.gtdrc.json` at the repository root — a
|
|
854
|
-
`$schema` link (so editors pick up completion/validation), a
|
|
855
|
-
`vars: { "testCommand": "npm test" }` entry (the var the built-in workflow's
|
|
856
|
-
check script reads), and a ready-to-edit `modes:` formatting suggestion (below).
|
|
857
|
-
It writes **no** `workflow:` key — the built-in default is used until you
|
|
858
|
-
declare your own. It takes **no argument** — passing one is a usage error
|
|
859
|
-
(`gtd init: too many arguments — init takes no argument`).
|
|
860
|
-
|
|
861
|
-
`gtd init` writes only config — it derives no git state — so it need not run in
|
|
862
|
-
a git repository. It runs at a **repository root** or in a directory **outside
|
|
863
|
-
any repository** (scaffolding a shared config a nested repo picks up by walking
|
|
864
|
-
up — see the parent-dir note under
|
|
865
|
-
[file references](#content-values-inline-or-a-file-reference)); its guidance
|
|
866
|
-
adjusts to say there is nothing to commit when it is not in a repo. It refuses
|
|
867
|
-
only to write into a repository **subdirectory**, where the upward config walk
|
|
868
|
-
would never find the file. Either way, it will not overwrite an existing config
|
|
869
|
-
at that directory (remove it first to re-scaffold).
|
|
870
|
-
|
|
871
|
-
The [`modes:`](#modes--pluggable-steering-file-modes) block it seeds is a
|
|
872
|
-
ready-to-edit suggestion — a Prettier `format:` for each built-in steering-file
|
|
873
|
-
mode the default workflow uses:
|
|
874
|
-
|
|
875
|
-
```jsonc
|
|
876
|
-
"modes": {
|
|
877
|
-
"qa": { "format": "npx prettier --write <%= it.file %>" },
|
|
878
|
-
"review": { "format": "npx prettier --write <%= it.file %>" },
|
|
879
|
-
"prose": { "format": "npx prettier --write <%= it.file %>" }
|
|
880
|
-
}
|
|
881
|
-
```
|
|
882
|
-
|
|
883
|
-
Only `format:` is declared, so gtd's built-in `qa`/`review` validators still do
|
|
884
|
-
the validating, and `prose` (a format-only built-in with no validator of its own
|
|
885
|
-
— see the `mode:` section above) still validates nothing (the two halves layer
|
|
886
|
-
independently). gtd ships no formatter, so this is the one default it suggests:
|
|
887
|
-
a fresh repo auto-formats its steering files out of the box, and you edit or
|
|
888
|
-
drop the block freely (swap Prettier for dprint, point at a script, or delete
|
|
889
|
-
the key).
|
|
890
|
-
|
|
891
|
-
The built-in default workflow has three entry points into a shared review/squash
|
|
892
|
-
tail (all walked through in
|
|
893
|
-
[STATES.md §10](../STATES.md#10-the-bundled-workflow-templates)):
|
|
894
|
-
|
|
895
|
-
- **the simple flow** — creating `.gtd/TODO.md` starts idle → planning ⇄
|
|
896
|
-
plan-review → building → checking ⇄ fixing (→ escalate) → reviewing →
|
|
897
|
-
await-review.
|
|
898
|
-
- **the advanced flow** — creating `.gtd/REQUIREMENTS.md` starts the fuller
|
|
899
|
-
machine: two-phase Q&A planning (product then architecture), package
|
|
900
|
-
decomposition, a per-package parallel build queue with an agentic spec-review
|
|
901
|
-
gate, then the same shared tail.
|
|
902
|
-
- **`gtd review <commitish>`** — enters the shared `reviewing` state directly to
|
|
903
|
-
review a foreign branch.
|
|
904
|
-
|
|
905
|
-
Both file-created flows converge on `reviewing` → `await-review`, and a full
|
|
906
|
-
sign-off squashes the whole cycle into one commit. To CUSTOMIZE any of this, add
|
|
907
|
-
a [`workflow:`](#the-workflow-key) key that fully replaces the default.
|
|
908
|
-
|
|
909
|
-
The file is written **uncommitted** — review it, then commit `.gtdrc.json`
|
|
910
|
-
before your first `gtd step` (an uncommitted file is a pending change the
|
|
911
|
-
initial state's `* **` edge would otherwise capture). `gtd init` refuses if the
|
|
912
|
-
repo root already has its OWN gtd config (remove it first to re-init; a
|
|
913
|
-
global/ancestor config such as `~/.gtdrc` does not count) and requires the
|
|
914
|
-
repository root (or a directory outside any repo).
|
|
915
|
-
|
|
916
|
-
## A complete example
|
|
917
|
-
|
|
918
|
-
A three-state note-taking machine: draft, revise-or-accept, and a squashed
|
|
919
|
-
commit — one prompt/message pair reading a workflow-declared `it.vars` entry:
|
|
920
|
-
|
|
921
|
-
```yaml
|
|
922
|
-
# .gtdrc.yaml
|
|
923
|
-
workflow:
|
|
924
|
-
vars:
|
|
925
|
-
reviewer: alice
|
|
926
|
-
states:
|
|
927
|
-
idle:
|
|
928
|
-
actor: human
|
|
929
|
-
initial: true
|
|
930
|
-
message: |
|
|
931
|
-
No active note. Write NOTE.md with what you want written up, then run
|
|
932
|
-
`gtd step human`.
|
|
933
|
-
on:
|
|
934
|
-
"* **": drafting
|
|
935
|
-
|
|
936
|
-
drafting:
|
|
937
|
-
actor: agent
|
|
938
|
-
prompt: |
|
|
939
|
-
Read NOTE.md and draft a short write-up in DRAFT.md.
|
|
940
|
-
Reviewer of record: <%= it.vars.reviewer %>.
|
|
941
|
-
Leave DRAFT.md uncommitted and finish your turn.
|
|
942
|
-
on:
|
|
943
|
-
"* **": revising
|
|
944
|
-
|
|
945
|
-
revising:
|
|
946
|
-
actor: human
|
|
947
|
-
message: |
|
|
948
|
-
DRAFT.md holds the draft. Edit it for another round, or run
|
|
949
|
-
`gtd step human` with no edits to accept it and write the commit
|
|
950
|
-
message.
|
|
951
|
-
on:
|
|
952
|
-
"C": working
|
|
953
|
-
"* **": drafting
|
|
954
|
-
|
|
955
|
-
working:
|
|
956
|
-
actor: agent
|
|
957
|
-
prompt: |
|
|
958
|
-
The draft is accepted. Write COMMIT_MSG.md with one conventional-
|
|
959
|
-
commits message for the whole cycle, then finish your turn.
|
|
960
|
-
on:
|
|
961
|
-
"A COMMIT_MSG.md": done
|
|
962
|
-
"M COMMIT_MSG.md": done
|
|
963
|
-
|
|
964
|
-
done:
|
|
965
|
-
commit: '<%~ it.read("COMMIT_MSG.md") %>'
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
A clean `gtd step human` at `revising` (the `C` event) accepts the draft and
|
|
969
|
-
moves to `working`; any other pending change loops back to `drafting`. Writing
|
|
970
|
-
`COMMIT_MSG.md` at `working` enters `done`, which squashes every commit since
|
|
971
|
-
`idle` into one, using `COMMIT_MSG.md`'s content as the message, and discards
|
|
972
|
-
the file.
|
|
973
|
-
|
|
974
|
-
## Recipe: a per-task builder loop without a script state
|
|
975
|
-
|
|
976
|
-
A `picking`-style queue arbiter (the bundled unified template's advanced flow
|
|
977
|
-
carries one — see
|
|
978
|
-
[STATES.md §10](../STATES.md#10-the-bundled-workflow-templates)) is a `script`
|
|
979
|
-
state that turns a task-queue glob into a diff patterns can see. A driver that
|
|
980
|
-
can't run scripts can get the same per-task loop out of pure prompt/`on`
|
|
981
|
-
declarations instead, at the cost of trusting the agent to honor a marker
|
|
982
|
-
protocol rather than a deterministic `ls`: see
|
|
983
|
-
[docs/design/work-packages.md §3](design/work-packages.md) ("Option B —
|
|
984
|
-
agent-encoded verdict") for the worked example.
|
|
985
|
-
|
|
986
|
-
## A fuller example: two-phase planning, task decomposition, agent-prepared review
|
|
987
|
-
|
|
988
|
-
The example above is deliberately small (it is close to the bundled unified
|
|
989
|
-
template's simple flow). For a heavier machine — Q&A planning loops, an
|
|
990
|
-
architecture phase, task decomposition, the deterministic per-task `picking`
|
|
991
|
-
arbiter, and agent-prepared `.gtd/REVIEW.md` review — the unified template's
|
|
992
|
-
**advanced flow** (started by creating `.gtd/REQUIREMENTS.md`) is exactly that,
|
|
993
|
-
walked through in
|
|
994
|
-
[STATES.md §10](../STATES.md#10-the-bundled-workflow-templates).
|