@pmelab/gtd 2.7.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.
- package/README.md +106 -1111
- package/bin/gtd-loop +43 -23
- package/dist/gtd.bundle.mjs +2281 -3387
- package/docs/cli.md +255 -0
- package/docs/configuration.md +377 -0
- package/docs/design/configurable-state-machine.md +1439 -0
- package/docs/design/delta-purity-plan.md +283 -0
- package/docs/design/pattern-machine-plan.md +164 -0
- package/docs/design/scripted-actor.md +289 -0
- package/docs/design/state-file-association.md +112 -0
- package/docs/design/steering-file-loops.md +164 -0
- package/docs/design/work-packages.md +194 -0
- package/docs/development.md +101 -0
- package/docs/examples/advanced-workflow.md +428 -0
- package/docs/loop.md +104 -0
- package/docs/upgrading.md +125 -0
- package/package.json +3 -2
- package/schema.json +6 -73
package/docs/cli.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# CLI reference
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
Usage: gtd [command] [options]
|
|
5
|
+
|
|
6
|
+
Commands:
|
|
7
|
+
step <actor> Authenticate as <actor>, match the resolved rest's
|
|
8
|
+
declared patterns against the pending changes, and commit
|
|
9
|
+
(or squash) the one resulting transition
|
|
10
|
+
next Print the resolved rest's rendered script/prompt/message
|
|
11
|
+
(no mutation)
|
|
12
|
+
run Execute the resolved rest's emitted script, then step its
|
|
13
|
+
actor (the built-in script driver)
|
|
14
|
+
status Print the resolved rest's state/actor and which declared
|
|
15
|
+
pattern (if any) each pending change matches (no mutation)
|
|
16
|
+
format <file> Format a markdown file in place
|
|
17
|
+
lsp Start the LSP server for .gtd/ steering files (stdio)
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--json Output structured JSON instead of plain text
|
|
21
|
+
--version, -v Print version and exit
|
|
22
|
+
--help, -h Print this help and exit
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`--version` (`-v`) and `--help` (`-h`) short-circuit before any git or
|
|
26
|
+
repository-state work — they run outside a repo and in any repo state. Bare
|
|
27
|
+
`gtd` (no subcommand) is a usage error: it prints the help text and exits 1
|
|
28
|
+
without touching the repository. Every other command must be run from the
|
|
29
|
+
**repository root** — gtd derives the workflow, pending changes, and process
|
|
30
|
+
history relative to cwd, so it refuses with a clear error if invoked from a
|
|
31
|
+
subdirectory.
|
|
32
|
+
|
|
33
|
+
`--json` is the only long option. Any other `--` option (including a typo like
|
|
34
|
+
`--jsn`) is rejected with a usage error rather than silently ignored, so a
|
|
35
|
+
mistyped flag can never degrade a JSON caller to plain-text mode.
|
|
36
|
+
|
|
37
|
+
## `gtd step <actor>`
|
|
38
|
+
|
|
39
|
+
Authenticates `<actor>` against the resolved rest and performs the ONE resulting
|
|
40
|
+
transition. Unlike v2, there is no fixpoint chain to drive: the pattern
|
|
41
|
+
machine's `on` edges are direct one-hop transitions, so a single invocation
|
|
42
|
+
authors at most one commit (a normal turn) or performs one squash
|
|
43
|
+
(§[STATES.md](../STATES.md#8-the-squash-lifecycle)) — never both, never a chain
|
|
44
|
+
of several. A caller that wants several transitions issues several invocations.
|
|
45
|
+
|
|
46
|
+
- **Out-of-turn refusal** — `<actor>` isn't the resolved state's declared actor:
|
|
47
|
+
exit non-zero, zero commits.
|
|
48
|
+
`gtd step <actor>: out of turn — "<state>" awaits <awaited-actor>`
|
|
49
|
+
- **No-match refusal** — the tree is dirty and no declared `on` pattern matches:
|
|
50
|
+
exit non-zero, zero commits, naming every declared pattern.
|
|
51
|
+
`gtd step <actor>: no declared pattern matches the pending changes at "<state>" — declared patterns: <p1>, <p2>, …`
|
|
52
|
+
- **No-op** — the tree is clean and the state declares no `C` pattern: exit
|
|
53
|
+
**0**, zero commits. This is the default, silent case a loop driver relies on
|
|
54
|
+
(see [Driving the loop](loop.md)).
|
|
55
|
+
- **Commit/squash** — a pattern matched: exit 0, one commit (or one squash)
|
|
56
|
+
authored.
|
|
57
|
+
|
|
58
|
+
Plain-mode output is one line:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
committed: gtd(human): grilling
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
or, at a no-op:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
nothing to do at "idle"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`--json` emits `{state, subject}` — `subject` is `null` at a no-op:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{ "state": "grilling", "subject": "gtd(human): grilling" }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## `gtd next [--json]`
|
|
77
|
+
|
|
78
|
+
Pure emitter of the resolved rest's rendered content — it **never mutates** the
|
|
79
|
+
repository (no commits, no file writes, no script execution). Resolves HEAD
|
|
80
|
+
exactly like `gtd step`, renders that state's declared
|
|
81
|
+
`script`/`prompt`/`message` template, and prints it. `kind` is never `"commit"`
|
|
82
|
+
here: resolution never rests at a commit state (entering one always ends the
|
|
83
|
+
process in the same step that entered it — see
|
|
84
|
+
[STATES.md §5](../STATES.md#5-resolution)).
|
|
85
|
+
|
|
86
|
+
Plain mode prints the rendered content verbatim (with exactly one trailing
|
|
87
|
+
newline) — never the `model`/`file`/`mode` hints, which are JSON-only. `--json`
|
|
88
|
+
emits `{state, actor, kind, content, model?, file?, mode?}`:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"state": "building",
|
|
93
|
+
"actor": "agent",
|
|
94
|
+
"kind": "prompt",
|
|
95
|
+
"content": "You are an autonomous coding agent. ..."
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- `state` — the resolved state.
|
|
100
|
+
- `actor` — the state's declared actor.
|
|
101
|
+
- `kind` — `"script"` | `"prompt"` | `"message"` — the dispatch key a driver
|
|
102
|
+
switches on (see [Driving the loop](loop.md)).
|
|
103
|
+
- `content` — the fully rendered template.
|
|
104
|
+
- `model` — the state's opaque `model:` hint, RENDERED through the same template
|
|
105
|
+
context as `content` (see
|
|
106
|
+
[Configuration](configuration.md#model--the-opaque-harness-hint-template-rendered)),
|
|
107
|
+
present only when the state declares one; **omitted entirely** (never `null`)
|
|
108
|
+
when unset.
|
|
109
|
+
- `file` — the state's declared steering file, RENDERED the same way; `mode` —
|
|
110
|
+
its format, verbatim (`"qa"` | `"review"`) (see
|
|
111
|
+
[Configuration](configuration.md#filemode--the-steering-file-association)).
|
|
112
|
+
Both present only when the state declares them; **omitted entirely** (never
|
|
113
|
+
`null`) otherwise.
|
|
114
|
+
|
|
115
|
+
## `gtd run`
|
|
116
|
+
|
|
117
|
+
The built-in driver for a `script`-content rest — the **only** place gtd itself
|
|
118
|
+
spawns a subprocess. Renders the resolved rest exactly like `gtd next`, executes
|
|
119
|
+
its content verbatim via `bash -c` (foreground, inherited stdio, exit code
|
|
120
|
+
deliberately ignored — a check script encodes its outcome in the tree, e.g.
|
|
121
|
+
writing a findings file, never in its exit status), then runs `gtd step <actor>`
|
|
122
|
+
for that state's own actor to capture the outcome in one command. Refuses (exit
|
|
123
|
+
non-zero, no execution, no step) when the resolved rest isn't a script:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
gtd run: "<state>" awaits a <kind> from "<actor>" — nothing scripted to run
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Reports the same `{state, subject}` (or `committed: …` / `nothing to do at "…"`)
|
|
130
|
+
as `gtd step` for the capturing step it performs. Takes no arguments — extra
|
|
131
|
+
positional args are rejected.
|
|
132
|
+
|
|
133
|
+
## `gtd status [--json]`
|
|
134
|
+
|
|
135
|
+
Pure, read-only dry-run reporter — the same resolution `gtd next` performs, but
|
|
136
|
+
reporting the resolved state/actor and, for every pending change, which declared
|
|
137
|
+
`on` pattern (if any) matches it — no mutation, and no CONTENT rendering (the
|
|
138
|
+
`script`/`prompt`/`message`/`commit` template is never rendered here, unlike
|
|
139
|
+
`gtd next`). It DOES render the resolved state's `model:`/`file:` hints (if
|
|
140
|
+
declared) through the same `it.vars`-carrying template context `gtd next` uses —
|
|
141
|
+
see
|
|
142
|
+
[Configuration](configuration.md#model--the-opaque-harness-hint-template-rendered)
|
|
143
|
+
— so a templated `model:`/`file:` failing to render fails `gtd status` too,
|
|
144
|
+
exactly like it would fail `gtd next`.
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
State: working
|
|
148
|
+
Awaits: agent
|
|
149
|
+
Pending:
|
|
150
|
+
A DONE.md -> A DONE.md
|
|
151
|
+
A scratch.txt -> (no match)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
or, on a clean tree: `Pending: (clean)`. A `Model: <value>` line appears right
|
|
155
|
+
after `Awaits:` when the resolved state declares a `model:` hint, and
|
|
156
|
+
`File: <value>`/`Mode: <value>` lines appear after that (in that order) when
|
|
157
|
+
declared — each independently, only when set.
|
|
158
|
+
|
|
159
|
+
`--json` emits
|
|
160
|
+
`{state, actor, changes: [{status, path, pattern}], model?, file?, mode?}` —
|
|
161
|
+
`pattern` is `null` when no declared row matches that change; `model`/`file`/
|
|
162
|
+
`mode` are present only when the resolved state declares them (omitted entirely,
|
|
163
|
+
never `null`, otherwise):
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"state": "working",
|
|
168
|
+
"actor": "agent",
|
|
169
|
+
"changes": [
|
|
170
|
+
{ "status": "A", "path": "DONE.md", "pattern": "A DONE.md" },
|
|
171
|
+
{ "status": "A", "path": "scratch.txt", "pattern": null }
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`gtd status` takes no arguments — extra positional args are rejected.
|
|
177
|
+
|
|
178
|
+
## `gtd format <file>`
|
|
179
|
+
|
|
180
|
+
Formats a markdown file in place with a bundled prettier (`parser: "markdown"`,
|
|
181
|
+
`printWidth: 80`, `proseWrap: "always"`), ignoring the host repo's own
|
|
182
|
+
`.prettierrc` so `.gtd/`-tracked files stay consistently formatted regardless of
|
|
183
|
+
the host project's toolchain. Rejects `--json` (exit 1,
|
|
184
|
+
`gtd format does not accept --json`) — it's a plain file operation, not a state
|
|
185
|
+
command.
|
|
186
|
+
|
|
187
|
+
Errors (all exit 1, message on stderr):
|
|
188
|
+
|
|
189
|
+
- Missing path: `gtd format: missing file path argument`
|
|
190
|
+
- Extra arguments: `gtd format: too many arguments — expected one path, got: …`
|
|
191
|
+
- Non-markdown file:
|
|
192
|
+
`gtd format: <file> is not a markdown file (expected .md or .markdown)`
|
|
193
|
+
- File not found: `gtd: skipped formatting <file>: not found`
|
|
194
|
+
|
|
195
|
+
## `gtd lsp`
|
|
196
|
+
|
|
197
|
+
Starts an LSP server over stdio for `.gtd/` steering files — document symbols
|
|
198
|
+
for a `qa`-mode file's open questions and a `review`-mode file's review
|
|
199
|
+
chunks/hunks, code actions to check/uncheck a hunk or a whole chunk, and
|
|
200
|
+
diagnostics publishing the same parser findings the bundled workflow's
|
|
201
|
+
`.gtd/FORMAT.md` validators produce (see `src/OpenQuestions.ts` /
|
|
202
|
+
`src/ReviewDoc.ts` and
|
|
203
|
+
[STATES.md §10](../STATES.md#10-the-bundled-default-workflow)).
|
|
204
|
+
|
|
205
|
+
**Config-driven** (see
|
|
206
|
+
[docs/design/state-file-association.md](design/state-file-association.md)): the
|
|
207
|
+
server locates the active gtd config the same way the CLI does, from the
|
|
208
|
+
`initialize` request's workspace root (falling back to the open document's own
|
|
209
|
+
directory), renders every state's `file:` into an absolute-path → `mode` map,
|
|
210
|
+
and dispatches on it — first declaring state wins a path conflict. A path the
|
|
211
|
+
map doesn't cover (or no config at all) falls back to the basename dispatch
|
|
212
|
+
(`TODO.md` → `qa`, `REVIEW.md` → `review`), so the server still works standalone
|
|
213
|
+
with no `.gtdrc` in sight. Also registers an `executeCommand`,
|
|
214
|
+
`gtd.openSteeringFile`: resolves the current state exactly like `gtd status`
|
|
215
|
+
(config + git HEAD) and asks the client to show its `file:`
|
|
216
|
+
(`window/showDocument`); a state with no `file:` gets an informational message
|
|
217
|
+
naming the state instead — bind it to an editor keybinding for a "jump to the
|
|
218
|
+
active steering file" command.
|
|
219
|
+
|
|
220
|
+
Dispatched before the repository-root guard and auto-init, like `gtd format`.
|
|
221
|
+
Rejects `--json` (exit 1, `gtd lsp does not accept --json`) and extra positional
|
|
222
|
+
arguments — it's a long-running server, not a state command. Runs until the
|
|
223
|
+
client disconnects (the LSP `exit` notification), then exits cleanly.
|
|
224
|
+
|
|
225
|
+
## Error envelope
|
|
226
|
+
|
|
227
|
+
Every command, in `--json` mode, reports a failure as a machine-readable
|
|
228
|
+
envelope on **stdout**, and still exits 1:
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{ "state": "error", "prompt": "<message>" }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
A human-readable `gtd: <message>` line is still written to **stderr** regardless
|
|
235
|
+
of `--json` — the envelope adds a structured stdout channel, it does not replace
|
|
236
|
+
the plain-text one.
|
|
237
|
+
|
|
238
|
+
## Repository requirements
|
|
239
|
+
|
|
240
|
+
- **Single writer, linear branch.** A process's history is walked via
|
|
241
|
+
**first-parent** commits only.
|
|
242
|
+
- **Test/build artifacts must be gitignored.** This is **load-bearing**, not a
|
|
243
|
+
style preference: every step decision detects "clean" via
|
|
244
|
+
`git diff --name-status HEAD` (tracked changes) unioned with
|
|
245
|
+
`git ls-files --others --exclude-standard` (untracked files), which silently
|
|
246
|
+
omits anything matched by `.gitignore`. If a `script` state's command (or the
|
|
247
|
+
build it triggers) writes tracked-but-untracked output — a `dist/`, a coverage
|
|
248
|
+
report, a log file — into the working tree, the tree never goes clean after a
|
|
249
|
+
green run, and the check's `"C"` pattern never fires. Gitignore every path
|
|
250
|
+
your scripts write before wiring gtd into a repo.
|
|
251
|
+
- **Repository root invocation.** Every state subcommand (`step`/`next`/`run`/
|
|
252
|
+
`status`) must run from the git repository root — the workflow, pending
|
|
253
|
+
changes, and process history are resolved against the process cwd.
|
|
254
|
+
`--help`/`--version`, `format`, and `lsp` skip this guard entirely (and any
|
|
255
|
+
git/`.gtdrc` dependency along with it).
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
gtd reads an optional `.gtdrc` config file via
|
|
4
|
+
[cosmiconfig](https://github.com/cosmiconfig/cosmiconfig). With no config, the
|
|
5
|
+
bundled default workflow applies (see
|
|
6
|
+
[STATES.md](../STATES.md#10-the-bundled-default-workflow)). Supported filenames
|
|
7
|
+
(searched in this order):
|
|
8
|
+
|
|
9
|
+
- `.gtdrc`
|
|
10
|
+
- `.gtdrc.json`
|
|
11
|
+
- `.gtdrc.yaml`
|
|
12
|
+
- `.gtdrc.yml`
|
|
13
|
+
- `gtd.config.json`
|
|
14
|
+
- `gtd.config.yaml`
|
|
15
|
+
|
|
16
|
+
## Schema
|
|
17
|
+
|
|
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.
|
|
25
|
+
- **`$schema`** (string, optional) — stripped before validation, so it never
|
|
26
|
+
counts as an unknown key. Point it at the published schema for editor-backed
|
|
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.
|
|
248
|
+
|
|
249
|
+
## Validation and errors
|
|
250
|
+
|
|
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:
|
|
265
|
+
|
|
266
|
+
- **Parse errors** (malformed YAML/JSON) — message includes the offending
|
|
267
|
+
filename.
|
|
268
|
+
- **Non-object top-level** — a YAML list or `null` at the root is rejected with
|
|
269
|
+
the filename in the message.
|
|
270
|
+
- **Unknown top-level key** — anything besides `workflow`/`vars`/`$schema` emits
|
|
271
|
+
`Invalid gtd config: <field>: <reason>`.
|
|
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.
|
|
278
|
+
|
|
279
|
+
## Lookup and precedence
|
|
280
|
+
|
|
281
|
+
gtd walks from the current working directory **up to your home directory** (or
|
|
282
|
+
to the filesystem root when cwd is outside home), collecting every `.gtdrc` it
|
|
283
|
+
finds along the way. All found levels are **deep-merged**, with the **innermost
|
|
284
|
+
(cwd) config winning** on conflicts — so a shared `.gtdrc` in a worktree-parent
|
|
285
|
+
directory cascades to every checkout beneath it, while any individual checkout
|
|
286
|
+
can still override with its own `.gtdrc`.
|
|
287
|
+
|
|
288
|
+
## Auto-init
|
|
289
|
+
|
|
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.
|
|
301
|
+
|
|
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:
|
|
306
|
+
|
|
307
|
+
```yaml
|
|
308
|
+
# .gtdrc.yaml
|
|
309
|
+
workflow:
|
|
310
|
+
vars:
|
|
311
|
+
reviewer: alice
|
|
312
|
+
states:
|
|
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") %>'
|
|
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.
|