@pmelab/gtd 3.4.0 → 3.6.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/docs/cli.md CHANGED
@@ -6,13 +6,20 @@ Usage: gtd [command] [options]
6
6
  Commands:
7
7
  step <actor> Authenticate as <actor>, match the resolved rest's
8
8
  declared patterns against the pending changes, and commit
9
- (or squash) the one resulting transition
9
+ (or squash) the one resulting transition. Pass
10
+ --cost=<n> (optionally --model=<name>) to record the
11
+ just-finished invocation's token cost and model on the
12
+ turn commit (summed into it.processCost/processCostByModel)
10
13
  next Print the resolved rest's rendered script/prompt/message
11
14
  (no mutation)
12
15
  run Execute the resolved rest's emitted script, then step its
13
16
  actor (the built-in script driver)
14
17
  status Print the resolved rest's state/actor and which declared
15
18
  pattern (if any) each pending change matches (no mutation)
19
+ validate Validate the steering file the resolved rest declares
20
+ (its file:/mode:) against that mode's format; exits
21
+ non-zero with findings when the file violates it (no
22
+ mutation)
16
23
  mermaid Print the active workflow's shape as Mermaid
17
24
  stateDiagram-v2 source (no mutation)
18
25
  format <file> Format a markdown file in place
@@ -20,6 +27,8 @@ Commands:
20
27
 
21
28
  Options:
22
29
  --json Output structured JSON instead of plain text
30
+ --cost=<n> (gtd step only) record the invocation's token cost
31
+ --model=<name> (gtd step only, with --cost) tag that cost's model
23
32
  --version, -v Print version and exit
24
33
  --help, -h Print this help and exit
25
34
  ```
@@ -32,11 +41,15 @@ without touching the repository. Every other command must be run from the
32
41
  history relative to cwd, so it refuses with a clear error if invoked from a
33
42
  subdirectory.
34
43
 
35
- `--json` is the only long option. Any other `--` option (including a typo like
36
- `--jsn`) is rejected with a usage error rather than silently ignored, so a
37
- mistyped flag can never degrade a JSON caller to plain-text mode.
44
+ `--json`, `--cost=<n>`, and `--model=<name>` (the latter two only for
45
+ `gtd step`) are the only long options. Any other `--` option (including a typo
46
+ like `--jsn`) is rejected with a usage error rather than silently ignored, so a
47
+ mistyped flag can never degrade a JSON caller to plain-text mode. A bare
48
+ `--cost`/`--model` with no value, a non-numeric or negative `--cost`, an empty
49
+ `--model`, `--model` without `--cost`, or either flag on any command other than
50
+ `gtd step` are all usage errors.
38
51
 
39
- ## `gtd step <actor>`
52
+ ## `gtd step <actor> [--cost=<n>] [--model=<name>]`
40
53
 
41
54
  Authenticates `<actor>` against the resolved rest and performs the ONE resulting
42
55
  transition. Unlike v2, there is no fixpoint chain to drive: the pattern
@@ -45,6 +58,16 @@ authors at most one commit (a normal turn) or performs one squash
45
58
  (§[STATES.md](../STATES.md#8-the-squash-lifecycle)) — never both, never a chain
46
59
  of several. A caller that wants several transitions issues several invocations.
47
60
 
61
+ `--cost=<n>` records the token cost of the invocation that produced the pending
62
+ changes (a non-negative number), and the optional `--model=<name>` tags it with
63
+ the model that ran, as a `Gtd-Cost: <n> <model>` trailer on the turn commit —
64
+ persisted in the git log, summed across the process into `it.processCost` and
65
+ grouped by model into `it.processCostByModel` (see
66
+ [Configuration: Token cost](configuration.md#token-cost)). `--model` requires
67
+ `--cost`. On a squash they are folded into the process total/breakdown the
68
+ `commit:` template renders rather than written as a trailer. Both are orthogonal
69
+ to `--json`.
70
+
48
71
  - **Out-of-turn refusal** — `<actor>` isn't the resolved state's declared actor:
49
72
  exit non-zero, zero commits.
50
73
  `gtd step <actor>: out of turn — "<state>" awaits <awaited-actor>`
@@ -69,10 +92,17 @@ or, at a no-op:
69
92
  nothing to do at "idle"
70
93
  ```
71
94
 
72
- `--json` emits `{state, subject}` — `subject` is `null` at a no-op:
95
+ `--json` emits `{state, subject}` — `subject` is `null` at a no-op — plus
96
+ `cost`/`model` keys echoing what `--cost`/`--model` recorded (each omitted when
97
+ not passed):
73
98
 
74
99
  ```json
75
- { "state": "grilling", "subject": "gtd(human): grilling" }
100
+ {
101
+ "state": "grilling",
102
+ "subject": "gtd(human): grilling",
103
+ "cost": 1450,
104
+ "model": "claude-opus-4-8"
105
+ }
76
106
  ```
77
107
 
78
108
  ## `gtd next [--json]`
@@ -171,15 +201,21 @@ Pending:
171
201
  or, on a clean tree: `Pending: (clean)`. A `Model: <value>` line appears right
172
202
  after `Awaits:` when the resolved state declares a `model:` hint, and
173
203
  `Memory: <value>`/`File: <value>`/`Mode: <value>` lines appear after that (in
174
- that order) when declared — each independently, only when set.
204
+ that order) when declared — each independently, only when set. A `Cost: <n>`
205
+ line appears after those when the current process has accumulated any
206
+ `Gtd-Cost:` (see [`gtd step --cost`](#gtd-step-actor---costn-model-name)),
207
+ omitted when the running total is `0`; when the breakdown adds information (more
208
+ than one model, or a single tagged model), indented ` <model>: <cost>` lines
209
+ follow it.
175
210
 
176
211
  `--json` emits
177
- `{state, actor, changes: [{status, path, pattern}], model?, memory?, file?, mode?, edges?}`
212
+ `{state, actor, changes: [{status, path, pattern}], model?, memory?, file?, mode?, cost?, costByModel?, edges?}`
178
213
  — `pattern` is `null` when no declared row matches that change;
179
214
  `model`/`memory`/`file`/`mode` are present only when the resolved state declares
180
- them (omitted entirely, never `null`, otherwise); `edges` is the resting state's
181
- `on` edges as `[{ pattern, target, describe? }]` (same as `gtd next --json`),
182
- omitted when the state has no `on`:
215
+ them; `cost`/`costByModel` (the `[{model, cost}]` breakdown) only when the
216
+ running process total is above `0`; and `edges` is the resting state's `on`
217
+ edges as `[{ pattern, target, describe? }]` (same as `gtd next --json`), omitted
218
+ when the state has no `on` (all omitted entirely, never `null`, otherwise):
183
219
 
184
220
  ```json
185
221
  {
@@ -194,6 +230,43 @@ omitted when the state has no `on`:
194
230
 
195
231
  `gtd status` takes no arguments — extra positional args are rejected.
196
232
 
233
+ ## `gtd validate [--json]`
234
+
235
+ Format **and** validate the steering file the resolved rest declares. It
236
+ resolves the current state exactly like `gtd status`, renders that state's
237
+ `file:`, **formats that file in place** (the same markdown formatter as
238
+ `gtd format`), then runs the parser its `mode:` selects over the formatted
239
+ contents — `qa` → `src/OpenQuestions.ts`, `review` → `src/ReviewDoc.ts` (the
240
+ same pure parsers the LSP publishes as diagnostics, so there is one source of
241
+ truth per format).
242
+
243
+ - A well-formed file exits `0` (`<file>: valid`, or `{"valid":true, ...}` with
244
+ `--json`) — and is left formatted.
245
+ - Violations exit **non-zero** with the findings, one per line — the signal a
246
+ producing agent, or the driving loop, loops on until the file is well-formed.
247
+ - A state that declares no `file:`/`mode:`, or whose file is **absent** (e.g.
248
+ `building` deleted `.gtd/TODO.md`, or a human deleted `.gtd/REVIEW.md` to
249
+ approve), has nothing to validate and exits `0`
250
+ (`nothing to validate at "<state>"`). Nothing is formatted in that case.
251
+
252
+ This is how the bundled default keeps its steering files tidy and well-formed
253
+ without an in-machine validation state — and the check runs whoever last touched
254
+ the file, agent or human:
255
+
256
+ - **The producing agent self-validates.** Plain `gtd next` appends a "run
257
+ `gtd validate` and fix all violations" instruction to a `prompt` rest that
258
+ declares `file:`+`mode:`; `gtd next --json` withholds it and the driving loop
259
+ runs `gtd validate` after the turn instead (see `bin/gtd-loop` /
260
+ [STATES.md §12](../STATES.md#12-steering-file-validation-gtd-validate)).
261
+ - **`gtd step` enforces the same gate.** Capturing a turn out of a state that
262
+ declares `file:`+`mode:` formats that file in place and validates it first,
263
+ and **refuses the step** (committing nothing) when it is invalid — so a
264
+ human's edit at a gate (answering at `grilling-answer`, reviewing at
265
+ `await-review`) is formatted and checked exactly like an agent's draft, and a
266
+ malformed steering file is never committed.
267
+
268
+ `gtd validate` takes no arguments.
269
+
197
270
  ## `gtd mermaid`
198
271
 
199
272
  Pure emitter of the active workflow's **shape** — not the resolved rest — as
@@ -228,7 +301,7 @@ Mermaid-aware renderer (GitHub, GitLab, VS Code, Obsidian, the
228
301
 
229
302
  State names are aliased to Mermaid-safe identifiers (non-word characters fold to
230
303
  `_`; a digit-led name gets an `s_` prefix) via a `state "<name>" as <alias>`
231
- declaration up front, so a hyphenated name like `todo-validating` still displays
304
+ declaration up front, so a hyphenated name like `grilling-answer` still displays
232
305
  with its exact declared spelling. Rejects `--json` (exit 1,
233
306
  `gtd mermaid does not accept --json`) — there is no structured shape to emit
234
307
  beyond the Mermaid source itself — and takes no arguments (extra positional args
@@ -256,10 +329,9 @@ Errors (all exit 1, message on stderr):
256
329
  Starts an LSP server over stdio for `.gtd/` steering files — document symbols
257
330
  for a `qa`-mode file's open questions and a `review`-mode file's review
258
331
  chunks/hunks, code actions to check/uncheck a hunk or a whole chunk, and
259
- diagnostics publishing the same parser findings the bundled workflow's
260
- `.gtd/FORMAT.md` validators produce (see `src/OpenQuestions.ts` /
261
- `src/ReviewDoc.ts` and
262
- [STATES.md §10](../STATES.md#10-the-bundled-default-workflow)).
332
+ diagnostics publishing the same parser findings `gtd validate` reports (see
333
+ `src/OpenQuestions.ts` / `src/ReviewDoc.ts` and
334
+ [STATES.md §12](../STATES.md#12-steering-file-validation-gtd-validate)).
263
335
 
264
336
  **Config-driven** (see
265
337
  [docs/design/state-file-association.md](design/state-file-association.md)): the
@@ -322,17 +322,66 @@ head is preserved under `refs/gtd/review-head` (the base under
322
322
  Every `script`/`prompt`/`message`/`commit`/`model`/`memory`/`file` template is
323
323
  rendered as an Eta template (`it.<name>`) with:
324
324
 
325
- | Variable | Meaning |
326
- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
327
- | `startCommit` | The hash the current process started from (before its first turn). |
328
- | `currentCommit` | HEAD's hash at render time. |
329
- | `previousCommit` | The hash before the last transition (HEAD's parent, in-process). |
330
- | `state` | The state whose content is being rendered. |
331
- | `actor` | The actor this render is for. |
332
- | `processDiff` | `startCommit..HEAD` plus the pending working-tree diff. |
333
- | `lastDiff` | The diff of the last transition alone. |
334
- | `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)). |
335
- | `vars` | The merged three-layer variable map, always a flat `Record<string, string>` — see ["Variables"](#variables) below. |
325
+ | Variable | Meaning |
326
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
327
+ | `startCommit` | The hash the current process started from (before its first turn). |
328
+ | `currentCommit` | HEAD's hash at render time. |
329
+ | `previousCommit` | The hash before the last transition (HEAD's parent, in-process). |
330
+ | `state` | The state whose content is being rendered. |
331
+ | `actor` | The actor this render is for. |
332
+ | `processDiff` | `startCommit..HEAD` plus the pending working-tree diff. |
333
+ | `lastDiff` | The diff of the last transition alone. |
334
+ | `processCost` | The accumulated token cost of the current process — the sum of every `Gtd-Cost:` trailer recorded by `gtd step <actor> --cost=<n>` across its turn commits, plus the in-flight step's own `--cost` (so a `commit:` squash template sees the whole-process total). A number, `0` when none recorded. See ["Token cost"](#token-cost). |
335
+ | `processCostByModel` | The same accumulated cost broken down per model: an array of `{ model, cost }`, highest-cost first, one entry per distinct `--model` tag (costs recorded with no `--model` group under `"unspecified"`). A squash template iterates it to itemize the feature's cost per model. Empty when none recorded. See ["Token cost"](#token-cost). |
336
+ | `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)). |
337
+ | `vars` | The merged three-layer variable map, always a flat `Record<string, string>` — see ["Variables"](#variables) below. |
338
+
339
+ ### Token cost
340
+
341
+ A loop driver that knows how many tokens the invocation it just drove cost — and
342
+ on which model — can record both with
343
+ `gtd step <actor> --cost=<n> [--model=<name>]` (`<n>` a non-negative number).
344
+ gtd appends them to that turn's commit as a `Gtd-Cost: <n> <model>` trailer — a
345
+ blank line then the trailer, below the untouched `gtd(<actor>): <state>` subject
346
+ — so the per-turn cost and model are persisted in the git log:
347
+
348
+ ```
349
+ gtd(agent): reviewing
350
+
351
+ Gtd-Cost: 1450 claude-opus-4-8
352
+ ```
353
+
354
+ `--model` is optional and requires `--cost` (a model tag with no token count
355
+ records nothing to sum); a cost recorded without a model is grouped under
356
+ `"unspecified"`. `gtd status` shows the process's running total and, when the
357
+ breakdown adds information, a per-model split (a `Cost:` line with indented
358
+ `<model>: <cost>` lines / a `cost` number and a `costByModel` array, omitted
359
+ when nothing has been recorded). `gtd step --json` echoes what it recorded
360
+ (`{ "state": …, "subject": …, "cost": 1450, "model": "claude-opus-4-8" }`).
361
+
362
+ Every template sees the running total as `it.processCost` and the per-model
363
+ breakdown as `it.processCostByModel` (the table above): the current process's
364
+ turn-commit entries, plus the in-flight step's own `--cost`/`--model`. Their
365
+ intended use is a `commit:` squash template, which renders against the pending
366
+ tree as the process collapses — so the whole feature's total AND its per-model
367
+ itemization (including the squashing step itself) land in the squash message
368
+ even though the intermediate per-turn trailers are discarded with the turns they
369
+ rode on:
370
+
371
+ ```yaml
372
+ done:
373
+ commit: |
374
+ feat: ship it
375
+
376
+ Total token cost: <%= it.processCost %>
377
+ <% it.processCostByModel.forEach(function(m){ %>
378
+ - <%= m.model %>: <%= m.cost %>
379
+ <% }) %>
380
+ ```
381
+
382
+ `--cost`/`--model` are accepted only by `gtd step` (a usage error on any other
383
+ command), and gtd never interprets the number's unit (tokens, cents, whatever
384
+ the driver records) or the model name — it only records, sums, and groups them.
336
385
 
337
386
  ## Variables
338
387
 
@@ -45,14 +45,14 @@ vars:
45
45
  feedbackFile: .gtd/FEEDBACK.md
46
46
  ```
47
47
 
48
- Per-state mapping (12 states):
48
+ Per-state mapping (10 states):
49
49
 
50
- | States | `file:` | `mode:` |
51
- | ------------------------------------------------------------------- | ----------------------------- | ------------------------- |
52
- | `grilling`, `todo-validating`, `grilling-answer`, `building` | `<%= it.vars.todoFile %>` | `qa` |
53
- | `reviewing`, `review-validating`, `await-review`, `review-deciding` | `<%= it.vars.reviewFile %>` | `review` |
54
- | `fixing`, `escalate` | `<%= it.vars.feedbackFile %>` | — (plain text, no format) |
55
- | `idle`, `checking` | — | — |
50
+ | States | `file:` | `mode:` |
51
+ | ---------------------------------------------- | ----------------------------- | ------------------------- |
52
+ | `grilling`, `grilling-answer`, `building` | `<%= it.vars.todoFile %>` | `qa` |
53
+ | `reviewing`, `await-review`, `review-deciding` | `<%= it.vars.reviewFile %>` | `review` |
54
+ | `fixing`, `escalate` | `<%= it.vars.feedbackFile %>` | — (plain text, no format) |
55
+ | `idle`, `checking` | — | — |
56
56
 
57
57
  Prompts and scripts that already name these files switch to the vars
58
58
  (`<%~ it.vars.todoFile %>` etc.) so each filename has ONE source of truth in
@@ -109,4 +109,5 @@ The advanced example (`docs/examples/advanced-workflow.md`) gains the same
109
109
  - Eta-rendering of `on` pattern keys (the rename-switch story) — future work,
110
110
  noted in §2.
111
111
  - New LSP modes beyond `qa`/`review` — the vocabulary can grow with new formats;
112
- adding one means: parser (spec), validator recipe, LSP dispatch arm, docs.
112
+ adding one means: parser (spec), a `gtd validate` dispatch arm for the mode,
113
+ LSP dispatch arm, docs.
@@ -51,7 +51,7 @@ Each of these passes today's validation:
51
51
  `retry: {max: 3, otherwise: escalate}`; delete that key and the machine
52
52
  validates clean but can ping-pong forever with no human in the loop.
53
53
  5. **Shadowed `on` row** — `on` is first-match-wins, so
54
- `{"* **": a, "A .gtd/FORMAT.md": b}` never routes to `b`: the catch-all
54
+ `{"* **": a, "A .gtd/FEEDBACK.md": b}` never routes to `b`: the catch-all
55
55
  subsumes the specific row. Dead rows are silent today.
56
56
  6. **`retry.otherwise` cycle / self-reference** — `applyRetry` terminates at
57
57
  runtime via its `visited` guard, but the guard's "accept the repeated target
@@ -400,7 +400,7 @@ the definition) — they fit `PatternMachine.step`'s purity discipline.
400
400
  - **Self-explaining refusals.** A no-match refusal already lists the state's
401
401
  declared patterns; for a user debugging their own machine, also list the
402
402
  pending changes that failed to match (status + path, first N). "Your tree has
403
- `M src/x.ts` but this state only matches `A .gtd/FORMAT.md`" turns the most
403
+ `M src/x.ts` but this state only matches `A .gtd/FEEDBACK.md`" turns the most
404
404
  common custom-workflow dead end from a mystery into a diff-vs-pattern
405
405
  diagnosis the user can act on immediately.
406
406
 
@@ -1,5 +1,16 @@
1
1
  # Plan: steering-file validation loops in the default workflow
2
2
 
3
+ > SUPERSEDED (§2–§4): the in-machine validation-loop states this plan added —
4
+ > `todo-validating` and `review-validating` — and their `.gtd/FORMAT.md`
5
+ > steering file have all been DELETED. Deterministic format-checking now lives
6
+ > in the `gtd validate` command (which runs the same pure `parseOpenQuestions` /
7
+ > `parseReviewDoc` parsers the state's `mode:` selects) plus producing-agent
8
+ > self-validation, rather than in bash-port `check`/`script` states. The default
9
+ > workflow is now 10 states, not 12. See
10
+ > [docs/design/steering-file-validation-command.md](steering-file-validation-command.md)
11
+ > and [STATES.md §12](../STATES.md). §1 (the file formats) and §5 (the LSP
12
+ > resurrection) below remain valid and current.
13
+
3
14
  > Status: §2–§4 (the two validation loops in the default workflow) AND §5 (the
4
15
  > LSP resurrection) both LANDED 2026-07-22. Goal: the bundled default workflow
5
16
  > completely maps the functionality the deleted v2 LSP server (`src/Lsp.ts`,