@pmelab/gtd 20.1.0 → 20.3.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 +3 -1
- package/claude/hooks/register.tsx +27 -30
- package/claude/hooks/schema.ts +52 -0
- package/dist/gtd.bundle.mjs +45988 -45953
- package/package.json +3 -1
- package/src/flows/runtime.test.ts +54 -0
- package/src/flows/runtime.ts +51 -5
package/README.md
CHANGED
|
@@ -501,7 +501,9 @@ The workflow itself is a plain async TypeScript function: a `gtd.config.ts` at
|
|
|
501
501
|
the repository root replaces it, and the pieces the bundled one is built from
|
|
502
502
|
are exported for yours to reuse. See
|
|
503
503
|
[Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md) —
|
|
504
|
-
and remember gtd evaluates that file on every command.
|
|
504
|
+
and remember gtd evaluates that file on every command. A shared workflow package
|
|
505
|
+
bundling its own `@pmelab/gtd` is checked against the running gtd and refused,
|
|
506
|
+
naming which side to upgrade, on a flows protocol mismatch.
|
|
505
507
|
|
|
506
508
|
## License
|
|
507
509
|
|
|
@@ -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
|
+
}
|