@pmelab/gtd 20.0.1 → 20.2.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/.claude-plugin/plugin.json +1 -1
- package/README.md +8 -1
- package/claude/hooks/register.tsx +27 -30
- package/claude/hooks/schema.ts +52 -0
- package/dist/gtd.bundle.mjs +45921 -45891
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -19,6 +19,10 @@ to any machine that checks it out.
|
|
|
19
19
|
npm install -g @pmelab/gtd
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
That installs `latest`, which moves once a day to the newest release. Every
|
|
23
|
+
merge to `main` publishes to `next` first: `npm install -g @pmelab/gtd@next`
|
|
24
|
+
gets each one as it lands.
|
|
25
|
+
|
|
22
26
|
Or run without installing (prefix every `gtd` below with `npx`) — see
|
|
23
27
|
[Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md)
|
|
24
28
|
for the settings most projects tune. A process setting's value is committed to
|
|
@@ -63,7 +67,10 @@ for you. Ask your agent if you have any questions.
|
|
|
63
67
|
back next week: run the driver and it resumes exactly where it stopped.
|
|
64
68
|
- **Shareable.** Push the branch, and the workflow travels with it — the state
|
|
65
69
|
lives in the commits, so another machine (or another person) picks up exactly
|
|
66
|
-
where you left off.
|
|
70
|
+
where you left off. Each commit records the history format it was written in
|
|
71
|
+
(`Gtd-Format:`), so a gtd that cannot read a history says so instead of
|
|
72
|
+
misreading it — see
|
|
73
|
+
[History format](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#history-format).
|
|
67
74
|
- **Files, not chat.** Conversation lives in files like `REQUIREMENTS.md` or
|
|
68
75
|
`ARCHITECTURE.md`. Request changes by editing them, approve by leaving the
|
|
69
76
|
tree clean — all in your own editor. There is no chat UI that could flicker.
|
|
@@ -5,13 +5,14 @@ import type { Run, Stop } from "../types"
|
|
|
5
5
|
import { accessDenial } from "./access"
|
|
6
6
|
import type { AccessDef } from "./access"
|
|
7
7
|
import { afterReload, drive, isTrue } from "./drive"
|
|
8
|
-
import type {
|
|
8
|
+
import type { Io, Turn, TurnEnd } from "./drive"
|
|
9
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"
|
|
13
13
|
import type { Judgment } from "./judge"
|
|
14
14
|
import { catchFrom, throwTo } from "./handoff"
|
|
15
|
+
import { documents } from "./schema"
|
|
15
16
|
import { READ_ONLY_GIT, ship } from "./ship"
|
|
16
17
|
import type { ShipIo } from "./ship"
|
|
17
18
|
import {
|
|
@@ -124,16 +125,13 @@ async function settle($: $, end: TurnEnd) {
|
|
|
124
125
|
return end
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
const docs = ($: $) => documents((args, stdin) => gtd($, args, stdin))
|
|
129
|
+
|
|
127
130
|
function io($: $): Io {
|
|
128
131
|
return {
|
|
129
|
-
next:
|
|
132
|
+
next: docs($).next,
|
|
130
133
|
plain: () => gtd($, ["next"]),
|
|
131
|
-
land:
|
|
132
|
-
JSON.parse(
|
|
133
|
-
verdict === undefined
|
|
134
|
-
? await gtd($, ["land", "--json"])
|
|
135
|
-
: await gtd($, ["judge", "answer", "--json"], verdict),
|
|
136
|
-
) as Landing,
|
|
134
|
+
land: docs($).land,
|
|
137
135
|
sh: async (script, log) => {
|
|
138
136
|
// Streamed, not `$.process.run`: a check beat can outlast run's 10-minute cap.
|
|
139
137
|
const argv = log
|
|
@@ -153,22 +151,22 @@ function io($: $): Io {
|
|
|
153
151
|
for await (const chunk of child) out += chunk.text
|
|
154
152
|
return { ok: (await child.result).code === 0, out: out.trim() }
|
|
155
153
|
},
|
|
156
|
-
judge:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
154
|
+
judge: () =>
|
|
155
|
+
docs($).judge(async (doc, j) => {
|
|
156
|
+
const key = (await $.env.get("TYPESAFE_API_KEY")) ?? (await $.env.get("TYPESAFE_AI_KEY"))
|
|
157
|
+
if (!key) return judgeInSession($, j)
|
|
158
|
+
try {
|
|
159
|
+
const r = await $.process.run(["gtd", "judge", "run", "--provider", "jev"], {
|
|
160
|
+
cwd: root,
|
|
161
|
+
stdin: doc,
|
|
162
|
+
env: { TYPESAFE_API_KEY: key },
|
|
163
|
+
timeoutMs: TEN_MINUTES,
|
|
164
|
+
})
|
|
165
|
+
return r.exitCode === 0 && r.stdout.trim() ? r.stdout : undefined
|
|
166
|
+
} catch {
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
169
|
+
}),
|
|
172
170
|
turn: async (t) => {
|
|
173
171
|
const entry = (await read($, scopes))[t.memory]
|
|
174
172
|
const name = (await personaSpec(t)).name
|
|
@@ -256,7 +254,7 @@ async function start($: $, landTurn?: string) {
|
|
|
256
254
|
// idle rest names; the opening beat captures them like a hand-edit.
|
|
257
255
|
async function begin($: $, requirements: string) {
|
|
258
256
|
await findRoot($)
|
|
259
|
-
const b =
|
|
257
|
+
const b = await docs($).next()
|
|
260
258
|
if (isTrue(b.initial) && !isTrue(b.idle)) {
|
|
261
259
|
const paths = (b.changes ?? []).map((c) => c.path).join(", ")
|
|
262
260
|
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.`
|
|
@@ -375,7 +373,7 @@ async function throwNow($: $, target: string | undefined) {
|
|
|
375
373
|
if ((await read($, run)).isRunning) return $.ui.log("Stop the loop first: /gtd stop.")
|
|
376
374
|
closeUi()
|
|
377
375
|
moveOn($)
|
|
378
|
-
const b =
|
|
376
|
+
const b = await docs($).next()
|
|
379
377
|
$.ui.status("handing off…")
|
|
380
378
|
const rest = {
|
|
381
379
|
state: b.state,
|
|
@@ -415,7 +413,7 @@ async function catchNow($: $, ref: string) {
|
|
|
415
413
|
const caught = await catchFrom(shipIo($), ref)
|
|
416
414
|
if (!caught.ok) return caught.text
|
|
417
415
|
await findRoot($)
|
|
418
|
-
const b =
|
|
416
|
+
const b = await docs($).next()
|
|
419
417
|
if (b.kind === "message") {
|
|
420
418
|
const stop: Stop = { kind: "gate", text: b.content ?? "", state: b.state, label: b.label }
|
|
421
419
|
if (b.judge) stop.isJudge = true
|
|
@@ -516,9 +514,8 @@ async function writer($: $, prompt: string) {
|
|
|
516
514
|
// Without a TypeSafe key a judge gate is put to a small model, as gtd's own
|
|
517
515
|
// llm provider does, but as a subagent of this session. Any doubt leaves the
|
|
518
516
|
// gate to the person, which routes the workflow its cautious way.
|
|
519
|
-
async function judgeInSession($:
|
|
517
|
+
async function judgeInSession($: $, j: Judgment) {
|
|
520
518
|
try {
|
|
521
|
-
const j = JSON.parse(await gtd($, ["judge", "--json"])) as Judgment
|
|
522
519
|
const model = (await $.env.get("GTD_JUDGE_MODEL")) ?? "haiku"
|
|
523
520
|
const reply = await delegate(
|
|
524
521
|
$,
|
|
@@ -675,7 +672,7 @@ async function command($: $, arg: string): Promise<string | undefined> {
|
|
|
675
672
|
}
|
|
676
673
|
|
|
677
674
|
async function resume($: $) {
|
|
678
|
-
const b =
|
|
675
|
+
const b = await docs($).next()
|
|
679
676
|
if (isTrue(b.idle)) {
|
|
680
677
|
return `Nothing is in progress. Start a process with /gtd <requirements>, or sketch the change in ${b.file ?? ".gtd/TODO.md"} and run /gtd.`
|
|
681
678
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { WIRE_SCHEMA } from "../../src/wire/constants.js"
|
|
2
|
+
import type { Beat, Landing } from "./drive"
|
|
3
|
+
import type { Judgment } from "./judge"
|
|
4
|
+
|
|
5
|
+
// The mod can meet a gtd installed apart from it (a checkout without a build
|
|
6
|
+
// falls back to the gtd on PATH), so each document's `schema`, never the
|
|
7
|
+
// package version, decides whether the mod can read it.
|
|
8
|
+
export function readDocument<T>(command: string, text: string): T {
|
|
9
|
+
return checked(command, JSON.parse(text))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function checked<T>(command: string, parsed: unknown): T {
|
|
13
|
+
const found = (parsed as { schema?: unknown } | null)?.schema
|
|
14
|
+
if (found !== WIRE_SCHEMA) throw new Error(unsupportedSchema(command, found))
|
|
15
|
+
return parsed as T
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function unsupportedSchema(command: string, found: unknown) {
|
|
19
|
+
const printed = found === undefined ? "no schema" : `schema ${JSON.stringify(found)}`
|
|
20
|
+
const upgrade =
|
|
21
|
+
typeof found === "number" && found > WIRE_SCHEMA
|
|
22
|
+
? "upgrade the gtd plugin to read it"
|
|
23
|
+
: "upgrade gtd to the plugin's version"
|
|
24
|
+
return `\`gtd ${command}\` printed ${printed}, but this gtd mod reads only schema ${WIRE_SCHEMA}: ${upgrade}.`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Gtd = (args: string[], stdin?: string) => Promise<string>
|
|
28
|
+
|
|
29
|
+
// Every gtd document the mod reads goes through here, so none skips the check.
|
|
30
|
+
export function documents(gtd: Gtd) {
|
|
31
|
+
return {
|
|
32
|
+
next: async () => readDocument<Beat>("next --json", await gtd(["next", "--json"])),
|
|
33
|
+
land: async (verdict?: string) => {
|
|
34
|
+
const args = verdict === undefined ? ["land", "--json"] : ["judge", "answer", "--json"]
|
|
35
|
+
return readDocument<Landing>(args.join(" "), await gtd(args, verdict))
|
|
36
|
+
},
|
|
37
|
+
// A failed or unparseable read leaves the judge step for a person to answer.
|
|
38
|
+
// Only a schema the mod does not read throws, stopping the run before a
|
|
39
|
+
// paid provider is asked.
|
|
40
|
+
judge: async (answer: (doc: string, j: Judgment) => Promise<string | undefined>) => {
|
|
41
|
+
const doc = await gtd(["judge", "--json"]).catch(() => undefined)
|
|
42
|
+
if (doc === undefined) return undefined
|
|
43
|
+
let parsed: unknown
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(doc)
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
return answer(doc, checked<Judgment>("judge --json", parsed))
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|