@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.
@@ -0,0 +1,194 @@
1
+ # Design: the builder / work-packages flow as pure workflow configuration
2
+
3
+ > Status: LANDED, then relocated (2026-07-22) — Option A shipped in the bundled
4
+ > default workflow's `picking` state; the bundled default has since been
5
+ > drastically simplified and no longer includes it. The full Option-A machine
6
+ > now lives as a copy-paste example at
7
+ > [docs/examples/advanced-workflow.md](../examples/advanced-workflow.md). The
8
+ > rest of this document is kept as the decision record: why Option A was chosen
9
+ > over B/C/D, and the process-per-task topology (§6) remains a documented,
10
+ > unimplemented recipe.
11
+ >
12
+ > Context (as originally written): v2 had an engine-level package loop
13
+ > (`hasPackages` / `packagesRemaining` guards, the `close-package` label,
14
+ > `removePackageDir`). v3 deleted all of it; the bundled default's `building`
15
+ > state used to implement the entire task set in ONE monolithic agent turn. This
16
+ > doc plans how to get the incremental task-by-task loop back **without the
17
+ > engine ever learning what a "package" is** — it stays a concern of the
18
+ > workflow configuration.
19
+
20
+ ## 1. What the loop actually needs
21
+
22
+ The per-task cycle is: _pick the next task → implement it → check → (fix loop) →
23
+ close the task → if tasks remain, loop; else review_. Decomposed into v3
24
+ vocabulary:
25
+
26
+ | Need | Already expressible? |
27
+ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
+ | Task queue | Yes — files under a glob (e.g. `.gtd/tasks/NN-name.md`), written by `decompose`. |
29
+ | "Close a task" | Yes — the actor deletes the task file; the diff carries `D .gtd/tasks/…`. |
30
+ | Per-task check + fix loop | Yes — the existing `checking`/`fixing` shape, unchanged. |
31
+ | Feed ONE task into the prompt | Mostly — a file naming the current task can be `it.read()` into the building prompt. |
32
+ | **The exit branch: "none left?"** | **No.** Patterns match the pending **diff**; deleting the _last_ task file produces the same diff shape as any other (`D .gtd/tasks/…`). Emptiness of a glob is a **tree** property, invisible to the grammar. |
33
+
34
+ So the whole design question is: who decides "the queue is empty", and how does
35
+ that decision become a diff the patterns can see? Everything below is an answer
36
+ to only that.
37
+
38
+ ## 2. Option A — scripted arbiter state (pure config, deterministic) — RECOMMENDED
39
+
40
+ Apply the `checking` discipline to the queue itself: a `script` state whose only
41
+ job is to inspect the tree (which patterns can't) and leave a file-op verdict
42
+ (which patterns can). Mechanics in the script, semantics in the `on` map — no
43
+ trust in an agent, no engine change.
44
+
45
+ ```yaml
46
+ decompose:
47
+ actor: agent
48
+ prompt: |
49
+ … write ordered task files under .gtd/tasks/ …
50
+ on:
51
+ "* .gtd/tasks/**": picking
52
+
53
+ picking: # the deterministic queue arbiter
54
+ actor: check
55
+ script: |
56
+ next=$(ls .gtd/tasks/*.md 2>/dev/null | head -n 1)
57
+ if [ -n "$next" ]; then
58
+ printf '%s' "$next" > .gtd/NEXT.md
59
+ else
60
+ rm -f .gtd/NEXT.md
61
+ fi
62
+ on:
63
+ "D .gtd/NEXT.md": reviewing # queue just emptied — MUST precede the `*` row
64
+ "* .gtd/NEXT.md": building # queue non-empty (A first time, M after)
65
+ "C": reviewing # queue empty and NEXT.md never existed
66
+
67
+ building:
68
+ actor: agent
69
+ prompt: |
70
+ Implement exactly ONE task: <%~ it.read(".gtd/NEXT.md") %> — read that
71
+ file, do the work, delete the task file when done. Touch nothing else
72
+ under .gtd/.
73
+ on:
74
+ "* **": checking
75
+
76
+ checking:
77
+ # unchanged, except green loops back to the arbiter:
78
+ on:
79
+ "A .gtd/FEEDBACK.md": fixing
80
+ "M .gtd/FEEDBACK.md": fixing
81
+ "C": picking
82
+ ```
83
+
84
+ Properties:
85
+
86
+ - **Deterministic.** The branch is a shell `ls`, not an agent's memory. Every
87
+ verdict lands in the commit history as an ordinary `gtd(check): <state>` turn.
88
+ - **`NEXT.md` is both the verdict and the context** — the building prompt
89
+ interpolates it, so each building turn sees exactly one task.
90
+ - The three `on` rows are total over the script's possible outcomes
91
+ (write/overwrite, delete, no-op) — no refusal path.
92
+ - Cost: one extra state and one extra driver round-trip per task (same cost
93
+ `checking` already pays; the loop already executes scripts via `gtd run`).
94
+
95
+ ## 3. Option B — agent-encoded verdict (pure config, zero extra states)
96
+
97
+ The v3 verdict discipline says branch outcomes are encoded by _which files the
98
+ actor writes or deletes_ (`D FEEDBACK.md` = approve). Apply it here: `decompose`
99
+ also writes a marker `.gtd/tasks/OPEN`; the building/closing prompt instructs:
100
+ _implement the next task and delete its file; if it was the last one, also
101
+ delete `.gtd/tasks/OPEN`_. Declaration order does the rest:
102
+
103
+ ```yaml
104
+ building:
105
+ on:
106
+ "D .gtd/tasks/OPEN": reviewing # declared first — wins when present
107
+ "* **": checking
108
+ ```
109
+
110
+ - Pro: no extra state, no script execution needed (works for drivers that only
111
+ handle prompt/message states).
112
+ - Con: correctness rests on the agent honouring the marker protocol. The failure
113
+ mode is soft (a forgotten marker just means one more "none left, delete the
114
+ marker" turn), but the branch is not deterministic and never appears as a
115
+ machine-authored verdict.
116
+
117
+ Documented as the zero-infrastructure variant; A is preferred wherever the
118
+ driver can run scripts.
119
+
120
+ ## 4. Option C — generic engine helper: tree-predicate guards on patterns
121
+
122
+ The one candidate engine change, if the arbiter round-trip ever proves too
123
+ heavy. The grammar gains an optional **tree predicate** appended to a pattern:
124
+ `<event> & N <glob>` ("and no file matching glob exists in the tree after the
125
+ pending changes") / `& E <glob>` ("and at least one exists"):
126
+
127
+ ```yaml
128
+ building:
129
+ on:
130
+ "D .gtd/tasks/** & N .gtd/tasks/**": reviewing # closed the LAST task
131
+ "D .gtd/tasks/**": picking # closed a task, more remain
132
+ ```
133
+
134
+ - Still fully agnostic: the engine learns _glob emptiness_, never "packages".
135
+ - δ-purity holds in the same sense as today: the decision is a function of (HEAD
136
+ tree + pending diff); the tree listing is already available at the edge
137
+ (`changedPaths` / `WorktreeReader`).
138
+ - The `&` form keeps rows **event-triggered** (a bare `N …` row that could fire
139
+ on any unrelated diff would be a foot-gun); the predicate only guards.
140
+ - Cost: pattern parser/matcher/validation growth, resolve needs a tree listing,
141
+ STATES.md §3 grows a second concept, property tests. Deliberately **deferred**
142
+ — Option A covers the need with zero engine surface; this only pays for itself
143
+ if arbiter states show up in every workflow and feel like boilerplate.
144
+
145
+ ## 5. Option D — template enumeration helper: `it.ls(glob)`
146
+
147
+ Small, orthogonal, generic: add `ls(glob) → string[]` (sorted, working-tree)
148
+ next to `read(path)` in the template context.
149
+
150
+ - Lets Option A's script shrink to pure verdict-writing and lets prompts and
151
+ messages show the queue ("Remaining tasks:\n<%=
152
+ it.ls('.gtd/tasks/*.md').join('\n') %>") without shell gymnastics.
153
+ - Lets Option B's prompt state the ground truth instead of trusting agent memory
154
+ ("these tasks remain: …; if this list has one entry, also delete the marker").
155
+ - No new decision power (templates render content, they don't route), so it
156
+ cannot compromise the δ discipline.
157
+
158
+ ## 6. A topology variant worth knowing: process-per-task
159
+
160
+ Nothing stops a workflow from making **each task its own process**: the per-task
161
+ cycle ends in its own `commit:` state, squashing just that task's work into one
162
+ commit; the squash boundary resolves back to the initial state, whose router (an
163
+ Option-A-style arbiter at or right after `initial`) picks the next task or falls
164
+ through to the review/idle path.
165
+
166
+ Why it's attractive:
167
+
168
+ - **Per-task commits** — history reads one commit per task instead of one per
169
+ cycle.
170
+ - **Per-task budgets** — `retry` visit-counts are process-scoped, so today a
171
+ 10-task cycle POOLS `fixing`'s `max: 3` across all tasks; task-sized processes
172
+ give each task its own cap. (This interaction exists regardless of which
173
+ option is chosen and should be called out in docs.)
174
+ - Smaller diffs at review time.
175
+
176
+ Why it's not the default recommendation: the initial state doubles as the human
177
+ entry point, so the router shape is more intricate (the arbiter must distinguish
178
+ "mid-flight, tasks remain" from "fresh repo, wait for a human"), and cycle-level
179
+ review/squash semantics change meaningfully. A worked example belongs in
180
+ `docs/configuration.md` once the basic loop lands.
181
+
182
+ ## 7. Recommendation
183
+
184
+ 1. **Now (config-only, no engine change):** re-author the bundled default's
185
+ builder segment as the Option-A loop
186
+ (`decompose → picking ⇄ building ⇄ checking/fixing → reviewing`), with the
187
+ `NEXT.md` arbiter script and the one-task building prompt. Document Option B
188
+ in `docs/configuration.md` as the script-less variant.
189
+ 2. **Now (one small generic helper):** add `it.ls(glob)` (Option D) — cheap,
190
+ broadly useful, and it makes both A and B materially better.
191
+ 3. **Deferred:** Option C (`& N/E <glob>` tree-predicate guards) — revisit only
192
+ if arbiter boilerplate becomes a demonstrated pain across real workflows.
193
+ 4. **Docs either way:** call out the retry-cap pooling interaction (§6) and the
194
+ process-per-task topology as a recipe.
@@ -0,0 +1,101 @@
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 default workflow (`src/workflows/default.yaml`) is imported as raw text
35
+ via tsdown's `.yaml`-as-text loader (`tsdown.config.ts`) and compiled through
36
+ the same path a `.gtdrc` `workflow:` key goes through
37
+ (`src/workflows/default.ts`) — no privileged code path.
38
+
39
+ `npm run dev` runs `src/main.ts` directly via Node's native TypeScript
40
+ type-stripping (requires Node 22.6+). It registers `dev/hooks.mjs`, which
41
+ resolves `./Foo.js` specifiers to the on-disk `./Foo.ts` and imports `*.yaml`
42
+ files as raw text, mirroring tsdown's `.yaml`-as-text loader — this is what lets
43
+ `src/workflows/default.yaml` resolve when running from source.
44
+
45
+ The decision core is pure and IO-free: the pattern machine's shape (states,
46
+ patterns, retry) is a plain-data `WorkflowDefinition` (`src/PatternMachine.ts`),
47
+ and the same module's `resolveState`/`step` are the pure resolver/interpreter
48
+ over it — so the whole engine is trivially unit-testable in isolation; all
49
+ git/filesystem/template IO is confined to the edge (`src/Edge.ts`).
50
+
51
+ `npm run build` produces `dist/gtd.bundle.mjs`, which npm exposes as the `gtd`
52
+ binary via the `bin` field in `package.json`.
53
+
54
+ ## The LSP server
55
+
56
+ `gtd lsp` (`src/Lsp.ts`) serves editor tooling over `.gtd/`'s two steering-file
57
+ formats — document symbols and diagnostics for `.gtd/TODO.md`'s open questions
58
+ and `.gtd/REVIEW.md`'s review chunks/hunks, plus check/uncheck code actions for
59
+ the latter. It's keyed on file NAME, not workflow state: a v3 workflow declares
60
+ no state→file mapping (a state names no file), so the server needs no
61
+ git/`.gtdrc` dependency at all and serves whatever content the editor hands it
62
+ over the LSP protocol, exactly like any other document.
63
+
64
+ `src/OpenQuestions.ts` and `src/ReviewDoc.ts` are the pure parsers behind both
65
+ the LSP and the bundled default workflow's own bash validators
66
+ (`todo-validating`/`review-validating` in `src/workflows/default.yaml`) — see
67
+ [docs/design/steering-file-loops.md](design/steering-file-loops.md) for the
68
+ "executable spec ↔ bash validator" contract linking the two independent
69
+ implementations, and each module's own doc comment for the format it defines.
70
+ Their unit tests are that format's spec tests.
71
+
72
+ ## Mutation testing
73
+
74
+ Run mutation testing on-demand with `npm run test:mutation` (StrykerJS) — never
75
+ run it as part of routine development; it is a deliberate, manually-triggered
76
+ check. `stryker.config.json`'s `mutate` list names the v3 pattern-machine module
77
+ set (`src/PatternMachine.ts`, `src/PatternConfig.ts`, `src/PatternTemplates.ts`,
78
+ `src/Edge.ts`, `src/Config.ts`, `src/Format.ts` — see
79
+ [Architecture](../AGENTS.md#the-pattern-machine-module-map) for the v3 module
80
+ map), plus the steering-file format parsers and the LSP built on them
81
+ (`src/OpenQuestions.ts`, `src/ReviewDoc.ts`, `src/Lsp.ts` — see
82
+ ["The LSP server"](#the-lsp-server) below). `src/Lsp.ts`'s protocol-adapter tail
83
+ (`startLspServer`) is exercised only by the `@live` e2e (excluded from the
84
+ mutation run's Cucumber harness), so mutants confined to that tail may survive
85
+ without it being a coverage gap in the ordinary test suite — the pure
86
+ symbol/edit/diagnostic builders above it are unit-tested and mutation-checked
87
+ normally.
88
+
89
+ `src/Git.ts` is excluded: the Cucumber harness stubs git at the Effect boundary,
90
+ so `Git.ts` mutants have zero in-memory coverage.
91
+
92
+ The HTML report lands in `reports/mutation/mutation.html` (git-ignored).
93
+
94
+ ## Releasing
95
+
96
+ Releases are automatic. Push releasable Conventional Commits (`fix:`, `feat:`,
97
+ or breaking changes) to `main` and the Release workflow runs the tests, then
98
+ `npx semantic-release`. Semantic-release computes the next version, writes it
99
+ into `package.json`, builds the bundle, commits the bump back as
100
+ `chore(release): X.Y.Z [skip ci]`, tags `vX.Y.Z`, and creates the GitHub release
101
+ with `gtd.bundle.mjs` attached.