@pmelab/gtd 2.8.0 → 3.0.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.
@@ -2,7 +2,9 @@
2
2
 
3
3
  gtd reads an optional `.gtdrc` config file via
4
4
  [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig). With no config, the
5
- built-in defaults apply. Supported filenames (searched in this order):
5
+ bundled default workflow applies (see
6
+ [STATES.md](../STATES.md#10-the-bundled-default-workflow)). Supported filenames
7
+ (searched in this order):
6
8
 
7
9
  - `.gtdrc`
8
10
  - `.gtdrc.json`
@@ -13,62 +15,266 @@ built-in defaults apply. Supported filenames (searched in this order):
13
15
 
14
16
  ## Schema
15
17
 
16
- - **`testCommand`** (string, default `npm run test`) — the command the edge runs
17
- after a build turn, and on the idle health-check path.
18
- - **`fixAttemptCap`** (non-negative integer, default `3`) — the test-fix budget:
19
- how many `gtd: errors` attempts are allowed per sub-loop before the failure is
20
- escalated to `.gtd/ERRORS.md` (Escalate). `0` disables the cap (escalates
21
- immediately on the first red run). Also reused as the health-fix budget — no
22
- separate config key.
23
- - **`reviewThreshold`** (integer ≥ 1, default `3`) — the review-fix budget: how
24
- many agentic-review findings rounds are allowed per package before Agentic
25
- Review force-approves.
26
- - **`agenticReview`** (boolean, default `true`) — kill-switch for the
27
- per-package Agentic Review gate. Set `false` to force-approve every package
28
- and proceed directly to human review.
29
- - **`squash`** (boolean, default `true`) — after `gtd: done` (or, once learning
30
- has run, `gtd: learning applied`), collapse the cycle's `gtd: *` commits into
31
- a single conventional-commits commit. Set `false` to keep the granular
32
- history.
33
- - **`learning`** (boolean, default `true`) — after `gtd: done` (or the
34
- health-fix path's green re-test), distill durable lessons from the cycle into
35
- `.gtd/LEARNINGS.md`, have a human review them, then integrate them into the
36
- project's own docs before the squash decision runs. Set `false` to skip the
37
- phase entirely — independent of `squash`.
38
- - **`decisionLog`** (boolean, default `true`) — squashing records this cycle's
39
- resolved open questions as a `## Decisions` section in the squash commit
40
- message; grilling/architecting inline every past squash commit's section,
41
- oldest to newest, as prior-decision context. Set `false` to skip both.
42
- - **`models`** — model selection for the subagent-spawning states:
43
- - `planning` — high-reasoning tier (default `claude-opus-4-8`), used by
44
- `decompose` (the `grilled`/`planning` states), `grilling`, `architecting`,
45
- `agentic-review`, and `clean` (the `review`/`squashing`/`learning`/
46
- `learning-apply` states).
47
- - `execution` — everyday tier (default `claude-sonnet-4-8`), used by
48
- `building` and `fixing`.
49
- - `states.*` — per-state overrides keyed by `decompose`, `grilling`,
50
- `architecting`, `building`, `fixing`, `agentic-review`, `clean`. Unknown
51
- `states` keys are **rejected**.
18
+ v3's `.gtdrc` has exactly two blessed top-level keys:
19
+
20
+ - **`workflow`** (object, optional) — the whole machine definition, compiled by
21
+ `src/PatternConfig.ts`. Absent = the bundled default workflow. See
22
+ ["The `workflow:` key" below](#the-workflow-key) for its schema.
23
+ - **`vars`** (object, optional) — a flat `name -> scalar` map, one layer of the
24
+ merged `it.vars` every template sees — see ["Variables"](#variables) below.
52
25
  - **`$schema`** (string, optional) — stripped before validation, so it never
53
26
  counts as an unknown key. Point it at the published schema for editor-backed
54
- autocompletion. A `schema.json` is generated from the config schema at build
55
- time and ships with the package.
27
+ autocompletion. A `schema.json` is generated from `src/ConfigSchema.ts` at
28
+ build time and ships with the package.
29
+
30
+ Any other top-level key is **rejected** (`onExcessProperty: "error"`) — v3 has
31
+ no `testCommand`, `fixAttemptCap`, `reviewThreshold`, `agenticReview`, `squash`,
32
+ `learning`, `decisionLog`, or `models` keys; all of that machinery is gone (see
33
+ [Upgrading](upgrading.md)). The engine blesses no VARIABLE NAMES either —
34
+ `testCommand` (the bundled default workflow's own var, see
35
+ ["Variables"](#variables)) is workflow-authored data like any other `it.vars`
36
+ entry, not a special key gtd interprets.
37
+
38
+ ## The `workflow:` key
39
+
40
+ The `workflow:` key is the **only** definition source — there is no `extends`,
41
+ no merge-over-a-built-in; the bundled default workflow is itself a YAML asset
42
+ compiled through the exact same compiler (`src/workflows/default.yaml` →
43
+ `compileWorkflowConfig`). Its shape:
44
+
45
+ ```yaml
46
+ workflow:
47
+ vars: # optional — the workflow's own declared `it.vars` defaults (see "Variables" below)
48
+ anyKey: anyScalarValue
49
+ states:
50
+ <name>:
51
+ actor: <string> # forbidden on a commit state, required otherwise
52
+ script: <string> # exactly one of script/prompt/message/commit
53
+ prompt: <string>
54
+ message: <string>
55
+ commit: <string>
56
+ on: # a mapping, DECLARATION ORDER PRESERVED
57
+ "<pattern>": <targetState>
58
+ initial: true # exactly one state across the whole workflow
59
+ retry:
60
+ max: <number>
61
+ otherwise: <targetState>
62
+ model: <string> # optional, opaque harness hint — forbidden on a commit state
63
+ file: <string> # optional, an Eta template naming the state's steering file — forbidden on a commit state
64
+ mode: qa | review # optional, requires "file" — forbidden on a commit state
65
+ ```
66
+
67
+ See [STATES.md](../STATES.md#1-the-model) for what each field means to the
68
+ engine, and [§3](../STATES.md#3-pattern-grammar) for the pattern grammar.
69
+
70
+ ### Content values: inline or a file reference
71
+
72
+ A `script`/`prompt`/`message`/`commit` value starting with `./` or `../` is a
73
+ **file reference** — resolved relative to the config file's own directory and
74
+ read at load time. A missing or unreadable file reference is a **load error**,
75
+ collected and thrown with every other config problem — never silently treated as
76
+ inline text. Any other string (including one that merely contains a `/`, or an
77
+ absolute path) is inline Eta template source, used verbatim.
78
+
79
+ ```yaml
80
+ workflow:
81
+ states:
82
+ working:
83
+ actor: agent
84
+ prompt: ./prompts/working.md # read from alongside this .gtdrc
85
+ on:
86
+ "* **": done
87
+ ```
88
+
89
+ A `vars:` key sibling to `states:` inside `workflow:` declares the workflow's
90
+ own defaults for `it.vars` — see ["Variables"](#variables) below for the full
91
+ three-layer picture.
92
+
93
+ ### `model:` — the opaque harness hint, template-rendered
94
+
95
+ A state may declare `model: <string>` — an OPAQUE label (e.g. `smart`, `fast`,
96
+ or a concrete model id) gtd never interprets; it is only passed through so the
97
+ driving loop can map it onto whatever models its agent harness provides. Unset
98
+ means "use the harness's default." Forbidden on a commit state (never at rest,
99
+ emits nothing):
100
+
101
+ ```yaml
102
+ workflow:
103
+ states:
104
+ working:
105
+ actor: agent
106
+ model: smart
107
+ prompt: do the thing
108
+ on:
109
+ "* **": done
110
+ ```
111
+
112
+ Like every content string, `model:` is rendered as an Eta template through the
113
+ exact same context as the state's content — a plain string with no Eta tags
114
+ (`smart` above) passes through unchanged, but
115
+ `model: "<%= it.vars.reviewModel %>"` resolves against the merged `it.vars` (see
116
+ ["Variables"](#variables)). A render failure behaves exactly like a content
117
+ render failure at the same call site: `gtd next`/`gtd status` error out, nothing
118
+ committed.
119
+
120
+ `gtd next --json` and `gtd status --json` include a `"model"` key (the RENDERED
121
+ value) only when the resolved state declares one — it is **omitted entirely**,
122
+ never emitted as `null`, when unset.
123
+
124
+ ### `file:`/`mode:` — the steering-file association
125
+
126
+ A state may additionally declare `file:` — an Eta template naming THE steering
127
+ file this state is about: the file a human/editor should look at while the
128
+ machine rests here (rendered through the same `it.vars`-carrying context as
129
+ content/`model`; must render non-empty). Forbidden on a commit state (never at
130
+ rest). Multiple states may share one `file:` (and, in the bundled default, do):
131
+
132
+ ```yaml
133
+ workflow:
134
+ vars:
135
+ planFile: .gtd/PLAN.md
136
+ states:
137
+ working:
138
+ actor: agent
139
+ file: <%= it.vars.planFile %>
140
+ mode: qa
141
+ prompt: develop the plan
142
+ on:
143
+ "* **": done
144
+ ```
145
+
146
+ `mode:` requires a sibling `file:` and names the file's FORMAT, from a closed,
147
+ documented vocabulary:
148
+
149
+ | `mode` | Format |
150
+ | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
151
+ | `qa` | The open-questions format (`## Open Questions`, one `###` sub-heading per question, `Suggested default: ...`/`Answer: ...`). |
152
+ | `review` | The checkbox review format (`# Review: <hash>` header, `<!-- base: <hash> -->` comment, `##` chunks, `- [ ]` pointers). |
153
+
154
+ An unknown `mode` value is a load error naming the allowed values (typos must
155
+ not silently disable editor support) — like `model`, the ENGINE never branches
156
+ on `mode`; only `gtd lsp` (see
157
+ [docs/design/state-file-association.md](design/state-file-association.md))
158
+ interprets it, to decide which document symbols/code actions/diagnostics a file
159
+ gets.
160
+
161
+ `gtd next --json`/`gtd status --json` gain `"file"` (the RENDERED path) and
162
+ `"mode"` (verbatim) keys, each **omitted entirely** (never `null`) when the
163
+ resolved state declares none — exactly like `model`. Plain `gtd status` prints
164
+ `File:`/`Mode:` lines (right after `Model:`, when present) when set.
165
+
166
+ **Known limitation — `on` pattern keys are NOT Eta templates.** A workflow's
167
+ `on` patterns keep LITERAL `.gtd/…` paths, so repointing a filename var
168
+ (`.gtdrc`'s top-level `vars:`, or a `GTD_VAR_` override) without ALSO overriding
169
+ the workflow's `on` patterns desyncs the machine: `file:` (and any template
170
+ reading/writing that path) follows the var, but the `on` map that decides what a
171
+ change to that path MEANS keeps matching the old literal path. The vars are a
172
+ DRY mechanism inside templates and the state↔file association, not a rename
173
+ switch. (Making pattern keys var-aware at compile time is possible future work.)
174
+
175
+ ### Template variables
176
+
177
+ Every `script`/`prompt`/`message`/`commit`/`model` template is rendered as an
178
+ Eta template (`it.<name>`) with:
179
+
180
+ | Variable | Meaning |
181
+ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
182
+ | `startCommit` | The hash the current process started from (before its first turn). |
183
+ | `currentCommit` | HEAD's hash at render time. |
184
+ | `previousCommit` | The hash before the last transition (HEAD's parent, in-process). |
185
+ | `state` | The state whose content is being rendered. |
186
+ | `actor` | The actor this render is for. |
187
+ | `processDiff` | `startCommit..HEAD` plus the pending working-tree diff. |
188
+ | `lastDiff` | The diff of the last transition alone. |
189
+ | `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)). |
190
+ | `vars` | The merged three-layer variable map, always a flat `Record<string, string>` — see ["Variables"](#variables) below. |
191
+
192
+ ## Variables
193
+
194
+ Every template — `script`/`prompt`/`message`/`commit`, and now `model` — sees
195
+ `it.vars`: a flat `Record<string, string>` assembled from three layers, **later
196
+ wins**:
197
+
198
+ 1. **The workflow's own `vars:` key** (sibling to `states:`, shown above) — the
199
+ workflow author's declared defaults. The bundled default workflow declares
200
+ `vars: { testCommand: "npm test" }`, read by `checking`'s script as
201
+ `<%~ it.vars.testCommand %>` (see
202
+ [STATES.md §10](../STATES.md#10-the-bundled-default-workflow)).
203
+ 2. **A top-level `.gtdrc` `vars:` key** (a sibling of `workflow:`, NOT nested
204
+ inside it) — per-repo tuning without redefining the whole workflow. Subject
205
+ to the same cwd→home deep merge as everything else in `.gtdrc` (innermost
206
+ wins per-key).
207
+ 3. **`GTD_VAR_<name>` environment variables** — highest precedence, checked at
208
+ every invocation. The prefix is stripped and the REMAINING CASE matched
209
+ exactly: `GTD_VAR_testCommand` sets `testCommand`, not `TESTCOMMAND` or
210
+ `testcommand`. This is the only layer that may introduce a name neither
211
+ config layer declared.
212
+
213
+ Values in layers 1–2 must be YAML scalars (string/number/boolean) — coerced to
214
+ strings at load time; an object or array value is a load error, collected
215
+ alongside every other config-shape finding (see
216
+ ["Validation and errors"](#validation-and-errors)). Environment values are
217
+ already strings.
218
+
219
+ ```yaml
220
+ # .gtdrc — overriding the bundled default's testCommand
221
+ vars:
222
+ testCommand: npm run test:ci
223
+ ```
224
+
225
+ ```bash
226
+ # highest precedence — beats both the workflow default and the .gtdrc value above
227
+ GTD_VAR_testCommand="npm run test -- --bail" gtd run
228
+ ```
229
+
230
+ A template reads any of it as `it.vars.<name>`:
231
+
232
+ ```yaml
233
+ workflow:
234
+ vars:
235
+ reviewer: alice
236
+ states:
237
+ working:
238
+ actor: agent
239
+ prompt: "Assigned reviewer: <%= it.vars.reviewer %>"
240
+ model: "<%= it.vars.reviewModel %>" # a GTD_VAR_reviewModel override, or a .gtdrc vars: entry
241
+ on:
242
+ "* **": done
243
+ ```
244
+
245
+ No variable name is blessed by the engine — `testCommand` is workflow-authored
246
+ data like any other `it.vars` entry, not a special key gtd interprets; a custom
247
+ workflow is free to declare and read any names it likes.
56
248
 
57
249
  ## Validation and errors
58
250
 
59
- If a config file fails to load or is invalid, gtd **exits with code 1** and
60
- writes a human-readable error to **stderr** (never stdout):
251
+ Config-shape problems (unknown keys, wrong types, unreadable file references)
252
+ are collected together; if the shape is clean, the assembled definition is
253
+ additionally run through the engine's own `validateDefinition` (see
254
+ [STATES.md §9](../STATES.md#9-validation)). A bad config throws **one** error
255
+ listing every finding, at load time — before anything touches the repository —
256
+ never partially, and never deferred to step time:
257
+
258
+ ```
259
+ workflow config:
260
+ - state "idle": must declare exactly one of script/prompt/message/commit (found 2)
261
+ - state "idle": "on" target "nowhere" is not a defined state
262
+ ```
263
+
264
+ Other load failures:
61
265
 
62
266
  - **Parse errors** (malformed YAML/JSON) — message includes the offending
63
267
  filename.
64
268
  - **Non-object top-level** — a YAML list or `null` at the root is rejected with
65
269
  the filename in the message.
66
- - **Schema violations** — unknown keys or out-of-range values emit
270
+ - **Unknown top-level key** — anything besides `workflow`/`vars`/`$schema` emits
67
271
  `Invalid gtd config: <field>: <reason>`.
68
- - **Missing test binary** — if `testCommand` names an executable that cannot be
69
- found (`ENOENT`), gtd exits 1 with `gtd: test command not found: <command>` on
70
- stderr. A non-zero test _exit code_ is not an error — it drives the normal red
71
- path.
272
+ - **A bad top-level `vars:` entry** — an object/array value fails the same way
273
+ as a bad workflow-level `vars:` entry (see ["Variables"](#variables)), one
274
+ aggregated error:
275
+ `gtd config:\n - "vars.<name>" must be a string, number, or boolean, got <type>`.
276
+
277
+ All of these exit **1** and write to **stderr**, never stdout.
72
278
 
73
279
  ## Lookup and precedence
74
280
 
@@ -77,37 +283,95 @@ to the filesystem root when cwd is outside home), collecting every `.gtdrc` it
77
283
  finds along the way. All found levels are **deep-merged**, with the **innermost
78
284
  (cwd) config winning** on conflicts — so a shared `.gtdrc` in a worktree-parent
79
285
  directory cascades to every checkout beneath it, while any individual checkout
80
- can still override settings with its own `.gtdrc`.
286
+ can still override with its own `.gtdrc`.
81
287
 
82
288
  ## Auto-init
83
289
 
84
- On every **state command** (`step`, `step-agent`, `next`, `status`, `review`)
85
- that has passed the repo-root guard, if the cwd→root walk finds **no** config
86
- anywhere, gtd creates and commits a starter `.gtdrc.json` at the repository root
87
- containing only a `$schema` link. Auto-init never runs for `--version`/`--help`,
88
- `format`, bare/unknown commands, or an invocation refused by the repo-root guard
89
- — those perform no repository mutation of any kind. On a repo with no commits
90
- yet, or whose HEAD is a plain (non-`gtd:`) commit, the stub is committed as its
91
- own `chore: add .gtdrc.json`. If HEAD is already a `gtd:`-owned commit
92
- (mid-workflow), the stub is instead **amended into HEAD** — stacking a fresh
93
- boundary commit there would produce an unrecognized HEAD most workflow states
94
- can't resolve past.
290
+ On every **state command** (`step`, `next`, `run`, `status`) that has passed the
291
+ repo-root guard, if the cwd→root walk finds **no** config anywhere, gtd creates
292
+ and commits a starter `.gtdrc.json` at the repository root containing only a
293
+ `$schema` link. Auto-init never runs for `--version`/`--help`, `format`,
294
+ bare/unknown commands, or an invocation refused by the repo-root guard — those
295
+ perform no repository mutation of any kind. On a repo with no commits yet, or
296
+ whose HEAD isn't a `gtd(actor): state` commit, the stub is committed as its own
297
+ `chore: add .gtdrc.json`. If HEAD is already a `gtd(actor): state` commit
298
+ (mid-process), the stub is instead **amended into HEAD** — stacking a fresh
299
+ boundary commit there would produce an unrecognized HEAD that resolves back to
300
+ the workflow's initial state.
95
301
 
96
- ## Example
302
+ ## A complete example
303
+
304
+ A three-state note-taking machine: draft, revise-or-accept, and a squashed
305
+ commit — one prompt/message pair reading a workflow-declared `it.vars` entry:
97
306
 
98
307
  ```yaml
99
308
  # .gtdrc.yaml
100
- testCommand: pnpm test
101
- fixAttemptCap: 3
102
- reviewThreshold: 3
103
- agenticReview: true
104
- squash: true
105
- learning: true
106
- decisionLog: true
107
- models:
108
- planning: claude-opus-4-8
109
- execution: claude-sonnet-4-8
309
+ workflow:
310
+ vars:
311
+ reviewer: alice
110
312
  states:
111
- decompose: claude-opus-4-8
112
- building: claude-sonnet-4-8
313
+ idle:
314
+ actor: human
315
+ initial: true
316
+ message: |
317
+ No active note. Write NOTE.md with what you want written up, then run
318
+ `gtd step human`.
319
+ on:
320
+ "* **": drafting
321
+
322
+ drafting:
323
+ actor: agent
324
+ prompt: |
325
+ Read NOTE.md and draft a short write-up in DRAFT.md.
326
+ Reviewer of record: <%= it.vars.reviewer %>.
327
+ Leave DRAFT.md uncommitted and finish your turn.
328
+ on:
329
+ "* **": revising
330
+
331
+ revising:
332
+ actor: human
333
+ message: |
334
+ DRAFT.md holds the draft. Edit it for another round, or run
335
+ `gtd step human` with no edits to accept it and write the commit
336
+ message.
337
+ on:
338
+ "C": working
339
+ "* **": drafting
340
+
341
+ working:
342
+ actor: agent
343
+ prompt: |
344
+ The draft is accepted. Write COMMIT_MSG.md with one conventional-
345
+ commits message for the whole cycle, then finish your turn.
346
+ on:
347
+ "A COMMIT_MSG.md": done
348
+ "M COMMIT_MSG.md": done
349
+
350
+ done:
351
+ commit: '<%~ it.read("COMMIT_MSG.md") %>'
113
352
  ```
353
+
354
+ A clean `gtd step human` at `revising` (the `C` event) accepts the draft and
355
+ moves to `working`; any other pending change loops back to `drafting`. Writing
356
+ `COMMIT_MSG.md` at `working` enters `done`, which squashes every commit since
357
+ `idle` into one, using `COMMIT_MSG.md`'s content as the message, and discards
358
+ the file.
359
+
360
+ ## Recipe: a per-task builder loop without a script state
361
+
362
+ A `picking`-style queue arbiter (see
363
+ [docs/examples/advanced-workflow.md](examples/advanced-workflow.md)) is a
364
+ `script` state that turns a task-queue glob into a diff patterns can see. A
365
+ driver that can't run scripts can get the same per-task loop out of pure
366
+ prompt/`on` declarations instead, at the cost of trusting the agent to honor a
367
+ marker protocol rather than a deterministic `ls`: see
368
+ [docs/design/work-packages.md §3](design/work-packages.md) ("Option B —
369
+ agent-encoded verdict") for the worked example.
370
+
371
+ ## A fuller example: two-phase planning, task decomposition, agent-prepared review
372
+
373
+ The bundled default above is deliberately small. For a heavier machine — Q&A
374
+ planning loops, an architecture phase, task decomposition, the deterministic
375
+ per-task `picking` arbiter, and agent-prepared `.gtd/REVIEW.md` review — see
376
+ [docs/examples/advanced-workflow.md](examples/advanced-workflow.md), a
377
+ copy-paste-ready `.gtdrc` recipe.