@pmelab/gtd 59.0.1 → 60.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +37 -33
- package/claude/hooks/doors.ts +8 -4
- package/dist/chunks/flows-DENt0uWV.mjs +707 -0
- package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
- package/dist/chunks/{testkit-07Z_ENch.mjs → testkit-BvecVjQ0.mjs} +570 -390
- package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +751 -645
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +245 -36
- package/dist/testkit.mjs +3 -2
- package/dist/web/main.js +234 -232
- package/dist/wire.mjs +2 -2
- package/dist/workflow-kit.mjs +3 -3
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +66 -60
- package/src/cli/Cli.test.ts +183 -182
- package/src/cli/Cli.ts +164 -186
- package/src/cli/History.ts +9 -4
- package/src/cli/Judges.ts +1 -1
- package/src/cli/Output.test.ts +35 -0
- package/src/cli/Output.ts +31 -1
- package/src/cli/Program.test.ts +65 -79
- package/src/cli/Program.ts +157 -118
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +3 -3
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +22 -5
- package/src/flows/helpers.test.ts +35 -0
- package/src/flows/helpers.ts +19 -10
- package/src/flows/index.test.ts +66 -0
- package/src/flows/index.ts +109 -5
- package/src/flows/internal.ts +7 -0
- package/src/flows/runtime.test.ts +192 -1
- package/src/flows/runtime.ts +188 -54
- package/src/gherkin/git-control.feature +6 -6
- package/src/gherkin/index.test.ts +127 -0
- package/src/gherkin/index.ts +370 -51
- package/src/gherkin/messages.test.ts +38 -4
- package/src/gherkin/messages.ts +21 -4
- package/src/gherkin/more-steps.feature +170 -0
- package/src/gherkin/scenario-end.feature +101 -0
- package/src/gherkin/testkit.feature +83 -7
- package/src/install/Install.test.ts +7 -7
- package/src/install/Install.ts +8 -8
- package/src/replay/Replay.test.ts +13 -2
- package/src/replay/Replay.ts +19 -30
- package/src/replay/judgeDiffShape.test.ts +1 -1
- package/src/steering/Footnotes.test.ts +1 -1
- package/src/steering/SteeringFormats.test.ts +1 -1
- package/src/steering/freeform.test.ts +1 -1
- package/src/steering/index.test.ts +27 -1
- package/src/steering/index.ts +6 -104
- package/src/steering/internal.ts +11 -0
- package/src/steering/qa.test.ts +7 -2
- package/src/steering/reads.test.ts +42 -0
- package/src/steering/reads.ts +43 -0
- package/src/steering/registry.ts +93 -0
- package/src/steering/review.test.ts +2 -2
- package/src/step/planStart.test.ts +18 -16
- package/src/step/planStart.ts +9 -9
- package/src/testkit/TestRepo.test.ts +245 -7
- package/src/testkit/TestRepo.ts +271 -40
- package/src/testkit/imports.test.ts +58 -0
- package/src/testkit/imports.ts +102 -0
- package/src/testkit/index.test.ts +28 -0
- package/src/testkit/index.ts +14 -2
- package/src/twins/EmittedScriptRecognizer.ts +2 -6
- package/src/twins/EmittedScripts.test.ts +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- package/src/wire/Script.ts +11 -0
- package/src/wire/WireSchema.test.ts +3 -0
- package/src/wire/golden/1/script.json +4 -0
- package/src/wire/index.ts +1 -0
- package/src/workflow/SteeringFormats.test.ts +1 -1
- package/src/workflow/compile.ts +2 -2
- package/src/workflow/discovery.test.ts +3 -3
- package/src/workflow/load.test.ts +256 -116
- package/src/workflow/load.ts +169 -146
- package/src/workflow-kit/index.test.ts +25 -0
- package/src/workflow-kit/index.ts +37 -4
- package/src/workflow-kit/review.ts +2 -2
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
agent,
|
|
17
17
|
check,
|
|
18
|
+
envSettings as env,
|
|
18
19
|
human,
|
|
19
20
|
judge,
|
|
20
21
|
moveScript,
|
|
@@ -22,8 +23,19 @@ import {
|
|
|
22
23
|
removeScript,
|
|
23
24
|
run,
|
|
24
25
|
showScript,
|
|
26
|
+
settings as vars,
|
|
27
|
+
workflow,
|
|
25
28
|
} from "../flows/index.js"
|
|
26
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
inTurn,
|
|
31
|
+
runsItsScript,
|
|
32
|
+
testRepo,
|
|
33
|
+
verdict,
|
|
34
|
+
verdictWhen,
|
|
35
|
+
whenPrompted,
|
|
36
|
+
type ScriptedActor,
|
|
37
|
+
type TestRepo,
|
|
38
|
+
} from "./TestRepo.js"
|
|
27
39
|
|
|
28
40
|
const tiny = async (): Promise<void> => {
|
|
29
41
|
await agent("plan", "Write PLAN.md.")
|
|
@@ -58,8 +70,8 @@ describe("testRepo", () => {
|
|
|
58
70
|
it("runs a workflow given as gtd.config.ts source", async () => {
|
|
59
71
|
const repo = testRepo({
|
|
60
72
|
workflow: [
|
|
61
|
-
`import { agent, human } from "@pmelab/gtd/flows"`,
|
|
62
|
-
`export const chores = async () => { await agent("sweep", "Sweep."); await human("done") }`,
|
|
73
|
+
`import { agent, human, workflow } from "@pmelab/gtd/flows"`,
|
|
74
|
+
`export const chores = workflow(async () => { await agent("sweep", "Sweep."); await human("done") })`,
|
|
63
75
|
`export default chores`,
|
|
64
76
|
].join("\n"),
|
|
65
77
|
})
|
|
@@ -76,7 +88,7 @@ describe("testRepo", () => {
|
|
|
76
88
|
await human("done")
|
|
77
89
|
}
|
|
78
90
|
const repo = testRepo({
|
|
79
|
-
workflow: { default: tidy, tidy },
|
|
91
|
+
workflow: { default: tidy, tidy: workflow(tidy) },
|
|
80
92
|
files: { "NOTES.md": "notes\n", "scratch.txt": "x\n" },
|
|
81
93
|
})
|
|
82
94
|
await repo.start("tidy")
|
|
@@ -216,7 +228,7 @@ describe("testRepo", () => {
|
|
|
216
228
|
})
|
|
217
229
|
await human(answers.ready?.answer === "yes" ? "ship" : "rework")
|
|
218
230
|
}
|
|
219
|
-
const plan:
|
|
231
|
+
const plan: ScriptedActor = (_, r) => r.write("PLAN.md", "p\n")
|
|
220
232
|
const template =
|
|
221
233
|
'expected a JSON array, one entry per question — answer a boolean for a noul, a string for a choice, a number for a score; p between 0 and 1:\n[{ "id": "ready", "answer": true, "p": 0.9 }]'
|
|
222
234
|
|
|
@@ -234,7 +246,7 @@ describe("testRepo", () => {
|
|
|
234
246
|
it("says an actor returned an object rather than no verdict", async () => {
|
|
235
247
|
const repo = testRepo({ workflow: { default: judged } })
|
|
236
248
|
repo.write("go", "\n")
|
|
237
|
-
const actor = (() => [{ id: "ready", answer: true, p: 0.9 }]) as unknown as
|
|
249
|
+
const actor = (() => [{ id: "ready", answer: true, p: 0.9 }]) as unknown as ScriptedActor
|
|
238
250
|
await expect(repo.drive({ plan, ready: actor })).rejects.toThrow(
|
|
239
251
|
`the actor for judge step "ready" returned an array, not a JSON string — JSON.stringify it\n${template}`,
|
|
240
252
|
)
|
|
@@ -251,7 +263,7 @@ describe("testRepo", () => {
|
|
|
251
263
|
|
|
252
264
|
describe("canned commands", () => {
|
|
253
265
|
const checked = async (): Promise<void> => {
|
|
254
|
-
const green = await check("test", "npm test",
|
|
266
|
+
const green = await check("test", "npm test", ".gtd/FEEDBACK.md")
|
|
255
267
|
await human(green ? "sign-off" : "red")
|
|
256
268
|
}
|
|
257
269
|
|
|
@@ -355,4 +367,230 @@ describe("testRepo", () => {
|
|
|
355
367
|
expect(() => repo.ref("refs/tags/v1", "nowhere")).toThrow("resolves to no commit")
|
|
356
368
|
})
|
|
357
369
|
})
|
|
370
|
+
|
|
371
|
+
describe("modules a config imports", () => {
|
|
372
|
+
it("serves a specifier the config's source imports, read off the module the test imported", async () => {
|
|
373
|
+
const shipped = { tiny, chores: tiny }
|
|
374
|
+
const repo = testRepo({
|
|
375
|
+
workflow: [
|
|
376
|
+
`import { workflow } from "@pmelab/gtd/flows"`,
|
|
377
|
+
`import tidy, * as all from "#workflow"`,
|
|
378
|
+
`export { chores } from "#workflow"`,
|
|
379
|
+
`export const both = all.tiny === tidy ? workflow(() => tidy()) : undefined`,
|
|
380
|
+
`export default tidy`,
|
|
381
|
+
].join("\n"),
|
|
382
|
+
modules: { "#workflow": { ...shipped, default: tiny } },
|
|
383
|
+
})
|
|
384
|
+
await repo.start("both")
|
|
385
|
+
expect((await repo.drive({ plan: (_, r) => r.write("PLAN.md", "p\n") })).state).toBe("build")
|
|
386
|
+
expect(repo.read("gtd.config.ts")).not.toContain('from "#workflow"')
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
describe("strict drives", () => {
|
|
391
|
+
const signOff = async (): Promise<void> => {
|
|
392
|
+
await agent("plan", "Plan.")
|
|
393
|
+
await human("sign-off")
|
|
394
|
+
await agent("ship", "Ship.")
|
|
395
|
+
}
|
|
396
|
+
const plan: ScriptedActor = (_, r) => r.write("PLAN.md", "p\n")
|
|
397
|
+
|
|
398
|
+
it("fails on an actor whose step the drive never reached", async () => {
|
|
399
|
+
const repo = testRepo({ workflow: { default: signOff } })
|
|
400
|
+
repo.write("go", "\n")
|
|
401
|
+
await expect(repo.drive({ plan, ship: () => undefined })).rejects.toThrow(
|
|
402
|
+
'strict drive: the process rests at "sign-off", and never reached the actors for "ship"',
|
|
403
|
+
)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it("leaves only the unreached-actor check out with unreachedActors: pass", async () => {
|
|
407
|
+
const repo = testRepo({ workflow: { default: signOff } })
|
|
408
|
+
repo.write("go", "\n")
|
|
409
|
+
expect(
|
|
410
|
+
(await repo.drive({ plan, ship: () => undefined }, { unreachedActors: "pass" })).state,
|
|
411
|
+
).toBe("sign-off")
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it("lets the same drive rest when it is not strict", async () => {
|
|
415
|
+
const repo = testRepo({ workflow: { default: signOff } })
|
|
416
|
+
repo.write("go", "\n")
|
|
417
|
+
expect((await repo.drive({ plan, ship: () => undefined }, { strict: false })).state).toBe(
|
|
418
|
+
"sign-off",
|
|
419
|
+
)
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it("counts a step an earlier drive reached as reached", async () => {
|
|
423
|
+
const twice = async (): Promise<void> => {
|
|
424
|
+
await signOff()
|
|
425
|
+
await human("done")
|
|
426
|
+
}
|
|
427
|
+
const repo = testRepo({ workflow: { default: twice } })
|
|
428
|
+
repo.write("go", "\n")
|
|
429
|
+
const actors: Record<string, ScriptedActor> = { plan }
|
|
430
|
+
expect((await repo.drive(actors)).state).toBe("sign-off")
|
|
431
|
+
repo.write("signed", "\n")
|
|
432
|
+
await repo.land()
|
|
433
|
+
actors["ship"] = (_, r) => r.write("shipped", "\n")
|
|
434
|
+
expect((await repo.drive(actors)).state).toBe("done")
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it("fails on a human step whose actor changes nothing, instead of repeating it", async () => {
|
|
438
|
+
const repo = testRepo({ workflow: { default: signOff } })
|
|
439
|
+
repo.write("go", "\n")
|
|
440
|
+
await expect(repo.drive({ plan, "sign-off": () => undefined })).rejects.toThrow(
|
|
441
|
+
'strict drive: the actor for human step "sign-off" changed nothing, so nothing landed',
|
|
442
|
+
)
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it("fails on a verdict returned at a step that is no judge", async () => {
|
|
446
|
+
const repo = testRepo({ workflow: { default: signOff } })
|
|
447
|
+
repo.write("go", "\n")
|
|
448
|
+
await expect(repo.drive({ plan: (beat, r) => (plan(beat, r), "[]") })).rejects.toThrow(
|
|
449
|
+
'the actor for agent step "plan" returned a verdict',
|
|
450
|
+
)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it("fails on a check step without an actor, unless checks run themselves", async () => {
|
|
454
|
+
const checked = async (): Promise<void> => {
|
|
455
|
+
const green = await check("test", "npm test", ".gtd/FEEDBACK.md")
|
|
456
|
+
await human(green ? "sign-off" : "red")
|
|
457
|
+
}
|
|
458
|
+
const strictRepo = testRepo({ workflow: { default: checked } })
|
|
459
|
+
strictRepo.write("go", "\n")
|
|
460
|
+
await expect(strictRepo.drive({})).rejects.toThrow(
|
|
461
|
+
'strict drive: check step "test" has no actor',
|
|
462
|
+
)
|
|
463
|
+
const repo = testRepo({
|
|
464
|
+
workflow: { default: checked },
|
|
465
|
+
commands: [{ run: "npm test", exit: 1, output: "1 failing" }],
|
|
466
|
+
})
|
|
467
|
+
repo.write("go", "\n")
|
|
468
|
+
expect((await repo.drive({}, { checks: "run" })).state).toBe("red")
|
|
469
|
+
expect(repo.read(".gtd/FEEDBACK.md")).toContain("1 failing")
|
|
470
|
+
})
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
describe("actor helpers", () => {
|
|
474
|
+
const judged = async (): Promise<void> => {
|
|
475
|
+
await agent("plan", "Plan.")
|
|
476
|
+
const { answers } = await judge("ready", {
|
|
477
|
+
questions: [{ id: "ready", primitive: "noul", instructions: "Ready?", criteria: "" }],
|
|
478
|
+
evidence: { plan: "p" },
|
|
479
|
+
})
|
|
480
|
+
await human(answers.ready?.answer === "yes" ? "ship" : "rework")
|
|
481
|
+
}
|
|
482
|
+
const plan: ScriptedActor = (_, r) => r.write("PLAN.md", "p\n")
|
|
483
|
+
|
|
484
|
+
it("builds a verdict keyed by question id, p defaulting to 0.9", () => {
|
|
485
|
+
expect(verdict({ ready: true, tier: { answer: "low", p: 0.6 } })).toEqual({
|
|
486
|
+
answers: [
|
|
487
|
+
{ id: "ready", answer: true, p: 0.9 },
|
|
488
|
+
{ id: "tier", answer: "low", p: 0.6 },
|
|
489
|
+
],
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
it("answers a judge with a verdict object", async () => {
|
|
494
|
+
const repo = testRepo({ workflow: { default: judged } })
|
|
495
|
+
repo.write("go", "\n")
|
|
496
|
+
expect((await repo.drive({ plan, ready: () => verdict({ ready: false }) })).state).toBe(
|
|
497
|
+
"rework",
|
|
498
|
+
)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it("answers by what the pending questions contain", async () => {
|
|
502
|
+
const repo = testRepo({ workflow: { default: judged } })
|
|
503
|
+
repo.write("go", "\n")
|
|
504
|
+
const ready = inTurn([
|
|
505
|
+
verdictWhen("Unrelated?", verdict({ ready: false })),
|
|
506
|
+
verdictWhen("Ready?", verdict({ ready: true })),
|
|
507
|
+
])
|
|
508
|
+
expect((await repo.drive({ plan, ready })).state).toBe("ship")
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it("runs an actor only at a visit prompted with the text, and every actor in turn", async () => {
|
|
512
|
+
const repo = testRepo({ workflow: { default: judged } })
|
|
513
|
+
repo.write("go", "\n")
|
|
514
|
+
const seen: string[] = []
|
|
515
|
+
await repo.drive({
|
|
516
|
+
plan: inTurn([
|
|
517
|
+
whenPrompted("Plan.", plan),
|
|
518
|
+
whenPrompted("Something else.", () => {
|
|
519
|
+
seen.push("else")
|
|
520
|
+
}),
|
|
521
|
+
() => {
|
|
522
|
+
seen.push("always")
|
|
523
|
+
},
|
|
524
|
+
]),
|
|
525
|
+
})
|
|
526
|
+
expect(repo.read("PLAN.md")).toBe("p\n")
|
|
527
|
+
expect(seen).toEqual(["always"])
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it("runs a script with commands of its own before the canned ones", () => {
|
|
531
|
+
const repo = testRepo({ workflow: { default: tiny }, commands: [{ run: /.*/, exit: 1 }] })
|
|
532
|
+
repo.runScript("make docs\n", {
|
|
533
|
+
commands: [{ run: "make docs", effect: (r) => r.write("docs.html", "<p>\n") }],
|
|
534
|
+
})
|
|
535
|
+
expect(repo.read("docs.html")).toBe("<p>\n")
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
describe("commits and the environment", () => {
|
|
540
|
+
it("lists every commit with its subject, message and trailers", async () => {
|
|
541
|
+
const repo = testRepo({ workflow: { default: tiny } })
|
|
542
|
+
repo.write("IDEA.md", "x\n")
|
|
543
|
+
repo.commit("feat: x\n\nA body.\n\nRefs: #1\nRefs: #2")
|
|
544
|
+
const [initial, feat] = repo.commits()
|
|
545
|
+
expect(initial).toMatchObject({ subject: "chore: initial commit", trailers: {} })
|
|
546
|
+
expect(feat).toEqual({
|
|
547
|
+
hash: repo.hashes()[1],
|
|
548
|
+
subject: "feat: x",
|
|
549
|
+
message: repo.messages()[1],
|
|
550
|
+
trailers: { Refs: ["#1", "#2"] },
|
|
551
|
+
})
|
|
552
|
+
await repo.drive({ plan: (_, r) => r.write("PLAN.md", "p\n") })
|
|
553
|
+
expect(repo.commits().at(-1)!.trailers["Gtd-Step"]).toEqual(["plan#1"])
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
const greeter = {
|
|
557
|
+
default: workflow(
|
|
558
|
+
async (): Promise<void> => {
|
|
559
|
+
await agent("wake", "Wake up.")
|
|
560
|
+
await agent("greet", `Say ${env["greeting"]}, flag ${vars["flag_probe"]}.`)
|
|
561
|
+
},
|
|
562
|
+
{ envDefaults: { greeting: "hi" }, defaults: { flag_probe: "0" } },
|
|
563
|
+
),
|
|
564
|
+
}
|
|
565
|
+
const greet = async (repo: TestRepo): Promise<string> => {
|
|
566
|
+
const rest = await repo.drive({ wake: (_, r) => r.write("awake", "\n") })
|
|
567
|
+
return rest.content
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
it("changes a variable gtd sees from the next call on", async () => {
|
|
571
|
+
const repo = testRepo({ workflow: greeter })
|
|
572
|
+
repo.write("go", "\n")
|
|
573
|
+
expect(await greet(repo)).toContain("Say hi")
|
|
574
|
+
repo.setEnv("GTD_GREETING", "hello")
|
|
575
|
+
expect((await repo.next()).content).toContain("Say hello")
|
|
576
|
+
repo.setEnv("GTD_GREETING", undefined)
|
|
577
|
+
expect((await repo.next()).content).toContain("Say hi")
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
it("keeps the runner's GTD_FLAG_* out when ambient flags are off", async () => {
|
|
581
|
+
const saved = process.env["GTD_FLAG_PROBE"]
|
|
582
|
+
process.env["GTD_FLAG_PROBE"] = "1"
|
|
583
|
+
try {
|
|
584
|
+
const ambient = testRepo({ workflow: greeter })
|
|
585
|
+
ambient.write("go", "\n")
|
|
586
|
+
expect(await greet(ambient)).toContain("flag 1")
|
|
587
|
+
const isolated = testRepo({ workflow: greeter, ambientFlags: false })
|
|
588
|
+
isolated.write("go", "\n")
|
|
589
|
+
expect(await greet(isolated)).toContain("flag 0")
|
|
590
|
+
} finally {
|
|
591
|
+
if (saved === undefined) delete process.env["GTD_FLAG_PROBE"]
|
|
592
|
+
else process.env["GTD_FLAG_PROBE"] = saved
|
|
593
|
+
}
|
|
594
|
+
})
|
|
595
|
+
})
|
|
358
596
|
})
|