@pmelab/gtd 7.0.0 → 7.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.
package/docs/cli.md CHANGED
@@ -32,14 +32,15 @@ Commands:
32
32
  validate Format and validate the steering file the resolved rest
33
33
  declares, with its mode's commands (its file:/mode:);
34
34
  exits non-zero with the findings when it is invalid
35
- mermaid Print the active workflow's shape as Mermaid
36
- stateDiagram-v2 source (no mutation)
37
35
  lsp Start the LSP server for .gtd/ steering files (stdio)
36
+ visualize Serve an interactive diagram of the active workflow locally
38
37
  version Print version and exit
39
38
  help Print this help and exit
40
39
 
41
40
  Options:
42
41
  --json Output structured JSON instead of plain text
42
+ --port=<n> (gtd visualize only) port to serve on (default: a free port)
43
+ --no-open (gtd visualize only) do not open the browser
43
44
  --cost=<n> (gtd step only) record the invocation's token cost
44
45
  --model=<name> (gtd step only, with --cost) tag that cost's model
45
46
  --version, -v Print version and exit
@@ -394,46 +395,6 @@ agent or human:
394
395
 
395
396
  `gtd validate` takes no arguments.
396
397
 
397
- ## `gtd mermaid`
398
-
399
- Pure emitter of the active workflow's **shape** — not the resolved rest — as
400
- Mermaid [`stateDiagram-v2`](https://mermaid.js.org/syntax/stateDiagram.html)
401
- source (see `src/Mermaid.ts`): one node per declared state, the `[*] -->`
402
- initial-state marker, one edge per declared `on` row labeled with its raw
403
- pattern string (same declaration order the engine itself evaluates), a `--> [*]`
404
- edge for every commit state (final, no outgoing edges — see
405
- [STATES.md §8](../STATES.md#8-the-squash-lifecycle)), and one `note right of`
406
- per rest naming its actor, content kind, and retry cap (e.g.
407
- `agent · prompt · retry 3→escalate`). No git, no HEAD resolution, no template
408
- rendering — purely a function of the compiled `WorkflowDefinition`, so its
409
- output is identical regardless of the current process/branch state.
410
-
411
- ```
412
- $ gtd mermaid
413
- stateDiagram-v2
414
- state "idle" as idle
415
- state "planning" as planning
416
- ...
417
- [*] --> idle
418
- idle --> planning : * **
419
- ...
420
- note right of idle : human · message
421
- ...
422
- ```
423
-
424
- Pipe it straight into a `.md`/`.mmd` file, a GitHub issue/PR description, or any
425
- Mermaid-aware renderer (GitHub, GitLab, VS Code, Obsidian, the
426
- [Mermaid Live Editor](https://mermaid.live)) to get a diagram of a custom
427
- `.gtdrc` `workflow:` with no hand-maintained docs required.
428
-
429
- State names are aliased to Mermaid-safe identifiers (non-word characters fold to
430
- `_`; a digit-led name gets an `s_` prefix) via a `state "<name>" as <alias>`
431
- declaration up front, so a hyphenated name like `plan-review` still displays
432
- with its exact declared spelling. Rejects `--json` (exit 1,
433
- `gtd mermaid does not accept --json`) — there is no structured shape to emit
434
- beyond the Mermaid source itself — and takes no arguments (extra positional args
435
- are rejected).
436
-
437
398
  ## `gtd lsp`
438
399
 
439
400
  Starts an LSP server over stdio for `.gtd/` steering files — document symbols
@@ -469,6 +430,39 @@ extra positional arguments — it's a long-running server, not a state command.
469
430
  Runs until the client disconnects (the LSP `exit` notification), then exits
470
431
  cleanly.
471
432
 
433
+ ## `gtd visualize [--port=<n>] [--no-open] [--json]`
434
+
435
+ Serve an interactive diagram of the ACTIVE workflow on a local web server: the
436
+ main flow as a graph (one box per sub-machine), and a click-through inspector
437
+ with each state's actor, content kind, model/memory, steering file+mode, retry,
438
+ flags, and outgoing/incoming edges. This is the replacement for the removed
439
+ `gtd mermaid` — a live viewer instead of a static diagram dump.
440
+
441
+ ```
442
+ $ gtd visualize
443
+ gtd visualize running at http://127.0.0.1:53017 — Ctrl-C to stop
444
+ ```
445
+
446
+ The server serves two routes: `/` (the self-contained HTML page) and
447
+ `/workflow.json` (the model the page renders). It runs until interrupted
448
+ (Ctrl-C), then closes cleanly. Options (orthogonal, `gtd visualize` only):
449
+
450
+ - `--port=<n>` (or `--port <n>`) — serve on a specific port (0–65535); the
451
+ default is a free ephemeral port, printed on start.
452
+ - `--no-open` — do not open the default browser (the URL is always printed).
453
+ - `--json` — print the workflow model to stdout and exit WITHOUT starting a
454
+ server. The model is `{ states, initial, groups, vars }`; each state carries
455
+ its `actor`/`kind`/`model`/`memory`/`file`/`mode`/`retry`/`flags`, its `on`
456
+ edges, its computed `incoming` edges, and its sub-machine `group`. `groups`
457
+ lists each sub-machine invocation and the concrete states it produced.
458
+
459
+ Dispatched before the repository-root guard and the config-reading path's review
460
+ window — it reads the active workflow (the built-in default when none is
461
+ configured) but touches no git/HEAD/review-window state. The diagram is rendered
462
+ with Mermaid loaded from a CDN, so the graph needs network access the first time
463
+ a browser loads the page; the inspector works offline regardless. Rejects
464
+ unknown options and unexpected positional arguments.
465
+
472
466
  ## Error envelope
473
467
 
474
468
  Every command, in `--json` mode, reports a failure as a machine-readable
@@ -496,8 +490,8 @@ the plain-text one.
496
490
  green run, and the check's `"C"` pattern never fires. Gitignore every path
497
491
  your scripts write before wiring gtd into a repo.
498
492
  - **Repository root invocation.** Every state subcommand (`step`/`review`/
499
- `next`/`status`/`mermaid`) must run from the git repository root — the
500
- workflow, pending changes, and process history are resolved against the
501
- process cwd. `--help`/`--version` (and the `help`/`version` subcommands),
502
- `format`, and `lsp` skip this guard entirely (and any git/`.gtdrc` dependency
503
- along with it).
493
+ `next`/`status`) must run from the git repository root — the workflow, pending
494
+ changes, and process history are resolved against the process cwd.
495
+ `--help`/`--version` (and the `help`/`version` subcommands), `lsp`, and
496
+ `visualize` skip this guard entirely (`visualize` still reads the `.gtdrc`
497
+ workflow, but needs no git state).
@@ -588,6 +588,71 @@ done:
588
588
  command), and gtd never interprets the number's unit (tokens, cents, whatever
589
589
  the driver records) or the model name — it only records, sums, and groups them.
590
590
 
591
+ ## Sub-machines (`submachines:` / `use:`)
592
+
593
+ Two OPTIONAL top-level keys let you factor repeated or complex clusters of
594
+ states into reusable **sub-machines** that are expanded into ordinary concrete
595
+ states at load time (see [STATES.md §13](../STATES.md) and
596
+ `src/Submachines.ts`). They add NO runtime behavior — the engine only ever sees
597
+ the expanded states — so a workflow authored with them is exactly a workflow
598
+ written flat.
599
+
600
+ ```yaml
601
+ workflow:
602
+ submachines:
603
+ gate: # authored once
604
+ params: [onGreen] # names referenced as `$onGreen` below
605
+ states:
606
+ check:
607
+ actor: check
608
+ script: <%~ it.vars.testCommand %>
609
+ on:
610
+ "A .gtd/FEEDBACK.md": blocked # a local state -> renamed by `as:`
611
+ "C": "$onGreen" # a param -> bound by `with:`
612
+ blocked:
613
+ actor: human
614
+ file: <%= it.vars.feedbackFile %>
615
+ message: "fix the baseline"
616
+ on: { "* **": check }
617
+ use:
618
+ - submachine: gate
619
+ as: { check: start-check, blocked: start-blocked } # local -> concrete (omit for identity)
620
+ with: { onGreen: planning } # bind each `$param`
621
+ set: { check: { reviewEntry: true } } # optional extra fields on a renamed state
622
+ states:
623
+ idle:
624
+ { actor: human, initial: true, message: "…", on: { "* **": start-check } }
625
+ planning: { actor: agent, prompt: "…", on: { "* **": start-check } }
626
+ ```
627
+
628
+ Rules:
629
+
630
+ - **`submachines: { <name>: { params, states } }`** — `states` are ordinary
631
+ state definitions, except an `on`/`retry.otherwise` target or any whole field
632
+ value may be a `$param` placeholder.
633
+ - **`use:`** is an ordered list. Each entry names a `submachine:` and may set:
634
+ - **`as:`** — a `local -> concreteName` rename map. Omit it (or a given local)
635
+ for identity. Alias to the names a flat workflow would use to keep history
636
+ and `git log` unchanged.
637
+ - **`with:`** — bindings for the sub-machine's `$param`s.
638
+ - **`set:`** — extra fields merged onto a renamed state (e.g. `reviewEntry`).
639
+ - **Substitution is WHOLE-VALUE only.** `$name` resolves only when it is the
640
+ entire value of a field or target — never a substring — so bash `$x`/`${x}`
641
+ and Eta `<%= it.vars.x %>` inside content are untouched. Pass per-instance
642
+ text (a whole message/prompt/`describe`) as a whole `$name` binding.
643
+ - A target naming one of the sub-machine's own locals is rewritten via `as:`;
644
+ any other target passes through verbatim (a concrete top-level state). `on`
645
+ PATTERN keys are never substituted.
646
+ - Expansion strips `submachines:`/`use:`; the merged `states` are then compiled
647
+ and `validateDefinition`'d normally. Unknown sub-machine, unbound `$param`,
648
+ and generated-name collisions are load-time errors, aggregated with the rest.
649
+
650
+ `gtd init` scaffolds the EXPANDED flat config, so a fresh project never sees
651
+ `submachines:`/`use:` unless it adds them itself. The bundled template is
652
+ authored with them (dedup: `assertGreen`/`makeGreen`/`qaLoop`; comprehension:
653
+ `planLoop`/`humanReview`/`specGate`/`packageLoop`) and its golden test pins that
654
+ expansion is byte-identical to the flat form.
655
+
591
656
  ## Variables
592
657
 
593
658
  Every template — `script`/`prompt`/`message`/`commit`, and `model`/`memory`/
package/docs/upgrading.md CHANGED
@@ -80,8 +80,14 @@ upgrading.
80
80
  existing review/feedback machinery unmodified — see
81
81
  [STATES.md §11](../STATES.md#11-the-review-checkout-window) and
82
82
  [CLI reference](cli.md#gtd-review-commitish---json). The current command
83
- surface is `init` / `step` / `next` / `status` / `validate` / `mermaid` /
84
- `review` / `lsp`.
83
+ surface is `init` / `step` / `next` / `status` / `validate` / `review` /
84
+ `lsp`.
85
+
86
+ - **`gtd mermaid` → `gtd visualize`.** The static Mermaid emitter has been
87
+ replaced by `gtd visualize`, which serves an interactive diagram of the active
88
+ workflow (main flow, sub-machines, per-state details) on a local web server —
89
+ see [CLI reference](cli.md#gtd-visualize---portn---no-open---json). Both are
90
+ read-only views of the active workflow; nothing in the machine changes.
85
91
 
86
92
  - **`gtd format <file>`, and the bundled prettier with it.** gtd no longer
87
93
  formats anything on its own: a steering-file mode declares its own `format:`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
package/schema.json CHANGED
@@ -36,6 +36,182 @@
36
36
  }
37
37
  }
38
38
  },
39
+ "submachines": {
40
+ "type": "object",
41
+ "description": "Optional reusable sub-machines, invoked by `use:` and expanded into concrete states at load time (see src/Submachines.ts).",
42
+ "additionalProperties": {
43
+ "type": "object",
44
+ "description": "A reusable, parameterized cluster of states, expanded at load time by each `use:` invocation into concrete states (see src/Submachines.ts). Purely a source-authoring convenience — the engine only ever sees the expanded states.",
45
+ "additionalProperties": false,
46
+ "required": ["states"],
47
+ "properties": {
48
+ "params": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "string"
52
+ },
53
+ "description": "Parameter names. A `$name` token used as a whole field value or `on`/`retry.otherwise` target is replaced by the invocation's `with:` binding."
54
+ },
55
+ "states": {
56
+ "type": "object",
57
+ "description": "The sub-machine's local states. `on`/`retry.otherwise` targets naming another local are rewritten via `as:`; a `$name` value is bound from `with:`.",
58
+ "minProperties": 1,
59
+ "additionalProperties": {
60
+ "type": "object",
61
+ "description": "One workflow state. Declare exactly one content kind (script/prompt/message/commit). A commit state is final: no actor, no on.",
62
+ "additionalProperties": false,
63
+ "properties": {
64
+ "actor": {
65
+ "type": "string",
66
+ "description": "Who acts at this state. Required on every non-commit state; forbidden on a commit state."
67
+ },
68
+ "script": {
69
+ "type": "string",
70
+ "description": "Content kind: a shell script (Eta template). The loop driver executes it verbatim via bash, then steps this state's actor."
71
+ },
72
+ "prompt": {
73
+ "type": "string",
74
+ "description": "Content kind: an agent prompt (Eta template), emitted by `gtd next`."
75
+ },
76
+ "message": {
77
+ "type": "string",
78
+ "description": "Content kind: a human-facing message (Eta template), emitted by `gtd next`."
79
+ },
80
+ "commit": {
81
+ "type": "string",
82
+ "description": "Content kind: entering this state ends the process by squashing it into one commit with this message (Eta template). Final — no actor, no on."
83
+ },
84
+ "on": {
85
+ "type": "object",
86
+ "description": "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise). A value is either the target state name (string) or a { to, describe } object whose describe is a human-readable sentence templates can surface as it.edges (e.g. in a human gate's message).",
87
+ "additionalProperties": {
88
+ "oneOf": [
89
+ {
90
+ "type": "string",
91
+ "description": "The target state name."
92
+ },
93
+ {
94
+ "type": "object",
95
+ "additionalProperties": false,
96
+ "required": ["to"],
97
+ "properties": {
98
+ "to": {
99
+ "type": "string",
100
+ "description": "The target state name."
101
+ },
102
+ "describe": {
103
+ "type": "string",
104
+ "description": "Human-readable sentence describing where this change routes; surfaced verbatim (never Eta-rendered) to templates as it.edges[].describe."
105
+ }
106
+ }
107
+ }
108
+ ]
109
+ }
110
+ },
111
+ "initial": {
112
+ "type": "boolean",
113
+ "description": "Exactly one state in the workflow must declare initial: true (and it must not be a commit state)."
114
+ },
115
+ "retry": {
116
+ "type": "object",
117
+ "description": "Redirect transitions INTO this state once it has been entered `max` times in the current process.",
118
+ "additionalProperties": false,
119
+ "required": ["max", "otherwise"],
120
+ "properties": {
121
+ "max": {
122
+ "type": "integer",
123
+ "minimum": 0,
124
+ "description": "Entries allowed this process before redirecting."
125
+ },
126
+ "otherwise": {
127
+ "type": "string",
128
+ "description": "Defined state to redirect to once over the cap."
129
+ }
130
+ }
131
+ },
132
+ "model": {
133
+ "type": "string",
134
+ "description": "Opaque harness hint passed through `gtd next --json`/`gtd status --json` (e.g. \"smart\"). Never interpreted by gtd. Forbidden on a commit state."
135
+ },
136
+ "memory": {
137
+ "type": "string",
138
+ "description": "Opaque memory-scope label passed through `gtd next --json`/`gtd status --json` (e.g. \"plan\"). A memory-aware driver retains an agent's memory across consecutive agent turns sharing this label and starts fresh when it changes. Never interpreted by gtd. Forbidden on a commit state."
139
+ },
140
+ "file": {
141
+ "type": "string",
142
+ "description": "The state's steering file: an Eta template naming the file a human/editor should look at while the machine rests here. Forbidden on a commit state."
143
+ },
144
+ "mode": {
145
+ "type": "string",
146
+ "description": "The steering file's format: the name of a built-in mode (qa/review, validated in-process by gtd) or of a `modes:` entry. gtd formats and validates the file with that mode before capturing a turn out of this state, and the LSP dispatches live diagnostics on the built-in names. Requires a sibling `file:`. Forbidden on a commit state."
147
+ },
148
+ "reviewWindow": {
149
+ "type": "boolean",
150
+ "description": "When true, gtd opens a review checkout window while the machine rests here — HEAD/index are rewound to the review base so the whole base..HEAD diff surfaces as uncommitted changes in the editor. Forbidden on a commit state."
151
+ },
152
+ "reviewBase": {
153
+ "type": "boolean",
154
+ "description": "Marks the state whose most-recent in-process commit anchors the review window's diff base; absent any, the base is the process start. Forbidden on a commit state."
155
+ },
156
+ "reviewEntry": {
157
+ "type": "boolean",
158
+ "description": "Marks the (at most one) state `gtd review <commitish>` enters to start a brand new process reviewing <commitish>..HEAD. Forbidden on a commit state and on the initial state."
159
+ },
160
+ "fixEntry": {
161
+ "type": "boolean",
162
+ "description": "Marks the (at most one) state `gtd fix` enters to start a brand new process that goes straight into repairing the current failing tests. Forbidden on a commit state and on the initial state."
163
+ },
164
+ "requireProgress": {
165
+ "type": "boolean",
166
+ "description": "When true, a step at this state is refused if its only pending change is deleting the state's own `file:` — a work-free turn that discards its input without addressing it. A `NOTHING ACTIONABLE` sentinel file is exempt (a legitimately non-actionable round makes no code change). Requires a `file:`. Forbidden on a commit state."
167
+ },
168
+ "answerGate": {
169
+ "type": "boolean",
170
+ "description": "When true, a step at this state is refused unless every open question in its qa-mode `file:` is answered — exactly one checkbox ticked per question. Requires a `file:` and `mode: qa`. Forbidden on a commit state."
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ }
177
+ },
178
+ "use": {
179
+ "type": "array",
180
+ "description": "Sub-machine invocations. Each clones a `submachines:` entry into concrete states: `as:` renames its locals (identity when omitted), `with:` binds its `$param`s, `set:` adds extra per-instance state fields.",
181
+ "items": {
182
+ "type": "object",
183
+ "additionalProperties": false,
184
+ "required": ["submachine"],
185
+ "properties": {
186
+ "submachine": {
187
+ "type": "string",
188
+ "description": "Name of a declared sub-machine."
189
+ },
190
+ "name": {
191
+ "type": "string",
192
+ "description": "Optional label for this instance (inert to expansion; used by tooling like `gtd visualize` to name the group). Defaults to the sub-machine name."
193
+ },
194
+ "as": {
195
+ "type": "object",
196
+ "description": "Rename map: local state name -> concrete state name (identity when omitted).",
197
+ "additionalProperties": {
198
+ "type": "string"
199
+ }
200
+ },
201
+ "with": {
202
+ "type": "object",
203
+ "description": "Bindings for the sub-machine's `$param`s."
204
+ },
205
+ "set": {
206
+ "type": "object",
207
+ "description": "Extra fields merged onto a renamed local state, keyed by local name.",
208
+ "additionalProperties": {
209
+ "type": "object"
210
+ }
211
+ }
212
+ }
213
+ }
214
+ },
39
215
  "states": {
40
216
  "type": "object",
41
217
  "description": "The workflow's named states. At least one; exactly one with initial: true.",
@@ -148,6 +324,10 @@
148
324
  "requireProgress": {
149
325
  "type": "boolean",
150
326
  "description": "When true, a step at this state is refused if its only pending change is deleting the state's own `file:` — a work-free turn that discards its input without addressing it. A `NOTHING ACTIONABLE` sentinel file is exempt (a legitimately non-actionable round makes no code change). Requires a `file:`. Forbidden on a commit state."
327
+ },
328
+ "answerGate": {
329
+ "type": "boolean",
330
+ "description": "When true, a step at this state is refused unless every open question in its qa-mode `file:` is answered — exactly one checkbox ticked per question. Requires a `file:` and `mode: qa`. Forbidden on a commit state."
151
331
  }
152
332
  }
153
333
  }