@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.
Files changed (83) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +37 -33
  3. package/claude/hooks/doors.ts +8 -4
  4. package/dist/chunks/flows-DENt0uWV.mjs +707 -0
  5. package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
  6. package/dist/chunks/{testkit-07Z_ENch.mjs → testkit-BvecVjQ0.mjs} +570 -390
  7. package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
  8. package/dist/flows.mjs +2 -676
  9. package/dist/gtd.bundle.mjs +751 -645
  10. package/dist/steering.mjs +2 -2
  11. package/dist/testkit/gherkin.mjs +245 -36
  12. package/dist/testkit.mjs +3 -2
  13. package/dist/web/main.js +234 -232
  14. package/dist/wire.mjs +2 -2
  15. package/dist/workflow-kit.mjs +3 -3
  16. package/package.json +1 -1
  17. package/skills/authoring/SKILL.md +66 -60
  18. package/src/cli/Cli.test.ts +183 -182
  19. package/src/cli/Cli.ts +164 -186
  20. package/src/cli/History.ts +9 -4
  21. package/src/cli/Judges.ts +1 -1
  22. package/src/cli/Output.test.ts +35 -0
  23. package/src/cli/Output.ts +31 -1
  24. package/src/cli/Program.test.ts +65 -79
  25. package/src/cli/Program.ts +157 -118
  26. package/src/edge/Edge.test.ts +64 -56
  27. package/src/edge/Edge.ts +3 -3
  28. package/src/flows/factory.test.ts +14 -0
  29. package/src/flows/factory.ts +22 -5
  30. package/src/flows/helpers.test.ts +35 -0
  31. package/src/flows/helpers.ts +19 -10
  32. package/src/flows/index.test.ts +66 -0
  33. package/src/flows/index.ts +109 -5
  34. package/src/flows/internal.ts +7 -0
  35. package/src/flows/runtime.test.ts +192 -1
  36. package/src/flows/runtime.ts +188 -54
  37. package/src/gherkin/git-control.feature +6 -6
  38. package/src/gherkin/index.test.ts +127 -0
  39. package/src/gherkin/index.ts +370 -51
  40. package/src/gherkin/messages.test.ts +38 -4
  41. package/src/gherkin/messages.ts +21 -4
  42. package/src/gherkin/more-steps.feature +170 -0
  43. package/src/gherkin/scenario-end.feature +101 -0
  44. package/src/gherkin/testkit.feature +83 -7
  45. package/src/install/Install.test.ts +7 -7
  46. package/src/install/Install.ts +8 -8
  47. package/src/replay/Replay.test.ts +13 -2
  48. package/src/replay/Replay.ts +19 -30
  49. package/src/replay/judgeDiffShape.test.ts +1 -1
  50. package/src/steering/Footnotes.test.ts +1 -1
  51. package/src/steering/SteeringFormats.test.ts +1 -1
  52. package/src/steering/freeform.test.ts +1 -1
  53. package/src/steering/index.test.ts +27 -1
  54. package/src/steering/index.ts +6 -104
  55. package/src/steering/internal.ts +11 -0
  56. package/src/steering/qa.test.ts +7 -2
  57. package/src/steering/reads.test.ts +42 -0
  58. package/src/steering/reads.ts +43 -0
  59. package/src/steering/registry.ts +93 -0
  60. package/src/steering/review.test.ts +2 -2
  61. package/src/step/planStart.test.ts +18 -16
  62. package/src/step/planStart.ts +9 -9
  63. package/src/testkit/TestRepo.test.ts +245 -7
  64. package/src/testkit/TestRepo.ts +271 -40
  65. package/src/testkit/imports.test.ts +58 -0
  66. package/src/testkit/imports.ts +102 -0
  67. package/src/testkit/index.test.ts +28 -0
  68. package/src/testkit/index.ts +14 -2
  69. package/src/twins/EmittedScriptRecognizer.ts +2 -6
  70. package/src/twins/EmittedScripts.test.ts +4 -2
  71. package/src/twins/InMemRepo.ts +1 -0
  72. package/src/wire/Script.ts +11 -0
  73. package/src/wire/WireSchema.test.ts +3 -0
  74. package/src/wire/golden/1/script.json +4 -0
  75. package/src/wire/index.ts +1 -0
  76. package/src/workflow/SteeringFormats.test.ts +1 -1
  77. package/src/workflow/compile.ts +2 -2
  78. package/src/workflow/discovery.test.ts +3 -3
  79. package/src/workflow/load.test.ts +256 -116
  80. package/src/workflow/load.ts +169 -146
  81. package/src/workflow-kit/index.test.ts +25 -0
  82. package/src/workflow-kit/index.ts +37 -4
  83. 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 { runsItsScript, testRepo, type Actor } from "./TestRepo.js"
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: Actor = (_, r) => r.write("PLAN.md", "p\n")
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 Actor
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", { report: ".gtd/FEEDBACK.md" })
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
  })