@pmelab/gtd 7.11.2 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,109 +0,0 @@
1
- # Development
2
-
3
- ```bash
4
- npm install
5
- npm run dev # run from source, no build (node dev/run.mjs)
6
- npm run build # tsdown → dist/gtd.bundle.mjs
7
- npm test # format:check, typecheck, lint, unit + e2e tests, fallow
8
- npm run test:unit # vitest unit tests (the pure resolver) — --project unit
9
- npm run test:e2e # gherkin e2e via vitest + quickpickle — --project e2e
10
- npm run test:mutation # StrykerJS mutation testing (manual only, ~2 min)
11
- npm run typecheck
12
- npm run lint
13
- ```
14
-
15
- ## Pre-commit hook
16
-
17
- A pre-commit hook is installed automatically via the `prepare` script when you
18
- run `npm install` on a fresh clone — no manual setup needed. The hook runs
19
- [lint-staged](https://github.com/lint-staged/lint-staged) with
20
- [oxfmt](https://oxc.rs/docs/guide/usage/formatter.html), formatting every staged
21
- file before each commit (`oxfmt --no-error-on-unmatched-pattern --write`),
22
- mirroring the `format:check` step enforced in CI (`oxfmt --check .`).
23
-
24
- ## Templates
25
-
26
- Every state's content (`script`/`prompt`/`message`/`commit`) is an Eta template
27
- string, rendered by `src/PatternTemplates.ts`'s `renderStateTemplate` against
28
- the variable set documented in
29
- [Configuration](configuration.md#template-variables). There is no template-file
30
- registry to keep in sync: a workflow's content strings are either inline in its
31
- YAML/`.gtdrc` or a `./`-relative file reference auto-inlined at config load
32
- (`src/PatternConfig.ts`) — see
33
- [Configuration](configuration.md#content-values-inline-or-a-file-reference). The
34
- bundled workflow template (`src/workflows/unified.yaml`) gtd runs as its
35
- built-in default (and materializes into config form via `renderInitConfig`) is
36
- imported as raw text via tsdown's `.yaml`-as-text loader (`tsdown.config.ts`)
37
- and compiled through the same path a `.gtdrc` `workflow:` key goes through
38
- (`src/workflows/templates.ts`) — no privileged code path.
39
-
40
- `npm run dev` runs `src/main.ts` directly via Node's native TypeScript
41
- type-stripping (requires Node 22.6+). It registers `dev/hooks.mjs`, which
42
- resolves `./Foo.js` specifiers to the on-disk `./Foo.ts` and imports `*.yaml`
43
- files as raw text, mirroring tsdown's `.yaml`-as-text loader — this is what lets
44
- `src/workflows/unified.yaml` resolve when running from source.
45
-
46
- The decision core is pure and IO-free: the pattern machine's shape (states,
47
- patterns, retry) is a plain-data `WorkflowDefinition` (`src/PatternMachine.ts`),
48
- and the same module's `resolveState`/`step` are the pure resolver/interpreter
49
- over it — so the whole engine is trivially unit-testable in isolation; all
50
- git/filesystem/template IO is confined to the edge (`src/Edge.ts`).
51
-
52
- `npm run build` produces `dist/gtd.bundle.mjs`, which npm exposes as the `gtd`
53
- binary via the `bin` field in `package.json`.
54
-
55
- ## The LSP server
56
-
57
- `gtd lsp` (`src/Lsp.ts`) serves editor tooling over `.gtd/`'s two steering-file
58
- formats — document symbols and diagnostics for `.gtd/TODO.md`'s open questions
59
- and `.gtd/REVIEW.md`'s review chunks/hunks, plus check/uncheck code actions for
60
- the latter. It's keyed on file NAME, not workflow state: a v3 workflow declares
61
- no state→file mapping (a state names no file), so the server needs no
62
- git/`.gtdrc` dependency at all and serves whatever content the editor hands it
63
- over the LSP protocol, exactly like any other document.
64
-
65
- `src/OpenQuestions.ts` and `src/ReviewDoc.ts` are the pure parsers behind both
66
- the LSP and the `gtd validate` command, which resolves the current state, reads
67
- its `file:`, and evaluates it per its `mode:` — ONE source of truth per format,
68
- no bash port and no dual-implementation contract to keep in sync. Mode dispatch
69
- itself lives in `src/SteeringMode.ts`, half by half: the two BUILT-IN mode names
70
- validate with these parsers (`qa` → `parseOpenQuestions`, `review` →
71
- `parseReviewDoc`), while a `modes:` entry's `format:`/`validate:` shell commands
72
- take precedence per half — gtd itself ships no formatter. See
73
- [docs/design/steering-file-validation-command.md](design/steering-file-validation-command.md)
74
- and
75
- [docs/design/pluggable-steering-modes.md](design/pluggable-steering-modes.md)
76
- and [STATES.md §12](../STATES.md) for the command, and each module's own doc
77
- comment for the format it defines. Their unit tests are that format's spec
78
- tests.
79
-
80
- ## Mutation testing
81
-
82
- Run mutation testing on-demand with `npm run test:mutation` (StrykerJS) — never
83
- run it as part of routine development; it is a deliberate, manually-triggered
84
- check. `stryker.config.json`'s `mutate` list names the v3 pattern-machine module
85
- set (`src/PatternMachine.ts`, `src/PatternConfig.ts`, `src/PatternTemplates.ts`,
86
- `src/Edge.ts`, `src/Config.ts`, `src/Format.ts` — see
87
- [Architecture](../AGENTS.md#the-pattern-machine-module-map) for the v3 module
88
- map), plus the steering-file format parsers and the LSP built on them
89
- (`src/OpenQuestions.ts`, `src/ReviewDoc.ts`, `src/Lsp.ts` — see
90
- ["The LSP server"](#the-lsp-server) below). `src/Lsp.ts`'s protocol-adapter tail
91
- (`startLspServer`) is exercised only by the `@live` e2e (excluded from the
92
- mutation run's Cucumber harness), so mutants confined to that tail may survive
93
- without it being a coverage gap in the ordinary test suite — the pure
94
- symbol/edit/diagnostic builders above it are unit-tested and mutation-checked
95
- normally.
96
-
97
- `src/Git.ts` is excluded: the Cucumber harness stubs git at the Effect boundary,
98
- so `Git.ts` mutants have zero in-memory coverage.
99
-
100
- The HTML report lands in `reports/mutation/mutation.html` (git-ignored).
101
-
102
- ## Releasing
103
-
104
- Releases are automatic. Push releasable Conventional Commits (`fix:`, `feat:`,
105
- or breaking changes) to `main` and the Release workflow runs the tests, then
106
- `npx semantic-release`. Semantic-release computes the next version, writes it
107
- into `package.json`, builds the bundle, commits the bump back as
108
- `chore(release): X.Y.Z [skip ci]`, tags `vX.Y.Z`, and creates the GitHub release
109
- with `gtd.bundle.mjs` attached.
package/docs/loop.md DELETED
@@ -1,289 +0,0 @@
1
- # Driving the loop
2
-
3
- gtd (v3, the pattern machine) exposes three commands a loop driver combines:
4
-
5
- - **`gtd step <actor>`** — authenticate as `<actor>`, match the resolved rest's
6
- declared patterns against the pending changes, and commit (or squash) the one
7
- resulting transition. Performs AT MOST one transition — there is no fixpoint
8
- chain to drive. Pass `--cost=<n>` (optionally `--model=<name>`) to record the
9
- just-finished invocation's token cost and model on the turn commit; gtd sums
10
- them across the process into `it.processCost`/`it.processCostByModel` (see
11
- [Configuration: Token cost](configuration.md#token-cost)).
12
- - **`gtd next`** — print the resolved rest's rendered script/prompt/message,
13
- without mutating anything.
14
-
15
- gtd never executes a workflow script itself: at a `script`-content rest the
16
- DRIVER runs the emitted `content` and then steps that state's own actor.
17
-
18
- A loop is a simple cycle:
19
-
20
- 1. Run `gtd next --json` — see [`cli.md`](cli.md#gtd-next---json) for the exact
21
- `{state, actor, kind, content, model?, memory?, label?}` field shape. `kind`
22
- is the dispatch key: `"message"` → halt (a human rest); `"script"` → execute
23
- `content` verbatim (ignoring its exit code), then `gtd step <actor>` to
24
- capture the outcome; `"prompt"` → feed `content` to the agent, then
25
- `gtd step <actor>` yourself once it's done acting. The optional `model` is an
26
- opaque string the workflow author chose, which you map onto your own
27
- harness's model selection if you use one. The optional `label` is a
28
- human-readable display name for the resolved state, present only when that
29
- state declares `label:` in the workflow; when it's absent, a driver that
30
- wants to show something falls back to `state` itself (that fallback lives in
31
- the driver, not gtd core — see `bin/gtd-loop`'s
32
- `label="$(jq -r '.label // .state' <<<"$next_json")"`).
33
- 2. Repeat until a `"message"` rest halts the loop, or a zero-commit script step
34
- at idle settles it (the green terminal signal).
35
-
36
- A human acts by editing files (e.g. writing/editing `.gtd/TODO.md`, fixing
37
- code); `gtd step human` captures the edit as their turn. A driver can run that
38
- capture for the human as its opening move (see below), so re-launching the loop
39
- is the only thing the human ever does — at a gate they edit and re-run, and the
40
- loop picks their change up.
41
-
42
- **A `"message"` rest is a real handoff, not a slow beat.** No driver signs off
43
- for the human: in the bundled workflow the loop runs the whole cycle unattended
44
- up to `await-review` and then rests there with the review checkout window open
45
- (the reviewable diff surfaced as uncommitted changes — see
46
- [STATES.md §11](../STATES.md#11-the-review-checkout-window)). Advancing takes a
47
- human edit to `.gtd/REVIEW.md`: ticking every box with **no** comment signs off
48
- into the squash finale, while any comment (a note, or a code edit of your own)
49
- is feedback that routes into another build + re-review round. That gate is the
50
- contract, not a stall — nothing advances until the edit is captured, whether the
51
- capture happens within the same run or a later one. A review nobody is going to
52
- finish is ended with [`gtd abandon`](cli.md#gtd-abandon---json), which drops the
53
- process's commits and keeps their content as uncommitted changes.
54
-
55
- **Editing is on by default.** Reaching a `"message"` rest, `bin/gtd` opens
56
- `${VISUAL:-$EDITOR}` on the rest's declared `.file` (or the repo root `.` when
57
- the state declares none), blocking until the editor exits, then runs
58
- `gtd step human` to capture whatever changed:
59
-
60
- - a new commit was captured → the loop keeps driving on its own; re-launching is
61
- never necessary;
62
- - zero commits were captured (a clean tree once the editor closed) → the loop
63
- halts, exit 0 — "done for now";
64
- - the step refuses (a malformed steering file, or an edit matching no declared
65
- pattern) → the refusal is surfaced verbatim and the loop halts non-zero,
66
- exactly as a manual `gtd step human` refusal would.
67
-
68
- Set `GTD_NO_EDIT` (any non-empty value), or pass `--no-edit` — see
69
- [`cli.md`](cli.md#--no-edit---edit---once) for its exact syntax and positioning
70
- — to disable this and restore the original halt-and-print behavior: the loop
71
- prints the gate via `gtd next` and exits 0 without touching an editor, leaving
72
- the human to edit and re-launch the loop themselves.
73
-
74
- Pass `--edit` (`-e`) to force the editor open at the current human gate right
75
- now, overriding an ambient `GTD_NO_EDIT`/`--no-edit` for this one run — useful
76
- when a shell profile sets `GTD_NO_EDIT` by default but you want to edit
77
- interactively just this once. It only means something when the machine is
78
- actually resting at a human gate: at any other rest, `bin/gtd` prints a note
79
- that it isn't at one yet and simply keeps driving, rather than pretending to
80
- force something undefined.
81
-
82
- Pass `--once` to restrict the loop to exactly one beat — one human gate, one
83
- script check+step, or one agent prompt+step — then exit, instead of driving all
84
- the way to idle/settled. It combines freely with `--edit`/`--no-edit` (e.g.
85
- `gtd --edit --once` forces the editor at the current gate, captures the edit,
86
- and stops right there rather than continuing to drive).
87
-
88
- ```bash
89
- gtd next --json # ask who's up and what they should do
90
- ```
91
-
92
- See [`skills/loop/SKILL.md`](../skills/loop/SKILL.md) for the agent-facing
93
- instructions that follow the same pinned contract. `gtd` itself (bare, or with
94
- `loop` as its first argument — see below) is the packaged, ready-to-run
95
- implementation of that same script for anyone who doesn't want to drive the loop
96
- by hand.
97
-
98
- ## The reference loop driver
99
-
100
- A minimal bash implementation of the pinned protocol, driving an agent CLI (e.g.
101
- `claude -p`) against `gtd next --json` output. This is the authoritative
102
- reference for what a loop driver must do; keep any other implementation
103
- (including `skills/loop/SKILL.md`) consistent with it rather than editing both
104
- independently. Both open with the same move — capture the human's pending edit
105
- when the machine rests at a `"message"` gate — so `gtd` is the only command a
106
- human runs. `bin/gtd` is the packaged entry point: invoked bare, or with `loop`
107
- as its first argument, it runs this exact script; any other first argument (e.g.
108
- `next`, `step`, `status`) hands off to `node dist/gtd.bundle.mjs` instead. The
109
- loop body it runs adds six things on top of the reference script above: it opens
110
- an editor at every `"message"` gate instead of just printing it (see "Editing is
111
- on by default" above; `--no-edit`/`GTD_NO_EDIT` fall back to the reference
112
- script's plain print-and-exit, while `--edit`/`-e` forces it on for this run
113
- even over an ambient `GTD_NO_EDIT`); it can restrict itself to exactly one beat
114
- via `--once` instead of driving to idle/settled; it stops with a diagnostic if
115
- the same `"prompt"` state/content repeat with no progress (see
116
- `skills/loop/SKILL.md`'s "Stall detection"); it lets `GTD_LOOP_AGENT_CMD` swap
117
- in any coding agent CLI in place of the default `claude -p`, receiving the
118
- prompt via `$GTD_LOOP_PROMPT`; it exports the resolved state's optional `model`
119
- hint as `$GTD_LOOP_MODEL`, appending `--model "$GTD_LOOP_MODEL"` to the default
120
- `claude -p` invocation whenever it's non-empty; and it acts on the optional
121
- `memory` scope hint (see "Agent memory scope" below) — continuing one agent
122
- session across consecutive same-scope turns and starting fresh when the scope
123
- changes.
124
-
125
- `bin/gtd` resolves both its bundle hand-off and the loop's own internal `gtd`
126
- calls against `dist/gtd.bundle.mjs` next to its own location. Set `GTD_BIN` — a
127
- full command, not just a path — to point both at source instead, e.g. for
128
- development or testing:
129
-
130
- ```bash
131
- GTD_BIN="node $PWD/dev/run.mjs" bin/gtd
132
- ```
133
-
134
- ```bash
135
- #!/usr/bin/env bash
136
- set -euo pipefail
137
-
138
- # Opening move: if the machine rests at a human gate, capture the human's
139
- # pending edit first, so re-launching the loop is the ONLY thing a human does
140
- # (they never run `gtd step human` by hand). Peek with the pure `gtd next
141
- # --json` and step only at a `"message"` rest — at an agent/check rest
142
- # `gtd step human` would refuse out-of-turn, and a mid-cycle restart must just
143
- # resume driving. A clean gate is a no-op; a genuine refusal (malformed steering
144
- # file, unrecognized edit) stops the loop instead of being driven past.
145
- if [[ "$(gtd next --json | jq -r .kind)" == "message" ]]; then
146
- gtd step human >/dev/null
147
- fi
148
-
149
- while true; do
150
- next_json="$(gtd next --json)"
151
- state="$(jq -r .state <<<"$next_json")"
152
- actor="$(jq -r .actor <<<"$next_json")"
153
- kind="$(jq -r .kind <<<"$next_json")"
154
- content="$(jq -r .content <<<"$next_json")"
155
-
156
- if [[ "$kind" == "message" ]]; then
157
- echo "your turn ($state)" # bin/gtd renders this via emit_gate instead
158
- gtd next
159
- exit 0
160
- fi
161
-
162
- if [[ "$kind" == "script" ]]; then
163
- head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
164
- bash -c "$content" || true # run the emitted script; exit code ignored
165
- gtd step "$actor" >/dev/null # capture whatever it left in the tree
166
- head_after="$(git rev-parse HEAD 2>/dev/null || echo none)"
167
- if [[ "$head_before" == "$head_after" ]]; then
168
- echo "settled ($state: check passed, nothing to do)" # bin/gtd renders this via emit_settled instead
169
- exit 0
170
- fi
171
- continue
172
- fi
173
-
174
- # kind == "prompt": feed the prompt to the agent, then close out its turn.
175
- claude -p "$content" --dangerously-skip-permissions
176
- gtd step "$actor" >/dev/null
177
- done
178
- ```
179
-
180
- The driver — not the prompt text — owns ending the agent's turn
181
- (`gtd step "$actor"` right after the agent acts): every default-workflow agent
182
- prompt says explicitly not to run `gtd step agent` itself.
183
-
184
- `bin/gtd log` opens the current repo/worktree's loop logfile —
185
- `"$(worktree_git_dir)/gtd-loop.log"` by default, or `$GTD_LOOP_LOG` verbatim
186
- when set — in `${VISUAL:-$EDITOR}`, the same editor resolution `bin/gtd edit`
187
- uses. It takes no arguments and errors out (rather than opening an empty buffer)
188
- if no loop has produced a log yet. `worktree_git_dir` derives the git dir from
189
- the cwd with `GIT_DIR`/`GIT_WORK_TREE` scrubbed, so an inherited `GIT_DIR`
190
- (leaked from a parent git process, a hook, or another worktree's shell) can
191
- never key the log — or the loop's memory marker — to a different worktree; two
192
- worktrees looping at once each keep their own log.
193
-
194
- ## Rendered output and logging
195
-
196
- `bin/gtd` detects its output capability once at startup (`FANCY`, true when
197
- stdout is a real terminal AND `NO_COLOR` is unset): on a real terminal it prints
198
- one colored/emoji line per event, and under `NO_COLOR` or a piped/redirected
199
- stdout it prints the same events as plain ASCII with no escape codes, per the
200
- [NO_COLOR convention](https://no-color.org). Normal events (a state transition,
201
- a captured commit, an agent/check turn starting, settling at idle) render to
202
- stdout; refusals, stalls, and validate-cap halts render to stderr, each followed
203
- by a `📄 see <log>` pointer (plain: `see <log>`).
204
-
205
- The three previously terminal-visible subprocess streams — the agent turn's own
206
- output, the check script's own output, and `gtd step`'s own output — are no
207
- longer printed directly: they're appended to the per-repo/per-worktree log file
208
- (truncated once at the start of the run) instead, and surfaced with
209
- `bin/gtd log` above.
210
-
211
- ## Agent memory scope
212
-
213
- A `"prompt"` beat may carry an optional `memory` key — an opaque scope label the
214
- workflow author chose (e.g. `plan`, `build`, `fix`). gtd never interprets it; it
215
- signals to the driver whether this agent turn should continue from the previous
216
- turn's memory or start fresh. The rule is a comparison, not a command: track the
217
- `memory` of the last `"prompt"` beat (script/message beats have none and never
218
- change it), and when a new `"prompt"` beat's `memory` **equals** the tracked
219
- value, continue the same agent session; when it **differs** — or it is the first
220
- turn, or either side has no `memory` — start fresh.
221
-
222
- This is what makes a loop retain memory while a phase boundary clears it: a loop
223
- that keeps re-entering one state emits the same label every lap, so the agent
224
- accumulates context across the loop; the move to the next phase's
225
- differently-labelled state resets it. `bin/gtd` implements this by mapping the
226
- signal onto claude's session flags — `--session-id` to pin a fresh session when
227
- the scope changes, `--resume` to continue it while the scope holds — and
228
- persists the current scope + session id in the git dir (never the working tree,
229
- so `gtd status` and the pending diff never see them) so retention survives even
230
- across the restarts a human gate forces mid-loop. See `skills/loop/SKILL.md`'s
231
- "Agent memory scope" for the same contract stated for a hand-driven loop.
232
-
233
- ## Using a different agent
234
-
235
- `gtd` (bare, or `gtd loop`) defaults to
236
- `claude -p "$GTD_LOOP_PROMPT" --dangerously-skip-permissions`, but the agent
237
- invocation is swappable: set `GTD_LOOP_AGENT_CMD` to any shell command, and it
238
- runs with the prompt available as `$GTD_LOOP_PROMPT` in its environment, along
239
- with the resolved state's opaque `model` hint (if any) as `$GTD_LOOP_MODEL` and
240
- its `memory` scope as `$GTD_LOOP_MEMORY` / `$GTD_LOOP_SESSION_ID` /
241
- `$GTD_LOOP_MEMORY_RESUME` (`1` when this turn should continue the prior
242
- session). An adapter that ignores any of these keeps working unchanged. For
243
- example, to drive a different agent CLI:
244
-
245
- ```bash
246
- GTD_LOOP_AGENT_CMD='my-agent-cli --prompt "$GTD_LOOP_PROMPT"' gtd
247
- ```
248
-
249
- ## Herdr integration (optional)
250
-
251
- `bin/gtd` optionally reports its lifecycle to [Herdr](https://herdr.dev) (a
252
- terminal multiplexer for coding agents) via a `herdr` CLI binary, entirely at
253
- the driver's edge — gtd core never talks to Herdr. Every call is best-effort
254
- (guarded, output discarded, `|| true`), so a missing/failing `herdr` binary
255
- never blocks, slows, or fails the loop. Because that guard also hides a herdr
256
- CLI-signature mismatch, set `GTD_HERDR_DEBUG=1` to surface every `herdr` call
257
- and its exit code on stderr instead.
258
-
259
- The reporting is a no-op unless all three guard conditions hold: `HERDR_ENV=1`,
260
- a non-empty `$HERDR_PANE_ID`, and `herdr` on `$PATH`. When they do, `gtd` makes
261
- three kinds of calls (note the positional `<PANE_ID>` comes BEFORE the options —
262
- herdr's `pane` subcommands reject a trailing pane id):
263
-
264
- - `herdr pane report-agent "$HERDR_PANE_ID" --source herdr:gtd --agent gtd --state <state> --message <label>`
265
- - `herdr pane release-agent "$HERDR_PANE_ID" --source herdr:gtd --agent gtd`
266
- - `herdr notification show <title> --body <label> --sound request`
267
-
268
- mapped onto the loop's states like this:
269
-
270
- | Loop moment | Call(s) |
271
- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
272
- | Top of every non-`"message"` iteration | `report-agent --state working` |
273
- | Right before halting at a `"message"` (human) gate (editing off) | `report-agent --state blocked` + `notification show` |
274
- | While the editor is open at a human gate (editing on) | `report-agent --state blocked`, flipped back to `working` once it closes |
275
- | Right before a clean settle (a script rest, zero new commits) | `report-agent --state idle` + `release-agent` |
276
- | Any non-zero exit (stall guard, validate-cap, `gtd next` failure, unhandled failure) | `report-agent --state blocked` + `notification show`, via an `EXIT` trap |
277
-
278
- `working`/`blocked` reports use the resolved rest's `label` (falling back to
279
- `state`, per the field above); the trap falls back to `$GTD_LAST_LABEL`, the
280
- label tracked from the last completed iteration, when it fires outside the
281
- normal per-iteration flow. `report-agent` claims display authority for the
282
- `herdr:gtd` source, so `working` is re-reported every lap rather than once, to
283
- stay fresh against Herdr's own heuristic detection of the inner agent process.
284
-
285
- Set `GTD_NO_NOTIFY` (any non-empty value) or pass `--no-notify` to suppress the
286
- two `notification show` calls above (the human-gate halt and the exit trap) for
287
- this run — the OS popup + sound only. `report-agent`/`release-agent` (the silent
288
- Herdr sidebar status) are untouched either way, mirroring
289
- `--no-edit`/`GTD_NO_EDIT`'s flag/env shape.