@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
|
@@ -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.
|