@pmelab/gtd 19.0.0 → 20.0.1
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/.claude-plugin/plugin.json +1 -1
- package/README.md +41 -39
- package/claude/hooks/access.ts +11 -4
- package/claude/hooks/doors.ts +34 -0
- package/claude/hooks/drive.ts +1 -0
- package/claude/hooks/register.tsx +12 -13
- package/claude/hooks/ship.ts +3 -2
- package/dist/gtd.bundle.mjs +516 -268
- package/package.json +2 -2
- package/skills/authoring/SKILL.md +39 -27
- package/src/flows/runtime.ts +22 -17
- package/src/workflows/access.test.ts +3 -3
- package/src/workflows/{unified.ts → bundled.ts} +30 -27
- package/src/workflows/index.ts +1 -1
- package/src/workflows/review.test.ts +3 -3
- package/src/workflows/review.ts +5 -5
- package/src/workflows/skills.test.ts +2 -2
- package/src/workflows/steps.test.ts +2 -2
- package/src/workflows/steps.ts +1 -1
- package/src/workflows/text.ts +1 -1
- package/claude/hooks/entry.ts +0 -34
package/README.md
CHANGED
|
@@ -46,9 +46,9 @@ enforces it at the OS level. See
|
|
|
46
46
|
|
|
47
47
|
> **A repository's `gtd.config.ts` is code, and gtd runs it.** A custom workflow
|
|
48
48
|
> is a TypeScript module, and every gtd command that looks at workflow state —
|
|
49
|
-
> `gtd next`
|
|
50
|
-
> it like a Makefile or a `package.json`
|
|
51
|
-
> 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.
|
|
52
52
|
|
|
53
53
|
## Quick start
|
|
54
54
|
|
|
@@ -307,14 +307,14 @@ Then, in any repository:
|
|
|
307
307
|
```
|
|
308
308
|
|
|
309
309
|
That starts a process from your requirements and drives it until it needs you.
|
|
310
|
-
`/gtd fix` and `/gtd review [base]` take the two
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
318
318
|
[Inside Claude Code](https://github.com/pmelab/gtd/blob/main/docs/driver.md#inside-claude-code-the-gtd-mod).
|
|
319
319
|
|
|
320
320
|
### Then let an agent build your own
|
|
@@ -332,28 +332,28 @@ you what you want before it starts driving. You get one prompt to paste, not a
|
|
|
332
332
|
state name to choose. The four commands: **`gtd-build`** drives beats until the
|
|
333
333
|
process rests; **`gtd-edit`** opens the steering file the process is waiting on
|
|
334
334
|
right now — falling back to `.gtd/TODO.md` when the resting state declares none;
|
|
335
|
-
**`gtd-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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).
|
|
357
357
|
|
|
358
358
|
### The workflow it ships with
|
|
359
359
|
|
|
@@ -422,8 +422,8 @@ noted in steps 3 and 4 below.
|
|
|
422
422
|
them once, with no re-review after the fix. A clean turn means approval only
|
|
423
423
|
when that lens found nothing at all. This lap is where code quality is looked
|
|
424
424
|
at, and every round pays for it. It never replaces step 4 — your review stays
|
|
425
|
-
the final gate, and nothing here skips it. The `
|
|
426
|
-
|
|
425
|
+
the final gate, and nothing here skips it. The `fix` door (below) repairs a
|
|
426
|
+
red baseline through this same lap.
|
|
427
427
|
|
|
428
428
|
A red suite that keeps failing past a few fix attempts escalates instead of
|
|
429
429
|
retrying forever: an agent turn reads the failing output and writes
|
|
@@ -478,15 +478,17 @@ the agent may overturn the answer. See
|
|
|
478
478
|
[Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md)
|
|
479
479
|
for its `ui:` settings.
|
|
480
480
|
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
483
484
|
|
|
484
485
|
```bash
|
|
485
|
-
gtd
|
|
486
|
+
gtd door review <commitish>
|
|
486
487
|
```
|
|
487
488
|
|
|
488
489
|
starts a pure review of everything from `<commitish>` to HEAD — straight to step
|
|
489
|
-
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.
|
|
490
492
|
|
|
491
493
|
The workflow itself is a plain async TypeScript function: a `gtd.config.ts` at
|
|
492
494
|
the repository root replaces it, and the pieces the bundled one is built from
|
package/claude/hooks/access.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { isAbsolute, relative, resolve } from "node:path"
|
|
2
|
-
|
|
3
1
|
import { globMatches } from "../../src/replay/Glob.js"
|
|
4
2
|
|
|
5
3
|
export type AccessDef = { read: string[] | null; write: string[] | null }
|
|
@@ -12,10 +10,19 @@ const WRITE: Record<string, string> = {
|
|
|
12
10
|
NotebookEdit: "notebook_path",
|
|
13
11
|
}
|
|
14
12
|
|
|
13
|
+
// Hand-rolled because a hooks module may not import `node:path`.
|
|
14
|
+
const segments = (p: string): string[] =>
|
|
15
|
+
p.split("/").reduce<string[]>((out, s) => {
|
|
16
|
+
if (s === "..") out.pop()
|
|
17
|
+
else if (s && s !== ".") out.push(s)
|
|
18
|
+
return out
|
|
19
|
+
}, [])
|
|
20
|
+
|
|
15
21
|
// Repo-relative form of a path; undefined when it lies outside the repo.
|
|
16
22
|
const inRepo = (p: string, root: string): string | undefined => {
|
|
17
|
-
const
|
|
18
|
-
|
|
23
|
+
const base = segments(root)
|
|
24
|
+
const target = segments(p.startsWith("/") ? p : `${root}/${p}`)
|
|
25
|
+
return base.every((s, i) => target[i] === s) ? target.slice(base.length).join("/") : undefined
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
const deny = (verb: string, path: string, globs: string[]) =>
|
|
@@ -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
|
+
}
|
package/claude/hooks/drive.ts
CHANGED
|
@@ -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 "./
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
|
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 (
|
|
681
|
-
return
|
|
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 |
|
|
728
|
+
"[requirements | <door> [args] | stop | status | ship [-n] | throw [@user] | catch <pr|branch>]",
|
|
730
729
|
immediate: true,
|
|
731
730
|
})
|
|
732
731
|
return next(e)
|
package/claude/hooks/ship.ts
CHANGED
|
@@ -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
|
|
166
|
-
if (
|
|
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)
|