@pmelab/gtd 2.8.0 → 3.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.
@@ -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,281 @@ 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
+ `validateDefinition`'s findings include the **semantic graph checks**: every
265
+ `on` target and `retry.otherwise` must name a defined state, and every state
266
+ must be **reachable** from the initial state by walking `on` targets and
267
+ `retry.otherwise` redirects — an unreachable state (typically a typo'd rename or
268
+ a leftover) is a load error like any other, since a workflow is bound to a
269
+ project and edited as a project-wide change.
270
+
271
+ Many of these problems never reach gtd at all if your editor validates against
272
+ the published schema: `schema.json` fully types the `workflow:` key (state
273
+ shape, content kinds, `on`/`retry` structure, the `mode` vocabulary), so a
274
+ yaml-language-server-style editor flags unknown keys and wrong types as you
275
+ type. The rules JSON Schema cannot express — exactly one content kind, exactly
276
+ one `initial: true`, targets naming defined states, reachability — remain the
277
+ compiler's job at load time.
278
+
279
+ Other load failures:
61
280
 
62
281
  - **Parse errors** (malformed YAML/JSON) — message includes the offending
63
282
  filename.
64
283
  - **Non-object top-level** — a YAML list or `null` at the root is rejected with
65
284
  the filename in the message.
66
- - **Schema violations** — unknown keys or out-of-range values emit
285
+ - **Unknown top-level key** — anything besides `workflow`/`vars`/`$schema` emits
67
286
  `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.
287
+ - **A bad top-level `vars:` entry** — an object/array value fails the same way
288
+ as a bad workflow-level `vars:` entry (see ["Variables"](#variables)), one
289
+ aggregated error:
290
+ `gtd config:\n - "vars.<name>" must be a string, number, or boolean, got <type>`.
291
+
292
+ All of these exit **1** and write to **stderr**, never stdout.
72
293
 
73
294
  ## Lookup and precedence
74
295
 
@@ -77,37 +298,95 @@ to the filesystem root when cwd is outside home), collecting every `.gtdrc` it
77
298
  finds along the way. All found levels are **deep-merged**, with the **innermost
78
299
  (cwd) config winning** on conflicts — so a shared `.gtdrc` in a worktree-parent
79
300
  directory cascades to every checkout beneath it, while any individual checkout
80
- can still override settings with its own `.gtdrc`.
301
+ can still override with its own `.gtdrc`.
81
302
 
82
303
  ## Auto-init
83
304
 
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.
305
+ On every **state command** (`step`, `next`, `run`, `status`) that has passed the
306
+ repo-root guard, if the cwd→root walk finds **no** config anywhere, gtd creates
307
+ and commits a starter `.gtdrc.json` at the repository root containing only a
308
+ `$schema` link. Auto-init never runs for `--version`/`--help`, `format`,
309
+ bare/unknown commands, or an invocation refused by the repo-root guard — those
310
+ perform no repository mutation of any kind. On a repo with no commits yet, or
311
+ whose HEAD isn't a `gtd(actor): state` commit, the stub is committed as its own
312
+ `chore: add .gtdrc.json`. If HEAD is already a `gtd(actor): state` commit
313
+ (mid-process), the stub is instead **amended into HEAD** — stacking a fresh
314
+ boundary commit there would produce an unrecognized HEAD that resolves back to
315
+ the workflow's initial state.
95
316
 
96
- ## Example
317
+ ## A complete example
318
+
319
+ A three-state note-taking machine: draft, revise-or-accept, and a squashed
320
+ commit — one prompt/message pair reading a workflow-declared `it.vars` entry:
97
321
 
98
322
  ```yaml
99
323
  # .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
324
+ workflow:
325
+ vars:
326
+ reviewer: alice
110
327
  states:
111
- decompose: claude-opus-4-8
112
- building: claude-sonnet-4-8
328
+ idle:
329
+ actor: human
330
+ initial: true
331
+ message: |
332
+ No active note. Write NOTE.md with what you want written up, then run
333
+ `gtd step human`.
334
+ on:
335
+ "* **": drafting
336
+
337
+ drafting:
338
+ actor: agent
339
+ prompt: |
340
+ Read NOTE.md and draft a short write-up in DRAFT.md.
341
+ Reviewer of record: <%= it.vars.reviewer %>.
342
+ Leave DRAFT.md uncommitted and finish your turn.
343
+ on:
344
+ "* **": revising
345
+
346
+ revising:
347
+ actor: human
348
+ message: |
349
+ DRAFT.md holds the draft. Edit it for another round, or run
350
+ `gtd step human` with no edits to accept it and write the commit
351
+ message.
352
+ on:
353
+ "C": working
354
+ "* **": drafting
355
+
356
+ working:
357
+ actor: agent
358
+ prompt: |
359
+ The draft is accepted. Write COMMIT_MSG.md with one conventional-
360
+ commits message for the whole cycle, then finish your turn.
361
+ on:
362
+ "A COMMIT_MSG.md": done
363
+ "M COMMIT_MSG.md": done
364
+
365
+ done:
366
+ commit: '<%~ it.read("COMMIT_MSG.md") %>'
113
367
  ```
368
+
369
+ A clean `gtd step human` at `revising` (the `C` event) accepts the draft and
370
+ moves to `working`; any other pending change loops back to `drafting`. Writing
371
+ `COMMIT_MSG.md` at `working` enters `done`, which squashes every commit since
372
+ `idle` into one, using `COMMIT_MSG.md`'s content as the message, and discards
373
+ the file.
374
+
375
+ ## Recipe: a per-task builder loop without a script state
376
+
377
+ A `picking`-style queue arbiter (see
378
+ [docs/examples/advanced-workflow.md](examples/advanced-workflow.md)) is a
379
+ `script` state that turns a task-queue glob into a diff patterns can see. A
380
+ driver that can't run scripts can get the same per-task loop out of pure
381
+ prompt/`on` declarations instead, at the cost of trusting the agent to honor a
382
+ marker protocol rather than a deterministic `ls`: see
383
+ [docs/design/work-packages.md §3](design/work-packages.md) ("Option B —
384
+ agent-encoded verdict") for the worked example.
385
+
386
+ ## A fuller example: two-phase planning, task decomposition, agent-prepared review
387
+
388
+ The bundled default above is deliberately small. For a heavier machine — Q&A
389
+ planning loops, an architecture phase, task decomposition, the deterministic
390
+ per-task `picking` arbiter, and agent-prepared `.gtd/REVIEW.md` review — see
391
+ [docs/examples/advanced-workflow.md](examples/advanced-workflow.md), a
392
+ copy-paste-ready `.gtdrc` recipe.