@pmelab/gtd 18.1.0 → 20.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "18.1.0",
3
+ "version": "20.0.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
package/README.md CHANGED
@@ -40,11 +40,15 @@ the read side to its agent. `read` is not a security boundary unless your driver
40
40
  enforces it at the OS level. See
41
41
  [Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#file-access).
42
42
 
43
+ > **`fastTestCommand` is required, with no fallback to `testCommand`.** Set it
44
+ > (everything but e2e) under `env:` in `.gtdrc` or as `GTD_FASTTESTCOMMAND`; gtd
45
+ > stops at the start gate, writing `.gtd/SETUP.md`, until it is.
46
+
43
47
  > **A repository's `gtd.config.ts` is code, and gtd runs it.** A custom workflow
44
48
  > is a TypeScript module, and every gtd command that looks at workflow state —
45
- > `gtd next` and `gtd lsp` included, not just `gtd land` — evaluates it. Treat
46
- > it like a Makefile or a `package.json` script: don't run gtd in a checkout you
47
- > don't trust.
49
+ > `gtd next`, `gtd lsp`, `gtd door` and `gtd doors` included, not just
50
+ > `gtd land` — evaluates it. Treat it like a Makefile or a `package.json`
51
+ > script: don't run gtd in a checkout you don't trust.
48
52
 
49
53
  ## Quick start
50
54
 
@@ -303,14 +307,14 @@ Then, in any repository:
303
307
  ```
304
308
 
305
309
  That starts a process from your requirements and drives it until it needs you.
306
- `/gtd fix` and `/gtd review [base]` take the two side doors described below.
307
- Every human rest opens Claude Code's own question dialog, with a link to
308
- `gtd ui` for the step; review there, then answer **I'm done, continue**. When
309
- the process finishes, **Yes, open the pull request** (or `/gtd ship`) squashes
310
- it into one commit and opens it. A process can change hands at any gate: **Hand
311
- off to someone else** (or `/gtd throw @dev`) opens a draft pull request assigned
312
- to them, and `/gtd catch <pr>` picks it up exactly where it waits, so whoever
313
- wrote the requirements can hand the architecture to someone else. See
310
+ `/gtd fix` and `/gtd review [base]` take the two doors described below. Every
311
+ human rest opens Claude Code's own question dialog, with a link to `gtd ui` for
312
+ the step; review there, then answer **I'm done, continue**. When the process
313
+ finishes, **Yes, open the pull request** (or `/gtd ship`) squashes it into one
314
+ commit and opens it. A process can change hands at any gate: **Hand off to
315
+ someone else** (or `/gtd throw @dev`) opens a draft pull request assigned to
316
+ them, and `/gtd catch <pr>` picks it up exactly where it waits, so whoever wrote
317
+ the requirements can hand the architecture to someone else. See
314
318
  [Inside Claude Code](https://github.com/pmelab/gtd/blob/main/docs/driver.md#inside-claude-code-the-gtd-mod).
315
319
 
316
320
  ### Then let an agent build your own
@@ -328,34 +332,34 @@ you what you want before it starts driving. You get one prompt to paste, not a
328
332
  state name to choose. The four commands: **`gtd-build`** drives beats until the
329
333
  process rests; **`gtd-edit`** opens the steering file the process is waiting on
330
334
  right now — falling back to `.gtd/TODO.md` when the resting state declares none;
331
- **`gtd-review <commitish>`** starts a review round over that commitish and
332
- drives it; **`gtd-fix`** enters the fix process and drives it. The
333
- `.gtd/TODO.md` fallback is also how you begin: on a clean repository the edit
334
- command opens the empty `.gtd/TODO.md`, and whatever you write there is the
335
- first sketch the whole process gets planned from. As a final step, if it finds
336
- an LSP-capable editor, the briefing also offers to wire up live diagnostics and
337
- review actions in it — asking first and naming the exact file, and merging
338
- rather than overwriting your editor's config. That editor integration also adds
339
- a footnote at the exact cursor position with one code action, landing the cursor
340
- in the new, empty definition ready to type, and jumps between a footnote's
341
- marker and its definition both ways — so leaving a comment for the next agent
342
- turn never means hand-typing the `[^name]` syntax yourself. In a review file, a
343
- `./path#42-70` hunk pointer is also a clickable link straight to that file and
344
- range, no go-to-definition required. A footnote can also hold a `H:`/`A:`
345
- conversation (a thread — see [configuration](docs/configuration.md)); the editor
346
- outlines threads, flags the open ones, and a `gtd: reply` code action adds your
347
- empty `- H:` entry and puts the cursor there;
348
- `gtd check <mode> <file> --open-threads` lists the ones still waiting on you,
349
- and a review question gets its answer at the review gate again, not a lap. The
350
- same conversation works as bare `// H: …` / `// A: …` line comments (`#`, `--`,
351
- `;` by language) in files the process changed; `gtd check --open-threads` alone
352
- lists the open ones (editor-only — the phone UI does not show them).
335
+ **`gtd-door <name> [args...]`** starts a process through a door (`gtd-door fix`,
336
+ `gtd-door review [base]`) and drives it. The `.gtd/TODO.md` fallback is also how
337
+ you begin: on a clean repository the edit command opens the empty
338
+ `.gtd/TODO.md`, and whatever you write there is the first sketch the whole
339
+ process gets planned from. As a final step, if it finds an LSP-capable editor,
340
+ the briefing also offers to wire up live diagnostics and review actions in it —
341
+ asking first and naming the exact file, and merging rather than overwriting your
342
+ editor's config. That editor integration also adds a footnote at the exact
343
+ cursor position with one code action, landing the cursor in the new, empty
344
+ definition ready to type, and jumps between a footnote's marker and its
345
+ definition both ways — so leaving a comment for the next agent turn never means
346
+ hand-typing the `[^name]` syntax yourself. In a review file, a `./path#42-70`
347
+ hunk pointer is also a clickable link straight to that file and range, no
348
+ go-to-definition required. A footnote can also hold a `H:`/`A:` conversation (a
349
+ thread — see [configuration](docs/configuration.md)); the editor outlines
350
+ threads, flags the open ones, and a `gtd: reply` code action adds your empty
351
+ `- H:` entry and puts the cursor there; `gtd check <mode> <file> --open-threads`
352
+ lists the ones still waiting on you, and a review question gets its answer at
353
+ the review gate again, not a lap. The same conversation works as bare `// H: …`
354
+ / `// A: …` line comments (`#`, `--`, `;` by language) in files the process
355
+ changed; `gtd check --open-threads` alone lists the open ones (editor-only — the
356
+ phone UI does not show them).
353
357
 
354
358
  ### The workflow it ships with
355
359
 
356
360
  One built-in workflow drives all of that. From where you sit, it has four
357
361
  moments — everything between them runs without you, with the judged exceptions
358
- noted in steps 2, 3, and 4 below.
362
+ noted in steps 3 and 4 below.
359
363
 
360
364
  1. **You sketch.** Change anything, or write the idea into `.gtd/TODO.md`. Rough
361
365
  is fine; it is treated as a sketch, not as work.
@@ -368,25 +372,22 @@ noted in steps 2, 3, and 4 below.
368
372
  the same gate again. Close a thread by replying with a conclusion or deleting
369
373
  it. While a thread's last entry is the agent's, moving on is refused. Leave
370
374
  the file untouched and start the loop to accept the plan as-is, unanswered
371
- questions and all. One point along this phase is judged rather than always
372
- asking you outright:
373
- - Before the how-it-should-be-built pass starts: does this plan actually need
374
- one? A confident no skips it — and the review it would have raised — going
375
- straight from your answers to a single built package, with no technical
376
- plan shown to you at all.
377
-
378
- The reference driver answers this judgment itself (`gtd judge run`, auto
379
- selection); if that fails it shows you the message and stops, same as any
380
- other question. **The `llm` provider's `p` is self-reported by the model, not
381
- a measured probability, so a confidently wrong haiku verdict can skip a
382
- question you would have asked.**
375
+ questions and all. Every plan gets the technical pass: its document has four
376
+ sections, in order — `## Interfaces`, `## Call Stacks`, `## E2E Scenarios`,
377
+ `## Unit Tests` — behind a leading `## Open Questions` when there are any.
378
+ `## E2E Scenarios` is never empty: it holds the scenarios, or, when nothing
379
+ user-visible changes, the line `No e2e change.` with a one-line reason.
383
380
 
384
381
  3. **You wait.** The work is split into packages and built one at a time, each
385
- one checked against your test suite and fixed until it passes, then reviewed
386
- against its own spec before moving on. Three points along that loop are
387
- judged rather than always asking you outright — each stops and hands you a
388
- verdict to make (`gtd judge answer`, or land with a clean tree to accept the
389
- conservative default, which never skips work;
382
+ one starting from the unit tests it declares (a build turn missing one is
383
+ refused), checked against the fast suite and fixed until it passes before
384
+ moving on. A package that rewords a frozen `.feature` step stops at a wording
385
+ gate: accept the change, or reject it and the original is restored. After the
386
+ last package a full run (e2e included) has its own fix loop before the
387
+ quality lap. Two points along the process are judged rather than always
388
+ asking you outright — each stops and hands you a verdict to make
389
+ (`gtd judge answer`, or land with a clean tree to accept the conservative
390
+ default, which never skips work;
390
391
  `gtd judge run --provider fixed --answers <path>` — or the
391
392
  `GTD_JUDGE_ANSWERS` env var, inline JSON — answers one from a file, piped
392
393
  between `gtd judge --json` and `gtd judge answer`;
@@ -403,17 +404,15 @@ noted in steps 2, 3, and 4 below.
403
404
  stdout when they cannot answer every question):
404
405
  - Every red round after the first: was the failure identical, new, or
405
406
  progress?
406
- - Before spending a review turn on a package: does the code already satisfy
407
- each of its requirements?
408
- - After a review turn raises concerns: would each one actually violate the
409
- spec if left unaddressed, or is it a nit?
407
+ - After you review: is each of your notes an edit, a question, a nit or
408
+ praise?
410
409
 
411
410
  The reference driver answers these itself (`gtd judge run`, auto selection:
412
411
  jev when `TYPESAFE_API_KEY` is set, else `llm` via `claude`, default model
413
412
  haiku, `--model <name>` overrides); if that fails it shows you the message
414
413
  and stops. **The `llm` provider's `p` is self-reported by the model, not a
415
- measured probability, so a confidently wrong verdict can clear the 0.9/0.7
416
- floors and skip a gate unattended.**
414
+ measured probability, so a confidently wrong verdict can clear the 0.7 floor
415
+ and skip a gate unattended.**
417
416
 
418
417
  Once the last package is built, the whole change goes through a qualitative
419
418
  review lap before you see anything: six lenses, one turn each, in order —
@@ -421,11 +420,10 @@ noted in steps 2, 3, and 4 below.
421
420
  `conventions`, `spec-challenge`. Each traces the change from its own angle
422
421
  and records every finding, blocking or not; one fix turn then fixes ALL of
423
422
  them once, with no re-review after the fix. A clean turn means approval only
424
- when that lens found nothing at all. The per-package review above only judges
425
- that package against its own spec; this lap is where code quality is looked
423
+ when that lens found nothing at all. This lap is where code quality is looked
426
424
  at, and every round pays for it. It never replaces step 4 — your review stays
427
- the final gate, and nothing here skips it. The `gtd --entry fix-precheck`
428
- side door (below) repairs a red baseline through this same lap.
425
+ the final gate, and nothing here skips it. The `fix` door (below) repairs a
426
+ red baseline through this same lap.
429
427
 
430
428
  A red suite that keeps failing past a few fix attempts escalates instead of
431
429
  retrying forever: an agent turn reads the failing output and writes
@@ -438,10 +436,10 @@ noted in steps 2, 3, and 4 below.
438
436
 
439
437
  4. **You review.** You get a review document listing what changed and what to
440
438
  look at. Before you see it, an automatic risk-fix pass
441
- (`build.review.fix-risks`) runs. Any risk the reviewer names (a note opening
442
- with `Risk:`) is fixed first, the suite kept green, and the review rewritten
443
- — once per review round, so a risk the rewrite still names reaches you
444
- unfixed; risk: a fix lands with no check that the risk was real. Tick the
439
+ (`build.review.fix.risks.fixing`) runs. Any risk the reviewer names (a note
440
+ opening with `Risk:`) is fixed first, the suite kept green, and the review
441
+ rewritten — once per review round, so a risk the rewrite still names reaches
442
+ you unfixed; risk: a fix lands with no check that the risk was real. Tick the
445
443
  boxes to approve, or write what is wrong. Approving ends the process;
446
444
  feedback is judged note by note, each as `edit`, `question`, `nit` or
447
445
  `praise`. An `edit` sends the process back to step 2 for a fresh plan — it
@@ -480,15 +478,17 @@ the agent may overturn the answer. See
480
478
  [Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md)
481
479
  for its `ui:` settings.
482
480
 
483
- Two side doors skip step 1. `gtd --entry fix-precheck` repairs a red baseline as
484
- its own reviewed commit instead of starting a process. And
481
+ The bundled workflows are named `feature` (the ordinary start above), `fix` and
482
+ `review`. Two doors start the other two and skip step 1. `gtd door fix` repairs
483
+ a red baseline as its own reviewed commit instead of starting a process. And
485
484
 
486
485
  ```bash
487
- gtd --entry review-gate.check --var reviewBase=<commitish>
486
+ gtd door review <commitish>
488
487
  ```
489
488
 
490
489
  starts a pure review of everything from `<commitish>` to HEAD — straight to step
491
- 4, no planning and no building.
490
+ 4, no planning and no building. `gtd doors` lists every door; a `gtd.config.ts`
491
+ can add its own. `gtd --workflow <name>` starts any workflow by name.
492
492
 
493
493
  The workflow itself is a plain async TypeScript function: a `gtd.config.ts` at
494
494
  the repository root replaces it, and the pieces the bundled one is built from
@@ -0,0 +1,34 @@
1
+ // Doors are the workflow's named shortcuts into a process (`gtd doors`). gtd
2
+ // prints the script that starts one; running it is the driver's job, as with
3
+ // every git write gtd plans. The mod hardcodes no door: it asks gtd.
4
+
5
+ import type { ShipIo, Shipped } from "./ship"
6
+
7
+ export type Door = {
8
+ name: string
9
+ workflow: string
10
+ args: { name: string; optional: boolean }[]
11
+ }
12
+
13
+ const fail = (text: string): Shipped => ({ ok: false, text })
14
+
15
+ // Every door the repository offers; none when gtd cannot say.
16
+ export async function doors(io: ShipIo): Promise<Door[]> {
17
+ const listed = await io.run(["gtd", "doors", "--json"])
18
+ if (listed.code !== 0) return []
19
+ try {
20
+ return JSON.parse(listed.out) as Door[]
21
+ } catch {
22
+ return []
23
+ }
24
+ }
25
+
26
+ export async function enter(io: ShipIo, name: string, args: string[]): Promise<Shipped> {
27
+ const argv = ["gtd", "door", name, ...args]
28
+ const planned = await io.run(argv)
29
+ if (planned.code !== 0)
30
+ return fail(planned.err.trim() || `${argv.join(" ")} exited ${planned.code}`)
31
+ const started = await io.run(["sh", "-c", planned.out])
32
+ if (started.code !== 0) return fail(`The door script failed.\n${started.err.trim()}`)
33
+ return { ok: true, text: `Started ${name}.` }
34
+ }
@@ -7,6 +7,7 @@ import type { AccessDef } from "./access"
7
7
  export type Beat = {
8
8
  kind: string
9
9
  idle?: boolean | string
10
+ initial?: boolean | string
10
11
  state?: string
11
12
  label?: string
12
13
  file?: string
@@ -6,7 +6,7 @@ import { accessDenial } from "./access"
6
6
  import type { AccessDef } from "./access"
7
7
  import { afterReload, drive, isTrue } from "./drive"
8
8
  import type { Beat, Io, Landing, Turn, TurnEnd } from "./drive"
9
- import { enter } from "./entry"
9
+ import { doors, enter } from "./doors"
10
10
  import { subagentModel } from "./models"
11
11
  import { personaSpec, resumable } from "./persona"
12
12
  import { JUDGE_SYSTEM, judgePrompt, toVerdicts } from "./judge"
@@ -257,11 +257,11 @@ async function start($: $, landTurn?: string) {
257
257
  async function begin($: $, requirements: string) {
258
258
  await findRoot($)
259
259
  const b = JSON.parse(await gtd($, ["next", "--json"])) as Beat
260
- if (b.state === "idle" && !isTrue(b.idle)) {
260
+ if (isTrue(b.initial) && !isTrue(b.idle)) {
261
261
  const paths = (b.changes ?? []).map((c) => c.path).join(", ")
262
262
  return `The working tree has uncommitted changes (${paths}); gtd would start from those. Commit, stash or revert them, or run /gtd to start from them.`
263
263
  }
264
- if (b.state !== "idle") {
264
+ if (!isTrue(b.initial)) {
265
265
  return `A gtd process is already underway at ${b.state}. Run /gtd to continue it.`
266
266
  }
267
267
  const file = b.file ?? ".gtd/TODO.md"
@@ -381,7 +381,7 @@ async function throwNow($: $, target: string | undefined) {
381
381
  state: b.state,
382
382
  label: b.label,
383
383
  content: b.content,
384
- isIdle: b.state === "idle" && isTrue(b.idle),
384
+ isIdle: isTrue(b.idle),
385
385
  }
386
386
  const thrown = await throwTo(shipIo($), rest, target, new Date().toISOString()).catch(
387
387
  (err: unknown) => ({ ok: false, text: String(err) }),
@@ -401,9 +401,9 @@ async function throwNow($: $, target: string | undefined) {
401
401
  }
402
402
  }
403
403
 
404
- async function enterNow($: $, door: "fix" | "review", base: string | undefined) {
404
+ async function enterNow($: $, door: string, args: string[]) {
405
405
  if ((await read($, run)).isRunning) return "Stop the loop first: /gtd stop."
406
- const entered = await enter(shipIo($), door, base)
406
+ const entered = await enter(shipIo($), door, args)
407
407
  if (entered.ok) await start($)
408
408
  return entered.text
409
409
  }
@@ -655,9 +655,6 @@ async function command($: $, arg: string): Promise<string | undefined> {
655
655
  switch (verb) {
656
656
  case "status":
657
657
  return gtd($, ["next"])
658
- case "fix":
659
- case "review":
660
- return enterNow($, verb, rest[0])
661
658
  case "throw":
662
659
  void throwNow($, rest[0])
663
660
  return undefined
@@ -671,14 +668,16 @@ async function command($: $, arg: string): Promise<string | undefined> {
671
668
  case "":
672
669
  return resume($)
673
670
  default:
674
- return begin($, arg)
671
+ return (await doors(shipIo($))).some((d) => d.name === verb)
672
+ ? enterNow($, verb, rest)
673
+ : begin($, arg)
675
674
  }
676
675
  }
677
676
 
678
677
  async function resume($: $) {
679
678
  const b = JSON.parse(await gtd($, ["next", "--json"])) as Beat
680
- if (b.state === "idle" && isTrue(b.idle)) {
681
- return "Nothing is in progress. Start a process with /gtd <requirements>, or sketch the change in .gtd/TODO.md and run /gtd."
679
+ if (isTrue(b.idle)) {
680
+ return `Nothing is in progress. Start a process with /gtd <requirements>, or sketch the change in ${b.file ?? ".gtd/TODO.md"} and run /gtd.`
682
681
  }
683
682
  await start($)
684
683
  return undefined
@@ -726,7 +725,7 @@ export const register: Register = (on) => {
726
725
  name: "gtd",
727
726
  description: "Drive gtd until it rests on you; pass requirements to start a new process",
728
727
  argumentHint:
729
- "[requirements | fix | review [base] | stop | status | ship [-n] | throw [@user] | catch <pr|branch>]",
728
+ "[requirements | <door> [args] | stop | status | ship [-n] | throw [@user] | catch <pr|branch>]",
730
729
  immediate: true,
731
730
  })
732
731
  return next(e)
@@ -162,8 +162,9 @@ async function squash(
162
162
  }
163
163
  // `gtd summary` also describes a process still underway; shipping that
164
164
  // would publish half of it.
165
- const state = (await io.run(["gtd", "next", "--json=state"])).out.trim()
166
- if (state !== "idle") {
165
+ const initial = (await io.run(["gtd", "next", "--json=initial"])).out.trim()
166
+ if (initial !== "true") {
167
+ const state = (await io.run(["gtd", "next", "--json=state"])).out.trim()
167
168
  return fail(`The gtd process is still underway (at ${state}). Finish it, then ship.`)
168
169
  }
169
170
  const range = await processRange(git, summary.out)