@pmelab/gtd 59.0.1 → 60.1.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 (108) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +48 -32
  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-BhTey1Do.mjs +30517 -0
  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 +11285 -19864
  10. package/dist/gtd.core.mjs +593 -0
  11. package/dist/steering.mjs +2 -2
  12. package/dist/testkit/gherkin.mjs +246 -37
  13. package/dist/testkit.mjs +3 -2
  14. package/dist/web/main.js +234 -232
  15. package/dist/wire.mjs +2 -2
  16. package/dist/workflow-kit.mjs +3 -3
  17. package/package.json +7 -7
  18. package/skills/authoring/SKILL.md +66 -60
  19. package/src/cli/Cli.test.ts +185 -184
  20. package/src/cli/Cli.ts +219 -224
  21. package/src/cli/History.ts +9 -4
  22. package/src/cli/Judges.ts +1 -1
  23. package/src/cli/Output.test.ts +35 -0
  24. package/src/cli/Output.ts +34 -3
  25. package/src/cli/Program.test.ts +78 -84
  26. package/src/cli/Program.ts +192 -128
  27. package/src/cli/ToolCommands.ts +2 -2
  28. package/src/edge/Edge.test.ts +72 -62
  29. package/src/edge/Edge.ts +6 -6
  30. package/src/flows/factory.test.ts +14 -0
  31. package/src/flows/factory.ts +22 -5
  32. package/src/flows/helpers.test.ts +35 -0
  33. package/src/flows/helpers.ts +19 -10
  34. package/src/flows/index.test.ts +66 -0
  35. package/src/flows/index.ts +109 -5
  36. package/src/flows/internal.ts +7 -0
  37. package/src/flows/runtime.test.ts +192 -1
  38. package/src/flows/runtime.ts +189 -55
  39. package/src/gherkin/git-control.feature +6 -6
  40. package/src/gherkin/index.test.ts +127 -0
  41. package/src/gherkin/index.ts +370 -51
  42. package/src/gherkin/messages.test.ts +38 -4
  43. package/src/gherkin/messages.ts +21 -4
  44. package/src/gherkin/more-steps.feature +170 -0
  45. package/src/gherkin/scenario-end.feature +101 -0
  46. package/src/gherkin/testkit.feature +83 -7
  47. package/src/install/Install.test.ts +7 -7
  48. package/src/install/Install.ts +8 -8
  49. package/src/install/Version.ts +9 -3
  50. package/src/install/index.ts +1 -1
  51. package/src/platform/CommandRunner.ts +13 -11
  52. package/src/platform/Commentary.ts +2 -2
  53. package/src/platform/Git.test.ts +4 -5
  54. package/src/platform/Git.ts +42 -38
  55. package/src/platform/Host.ts +1 -1
  56. package/src/platform/NodeServices.ts +11 -0
  57. package/src/platform/Workspace.test.ts +4 -5
  58. package/src/platform/Workspace.ts +3 -3
  59. package/src/platform/WorktreeState.test.ts +2 -3
  60. package/src/platform/WorktreeState.ts +1 -1
  61. package/src/platform/index.ts +2 -1
  62. package/src/replay/Replay.test.ts +13 -2
  63. package/src/replay/Replay.ts +19 -30
  64. package/src/replay/judgeDiffShape.test.ts +1 -1
  65. package/src/steering/Footnotes.test.ts +1 -1
  66. package/src/steering/SteeringFormats.test.ts +1 -1
  67. package/src/steering/freeform.test.ts +1 -1
  68. package/src/steering/index.test.ts +27 -1
  69. package/src/steering/index.ts +6 -104
  70. package/src/steering/internal.ts +11 -0
  71. package/src/steering/qa.test.ts +7 -2
  72. package/src/steering/reads.test.ts +42 -0
  73. package/src/steering/reads.ts +43 -0
  74. package/src/steering/registry.ts +93 -0
  75. package/src/steering/review.test.ts +2 -2
  76. package/src/step/planStart.test.ts +18 -16
  77. package/src/step/planStart.ts +12 -12
  78. package/src/testkit/TestRepo.test.ts +245 -7
  79. package/src/testkit/TestRepo.ts +271 -40
  80. package/src/testkit/imports.test.ts +58 -0
  81. package/src/testkit/imports.ts +102 -0
  82. package/src/testkit/index.test.ts +28 -0
  83. package/src/testkit/index.ts +14 -2
  84. package/src/twins/EmittedScriptRecognizer.ts +2 -6
  85. package/src/twins/EmittedScripts.test.ts +5 -3
  86. package/src/twins/GitTiers.ts +6 -7
  87. package/src/twins/InMemRepo.ts +1 -0
  88. package/src/twins/Layers.test.ts +1 -1
  89. package/src/twins/Layers.ts +10 -3
  90. package/src/twins/cliIo.ts +9 -4
  91. package/src/wire/Script.ts +11 -0
  92. package/src/wire/WireSchema.test.ts +3 -0
  93. package/src/wire/golden/1/script.json +4 -0
  94. package/src/wire/index.ts +1 -0
  95. package/src/workflow/SteeringFormats.test.ts +1 -1
  96. package/src/workflow/compile.ts +2 -2
  97. package/src/workflow/discovery.test.ts +27 -10
  98. package/src/workflow/discovery.ts +5 -6
  99. package/src/workflow/index.ts +1 -0
  100. package/src/workflow/load.test.ts +316 -119
  101. package/src/workflow/load.ts +478 -305
  102. package/src/workflow/pin.test.ts +159 -0
  103. package/src/workflow/pin.ts +72 -0
  104. package/src/workflow/vars.ts +3 -3
  105. package/src/workflow-kit/index.test.ts +25 -0
  106. package/src/workflow-kit/index.ts +37 -4
  107. package/src/workflow-kit/review.ts +2 -2
  108. package/dist/chunks/testkit-07Z_ENch.mjs +0 -32175
@@ -35,8 +35,8 @@ const FLAG_NAMES = [
35
35
  "--dev",
36
36
  "--cost",
37
37
  "--model",
38
- "--workflow",
39
38
  "--var",
39
+ "--threshold",
40
40
  "--open-questions",
41
41
  "--open-threads",
42
42
  "--verbose",
@@ -105,13 +105,18 @@ describe("parseArgv — scope", () => {
105
105
  if (plan.kind === "usage") expect(plan.message).toContain("only valid for `gtd land`")
106
106
  })
107
107
 
108
- it("--json is in scope for next, land, judge and judge answer — every other command usage-errors on it", () => {
108
+ it("--json is in scope for next, land, the judge and script commands and the listings — every other command usage-errors on it", () => {
109
109
  for (const args of [
110
110
  ["next", "--json"],
111
111
  ["land", "--json"],
112
112
  ["judge", "--json"],
113
113
  ["judge", "answer", "--json"],
114
- ["doors", "--json"],
114
+ ["validate", "--json"],
115
+ ["abandon", "--json"],
116
+ ["restore", "--json"],
117
+ ["start", "--json"],
118
+ ["start", "fix", "--json"],
119
+ ["history", "fetch", "--json"],
115
120
  ]) {
116
121
  const ok = parseArgv(["node", "gtd.js", ...args])
117
122
  expect(ok.kind).toBe("command")
@@ -120,18 +125,16 @@ describe("parseArgv — scope", () => {
120
125
 
121
126
  for (const args of [
122
127
  ["lsp", "--json"],
123
- ["validate", "--json"],
124
128
  ["check", "qa", "TODO.md", "--json"],
125
- ["install", "--json"],
126
- ["abandon", "--json"],
127
- ["restore", "--json"],
128
- ["--workflow", "some-state", "--json"],
129
+ ["guide", "--json"],
130
+ ["summary", "--json"],
131
+ ["base", "--json"],
129
132
  ]) {
130
133
  const plan = parseArgv(["node", "gtd.js", ...args])
131
134
  expect(plan.kind).toBe("usage")
132
135
  if (plan.kind === "usage") {
133
136
  expect(plan.message).toContain("only valid for `gtd next`/`gtd land`")
134
- expect(plan.message).toContain("gtd install")
137
+ expect(plan.message).toContain("gtd guide")
135
138
  }
136
139
  }
137
140
  })
@@ -167,38 +170,31 @@ describe("parseArgv — scope", () => {
167
170
  }
168
171
  })
169
172
 
170
- it("--var without --workflow is rejected", () => {
171
- const plan = parseArgv(["node", "gtd.js", "land", "--var", "a=1"])
172
- expect(plan.kind).toBe("usage")
173
- if (plan.kind === "usage")
174
- expect(plan.message).toBe("gtd: --var requires --workflow (or `gtd history judges`)")
175
- })
176
-
177
- it("--cost with --workflow is rejected (landing and starting are different verbs)", () => {
178
- const plan = parseArgv(["node", "gtd.js", "--workflow", "foo", "--cost=5"])
179
- expect(plan.kind).toBe("usage")
180
- if (plan.kind === "usage") expect(plan.message).toContain("only valid for `gtd land`")
173
+ it("--var outside `gtd start` is rejected", () => {
174
+ for (const args of [
175
+ ["land", "--var", "a=1"],
176
+ ["history", "judges", "--var", "qualityTriageMinP=0.9"],
177
+ ]) {
178
+ const plan = parseArgv(["node", "gtd.js", ...args])
179
+ expect(plan.kind).toBe("usage")
180
+ if (plan.kind === "usage")
181
+ expect(plan.message).toBe("gtd: --var is only valid for `gtd start <name>`")
182
+ }
181
183
  })
182
184
 
183
- it("--model with --workflow is rejected the same way", () => {
184
- const plan = parseArgv(["node", "gtd.js", "--workflow", "foo", "--model=gpt"])
185
- expect(plan.kind).toBe("usage")
186
- if (plan.kind === "usage") expect(plan.message).toContain("only valid for `gtd land`")
185
+ it("--cost and --model on `gtd start` are rejected (landing and starting are different verbs)", () => {
186
+ for (const flag of ["--cost=5", "--model=gpt"]) {
187
+ const plan = parseArgv(["node", "gtd.js", "start", "foo", flag])
188
+ expect(plan.kind).toBe("usage")
189
+ if (plan.kind === "usage") expect(plan.message).toContain("only valid for `gtd land`")
190
+ }
187
191
  })
188
192
 
189
- it("--workflow on `gtd land` (or any other command) is rejected — landing and starting are different verbs", () => {
190
- for (const args of [
191
- ["validate", "--workflow", "e"],
192
- ["land", "--workflow", "e"],
193
- ]) {
193
+ it("--workflow is gone: an unknown option on its own and after any command", () => {
194
+ for (const args of [["--workflow", "e"], ["--workflow=e"], ["land", "--workflow", "e"]]) {
194
195
  const plan = parseArgv(["node", "gtd.js", ...args])
195
196
  expect(plan.kind).toBe("usage")
196
- if (plan.kind === "usage") {
197
- expect(plan.message).toBe(
198
- "gtd: --workflow is only valid with no other command — use the bare `gtd --workflow <name>` " +
199
- "form; landing and starting are different verbs",
200
- )
201
- }
197
+ if (plan.kind === "usage") expect(plan.message).toContain("unknown option '--workflow")
202
198
  }
203
199
  })
204
200
 
@@ -370,13 +366,13 @@ describe("parseArgv — bare/unknown command under --json", () => {
370
366
  }
371
367
  })
372
368
 
373
- it("bare gtd prints a short usage, not the overview, and points at gtd help and gtd install", () => {
369
+ it("bare gtd prints a short usage, not the overview, and points at gtd help and gtd guide", () => {
374
370
  const plan = parseArgv(["node", "gtd.js"])
375
371
  expect(plan.kind).toBe("usage")
376
372
  if (plan.kind === "usage") {
377
373
  expect(plan.message).toContain("missing command")
378
374
  expect(plan.message).toContain("`gtd help`")
379
- expect(plan.message).toContain("gtd install")
375
+ expect(plan.message).toContain("gtd guide")
380
376
  expect(plan.stdout).toMatch(/^Usage: gtd <command>/)
381
377
  expect(plan.stdout.split("\n").length).toBeLessThan(10)
382
378
  }
@@ -435,43 +431,16 @@ describe("parseArgv — bare/unknown command under --json", () => {
435
431
  })
436
432
  })
437
433
 
438
- describe("parseArgv — entry/var flag details (formerly parseEntryFlags/takeFlagValues)", () => {
439
- it("accepts --workflow=<state> (= form)", () => {
440
- const plan = parseArgv(["node", "gtd.js", "--workflow=side-entry"])
441
- expect(plan.kind).toBe("command")
442
- if (plan.kind === "command") {
443
- expect(plan.command).toEqual({
444
- kind: "start",
445
- actor: "human",
446
- workflow: "side-entry",
447
- vars: {},
448
- label: "gtd --workflow side-entry",
449
- })
450
- }
451
- })
452
-
453
- it("a bare --workflow with no value is a usage error", () => {
454
- const plan = parseArgv(["node", "gtd.js", "--workflow"])
455
- expect(plan.kind).toBe("usage")
456
- if (plan.kind === "usage") expect(plan.message).toContain("--workflow requires a value")
457
- })
458
-
459
- it("a second --workflow occurrence is a usage error (not last-wins)", () => {
460
- const plan = parseArgv(["node", "gtd.js", "--workflow", "a", "--workflow", "b"])
461
- expect(plan.kind).toBe("usage")
462
- if (plan.kind === "usage")
463
- expect(plan.message).toContain("--workflow may be given at most once")
464
- })
465
-
466
- it("a duplicate --var NAME is a usage error", () => {
467
- const plan = parseArgv(["node", "gtd.js", "--workflow", "e", "--var", "a=1", "--var", "a=2"])
434
+ describe("parseArgv — --var details", () => {
435
+ it("a second --var occurrence of one name is a usage error (not last-wins)", () => {
436
+ const plan = parseArgv(["node", "gtd.js", "start", "e", "--var", "a=1", "--var", "a=2"])
468
437
  expect(plan.kind).toBe("usage")
469
438
  if (plan.kind === "usage") expect(plan.message).toContain("--var a")
470
439
  })
471
440
 
472
441
  it("a --var name outside the setting-name rule is a usage error naming it", () => {
473
442
  for (const raw of ["a b=1", "a-b=1", "1a=1", "a\nb=1"]) {
474
- const plan = parseArgv(["node", "gtd.js", "--workflow", "e", "--var", raw])
443
+ const plan = parseArgv(["node", "gtd.js", "start", "e", "--var", raw])
475
444
  expect(plan.kind).toBe("usage")
476
445
  if (plan.kind === "usage") {
477
446
  expect(plan.message).toContain(
@@ -482,36 +451,25 @@ describe("parseArgv — entry/var flag details (formerly parseEntryFlags/takeFla
482
451
  })
483
452
 
484
453
  it("a multiline --var value is a usage error", () => {
485
- const plan = parseArgv(["node", "gtd.js", "--workflow", "e", "--var", "a=1\n2"])
454
+ const plan = parseArgv(["node", "gtd.js", "start", "e", "--var", "a=1\n2"])
486
455
  expect(plan.kind).toBe("usage")
487
456
  })
488
457
  })
489
458
 
490
459
  describe("parseArgv — --entry is removed", () => {
491
- it("is an ordinary unknown option, with no hint at --workflow", () => {
460
+ it("is an ordinary unknown option", () => {
492
461
  const plan = parseArgv(["node", "gtd.js", "--entry", "fix-precheck"])
493
462
  expect(plan.kind).toBe("usage")
494
- if (plan.kind === "usage") {
495
- expect(plan.message).toContain("--entry")
496
- expect(plan.message).not.toContain("--workflow")
497
- }
463
+ if (plan.kind === "usage") expect(plan.message).toContain("--entry")
498
464
  })
499
465
  })
500
466
 
501
- describe("parseArgv — the --workflow selector", () => {
502
- it("gtd --workflow version parses to { kind: start, workflow: version } — the regression this RFC fixes", () => {
503
- const plan = parseArgv(["node", "gtd.js", "--workflow", "version"])
504
- expect(plan.kind).toBe("command")
505
- if (plan.kind === "command") {
506
- expect(plan.command).toMatchObject({ kind: "start", workflow: "version" })
507
- }
508
- })
509
-
510
- it("gtd --workflow help likewise parses to a start command, not the help text", () => {
511
- const plan = parseArgv(["node", "gtd.js", "--workflow", "help"])
512
- expect(plan.kind).toBe("command")
513
- if (plan.kind === "command") {
514
- expect(plan.command).toMatchObject({ kind: "start", workflow: "help" })
467
+ describe("parseArgv — gtd start <name>", () => {
468
+ it("a workflow named like a command word still parses to a start command", () => {
469
+ for (const name of ["version", "help"]) {
470
+ const plan = parseArgv(["node", "gtd.js", "start", name])
471
+ expect(plan.kind).toBe("command")
472
+ if (plan.kind === "command") expect(plan.command).toMatchObject({ kind: "start", name })
515
473
  }
516
474
  })
517
475
  })
@@ -720,7 +678,7 @@ describe("parseArgv — --verbose / -v (the -v/-V swap)", () => {
720
678
  ["land", "--verbose"],
721
679
  ["next", "--verbose", "--json"],
722
680
  ["check", "qa", "TODO.md", "--verbose"],
723
- ["--workflow", "some-state", "--verbose"],
681
+ ["start", "some-state", "--verbose"],
724
682
  ]) {
725
683
  const plan = parseArgv(["node", "gtd.js", ...args])
726
684
  expect(plan.kind).toBe("command")
@@ -729,30 +687,39 @@ describe("parseArgv — --verbose / -v (the -v/-V swap)", () => {
729
687
  })
730
688
  })
731
689
 
732
- describe("parseArgv — gtd install", () => {
733
- it("parses to an install command", () => {
734
- const plan = parseArgv(["node", "gtd.js", "install"])
690
+ describe("parseArgv — gtd guide", () => {
691
+ it("parses to a guide command", () => {
692
+ const plan = parseArgv(["node", "gtd.js", "guide"])
735
693
  expect(plan.kind).toBe("command")
736
694
  if (plan.kind === "command") {
737
- expect(plan.command).toEqual({ kind: "install" })
695
+ expect(plan.command).toEqual({ kind: "guide" })
738
696
  expect(plan.json).toEqual({ kind: "off" })
739
697
  }
740
698
  })
741
699
 
742
- it("--json is out of scope for install — the briefing is plain text only", () => {
743
- const plan = parseArgv(["node", "gtd.js", "install", "--json"])
744
- expect(plan.kind).toBe("usage")
745
- if (plan.kind === "usage") expect(plan.message).toContain("only valid for `gtd next`")
700
+ it("install, door and doors are gone: unknown commands", () => {
701
+ expect(parseArgv(["node", "gtd.js", "install"])).toMatchObject({
702
+ kind: "usage",
703
+ message: expect.stringContaining("unknown command 'install'"),
704
+ })
705
+ for (const word of ["door", "doors"]) {
706
+ expect(parseArgv(["node", "gtd.js", word, "fix"])).toMatchObject({
707
+ kind: "usage",
708
+ message: expect.stringContaining(`unknown command '${word}'`),
709
+ })
710
+ }
711
+ expect(parseArgv(["node", "gtd.js", "help", "install"]).kind).toBe("usage")
712
+ expect(renderHelp()).not.toMatch(/^ {2}(install|doors?)\b|--workflow|Older spellings/m)
746
713
  })
747
714
 
748
- it("gtd install extra is a usage error (install takes no argument)", () => {
749
- const plan = parseArgv(["node", "gtd.js", "install", "extra"])
715
+ it("gtd guide extra is a usage error (guide takes no argument)", () => {
716
+ const plan = parseArgv(["node", "gtd.js", "guide", "extra"])
750
717
  expect(plan.kind).toBe("usage")
751
718
  if (plan.kind === "usage") expect(plan.message).toContain("too many arguments")
752
719
  })
753
720
 
754
- it("a scoped-out flag (e.g. --port) is rejected on install", () => {
755
- const plan = parseArgv(["node", "gtd.js", "install", "--port=3"])
721
+ it("a scoped-out flag (e.g. --port) is rejected on guide", () => {
722
+ const plan = parseArgv(["node", "gtd.js", "guide", "--port=3"])
756
723
  expect(plan.kind).toBe("usage")
757
724
  if (plan.kind === "usage") expect(plan.message).toContain("only valid for `gtd ui`")
758
725
  })
@@ -895,7 +862,7 @@ describe("parseArgv — gtd judge run", () => {
895
862
  [["judge", "run", "--answers", "a.json"], "--answers is only valid with --provider fixed"],
896
863
  [["judge", "run", "--provider", "jev", "--model", "x"], "--model"],
897
864
  [["judge", "run", "--provider", "fixed", "--model", "x"], "--model"],
898
- [["next", "--model", "x"], "`gtd land` (with --cost) or `gtd judge run`"],
865
+ [["next", "--model", "x"], "`gtd land` (with --cost: the model to record the cost against)"],
899
866
  [["judge", "run", "--provider", "fixed", "--json"], "--json is only valid"],
900
867
  [["judge", "run", "--provider", "fixed", "--answers="], "requires a value"],
901
868
  [["judge", "--provider", "fixed"], "only valid for `gtd judge run`"],
@@ -908,59 +875,6 @@ describe("parseArgv — gtd judge run", () => {
908
875
  })
909
876
  })
910
877
 
911
- describe("parseArgv — doors", () => {
912
- const parse = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", ...args])
913
-
914
- it("`gtd door <name>` carries the name and no args", () => {
915
- const plan = parse("door", "fix")
916
- expect(plan).toMatchObject({
917
- kind: "command",
918
- command: { kind: "door", name: "fix", args: [] },
919
- })
920
- })
921
-
922
- it("`gtd door <name> a b` carries every positional after the name, in order", () => {
923
- const plan = parse("door", "review", "v1.0", "extra")
924
- expect(plan).toMatchObject({
925
- kind: "command",
926
- command: { kind: "door", name: "review", args: ["v1.0", "extra"] },
927
- })
928
- })
929
-
930
- it("`gtd door` with no name parses, so the loaded config can list its doors", () => {
931
- expect(parse("door")).toMatchObject({ kind: "command", command: { kind: "door", args: [] } })
932
- })
933
-
934
- it("`gtd doors` parses with every --json shape", () => {
935
- expect(parse("doors")).toMatchObject({
936
- kind: "command",
937
- command: { kind: "doors" },
938
- json: { kind: "off" },
939
- })
940
- expect(parse("doors", "--json")).toMatchObject({ json: { kind: "document" } })
941
- expect(parse("doors", "--json=0.name")).toMatchObject({
942
- json: { kind: "select", path: "0.name" },
943
- })
944
- })
945
-
946
- it("`gtd doors` takes no argument", () => {
947
- const plan = parse("doors", "extra")
948
- expect(plan.kind).toBe("usage")
949
- if (plan.kind === "usage") expect(plan.message).toContain("too many arguments")
950
- })
951
-
952
- it("`gtd door` rejects --json and --var — a door's vars come from its args", () => {
953
- expect(parse("door", "fix", "--json").kind).toBe("usage")
954
- expect(parse("door", "fix", "--var", "a=1").kind).toBe("usage")
955
- })
956
-
957
- it("help lists both commands", () => {
958
- const help = renderHelp()
959
- expect(help).toContain("door <name> [args...]")
960
- expect(help).toMatch(/^ {2}doors\b/m)
961
- })
962
- })
963
-
964
878
  describe("parseArgv — gtd history", () => {
965
879
  const parse = (...args: string[]) => parseArgv(["node", "gtd.js", ...args])
966
880
  const usage = (plan: CliPlan): string => (plan.kind === "usage" ? plan.message : "")
@@ -987,7 +901,7 @@ describe("parseArgv — gtd history", () => {
987
901
  expect(parse("history", "metrics", "a", "feat/b")).toMatchObject({
988
902
  command: { kind: "historyMetrics", refs: ["a", "feat/b"] },
989
903
  })
990
- expect(parse("history", "judges", "a", "--var", "qualityTriageMinP=0.9")).toMatchObject({
904
+ expect(parse("history", "judges", "a", "--threshold", "qualityTriageMinP=0.9")).toMatchObject({
991
905
  command: {
992
906
  kind: "historyJudges",
993
907
  refs: ["a"],
@@ -1000,21 +914,40 @@ describe("parseArgv — gtd history", () => {
1000
914
  })
1001
915
  })
1002
916
 
1003
- it("judges takes only judge thresholds as --var, and --jsonl alone", () => {
1004
- expect(usage(parse("history", "judges", "--var", "reviewBase=main"))).toContain(
1005
- "no judge threshold",
917
+ it("judges takes only judge thresholds as --threshold, and --jsonl alone", () => {
918
+ expect(
919
+ parse(
920
+ "history",
921
+ "judges",
922
+ "--threshold=qualityTriageMinP=0.9",
923
+ "--threshold",
924
+ "judgeIdenticalMinP=0.5",
925
+ ),
926
+ ).toMatchObject({
927
+ command: { vars: { qualityTriageMinP: "0.9", judgeIdenticalMinP: "0.5" } },
928
+ })
929
+ expect(usage(parse("history", "judges", "--threshold", "reviewBase=main"))).toBe(
930
+ "gtd history judges: --threshold reviewBase is no judge threshold — use judgeIdenticalMinP or qualityTriageMinP",
1006
931
  )
1007
- expect(usage(parse("history", "judges", "--var", "toString=0.9"))).toContain(
932
+ expect(usage(parse("history", "judges", "--threshold", "toString=0.9"))).toContain(
1008
933
  "no judge threshold",
1009
934
  )
1010
- expect(usage(parse("history", "judges", "--var", "qualityTriageMinP=0,9"))).toContain(
1011
- "must be a number",
1012
- )
935
+ for (const value of ["x", "0,9"]) {
936
+ expect(usage(parse("history", "judges", "--threshold", `qualityTriageMinP=${value}`))).toBe(
937
+ `gtd history judges: --threshold qualityTriageMinP must be a number — got "${value}"`,
938
+ )
939
+ }
1013
940
  expect(usage(parse("history", "judges", "--jsonl", "--json"))).toContain("mutually exclusive")
1014
- expect(usage(parse("history", "judges", "--jsonl", "--var", "qualityTriageMinP=1"))).toContain(
1015
- "mutually exclusive",
941
+ expect(
942
+ usage(parse("history", "judges", "--jsonl", "--threshold", "qualityTriageMinP=1")),
943
+ ).toContain("mutually exclusive")
944
+ expect(usage(parse("history", "metrics", "--threshold", "qualityTriageMinP=1"))).toContain(
945
+ "only valid for `gtd history judges`",
1016
946
  )
1017
947
  expect(usage(parse("history", "metrics", "--jsonl"))).toContain("only valid for")
948
+ const help = helpFor("help", "history", "judges")
949
+ expect(help).toContain("--threshold <name>=<value>")
950
+ expect(help).not.toContain("--var")
1018
951
  })
1019
952
 
1020
953
  it("bare history and an unknown subcommand are usage errors naming list, show, metrics, fetch", () => {
@@ -1035,7 +968,7 @@ describe("parseArgv — gtd history", () => {
1035
968
  expect(usage(parse("history", "fetch", "a", "b"))).toContain("too many arguments")
1036
969
  })
1037
970
 
1038
- it("--json is accepted for list, show and metrics, rejected for fetch", () => {
971
+ it("--json is accepted for list, show, metrics and fetch", () => {
1039
972
  expect(parse("history", "list", "--json")).toMatchObject({ json: { kind: "document" } })
1040
973
  expect(parse("history", "show", "x", "--json=ended")).toMatchObject({
1041
974
  json: { kind: "select", path: "ended" },
@@ -1043,7 +976,9 @@ describe("parseArgv — gtd history", () => {
1043
976
  expect(parse("history", "metrics", "--json=total.refactorFirstRate")).toMatchObject({
1044
977
  json: { kind: "select", path: "total.refactorFirstRate" },
1045
978
  })
1046
- expect(parse("history", "fetch", "--json").kind).toBe("usage")
979
+ expect(parse("history", "fetch", "--json=script")).toMatchObject({
980
+ json: { kind: "select", path: "script" },
981
+ })
1047
982
  })
1048
983
 
1049
984
  it("help lists the four history rows", () => {
@@ -1056,6 +991,70 @@ describe("parseArgv — gtd history", () => {
1056
991
  })
1057
992
  })
1058
993
 
994
+ describe("parseArgv — gtd start", () => {
995
+ const parse = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", ...args])
996
+
997
+ it("bare `gtd start` parses with no name, so the loaded config can list what starts", () => {
998
+ expect(parse("start")).toEqual({
999
+ kind: "command",
1000
+ command: { kind: "start", args: [], vars: {} },
1001
+ json: { kind: "off" },
1002
+ verbose: false,
1003
+ })
1004
+ expect(parse("start", "--json")).toMatchObject({ json: { kind: "document" } })
1005
+ })
1006
+
1007
+ it("carries the name, every positional after it, and the --var pins", () => {
1008
+ expect(parse("start", "review", "main", "--var", "a=1", "--var=b=2")).toMatchObject({
1009
+ command: { kind: "start", name: "review", args: ["main"], vars: { a: "1", b: "2" } },
1010
+ })
1011
+ })
1012
+
1013
+ it("--var with no name is a usage error", () => {
1014
+ const plan = parse("start", "--var", "a=1")
1015
+ expect(plan.kind === "usage" && plan.message).toContain("--var needs a <name>")
1016
+ })
1017
+
1018
+ it("its help names doors, workflows, --var and the --json=script idiom", () => {
1019
+ const help = helpFor("help", "start")
1020
+ expect(help).toMatch(/^Usage: gtd start \[name\] \[args\.\.\.\] \[options\]\n/)
1021
+ expect(help).toMatch(/A door\s+shadows a workflow of the same name/)
1022
+ expect(help).toContain("--var <name>=<value>")
1023
+ expect(help).toContain('s=$(gtd start <name> --json=script) && sh -c "$s"')
1024
+ })
1025
+ })
1026
+
1027
+ describe("parseArgv — -- ends the options", () => {
1028
+ const parse = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", ...args])
1029
+
1030
+ it("every word after -- is a positional, even one that looks like an option", () => {
1031
+ expect(parse("strays", "a..b", "--", "--weird", "-x")).toMatchObject({
1032
+ command: { kind: "strays", globs: ["--weird", "-x"] },
1033
+ })
1034
+ expect(parse("start", "fix", "--json=script", "--", "--var")).toMatchObject({
1035
+ command: { kind: "start", name: "fix", args: ["--var"], vars: {} },
1036
+ json: { kind: "select", path: "script" },
1037
+ })
1038
+ })
1039
+
1040
+ it("--help, -h, --version and -V after -- are arguments, not short-circuits", () => {
1041
+ expect(parse("start", "fix", "--", "--help")).toMatchObject({
1042
+ command: { kind: "start", args: ["--help"] },
1043
+ })
1044
+ expect(parse("start", "fix", "--", "-V").kind).toBe("command")
1045
+ expect(parse("next", "--", "--version")).toMatchObject({ kind: "usage" })
1046
+ })
1047
+
1048
+ it("a bare -- changes nothing", () => {
1049
+ expect(parse("next", "--")).toEqual(parse("next"))
1050
+ })
1051
+
1052
+ it("an option that needs a value never takes -- as it", () => {
1053
+ const plan = parse("start", "fix", "--var", "--", "a=1")
1054
+ expect(plan.kind === "usage" && plan.message).toContain("--var requires a value")
1055
+ })
1056
+ })
1057
+
1059
1058
  describe("parseArgv — gtd strays", () => {
1060
1059
  const strays = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", "strays", ...args])
1061
1060
 
@@ -1102,14 +1101,14 @@ describe("parseArgv — gtd strays", () => {
1102
1101
 
1103
1102
  describe("standaloneKinds / needsOf", () => {
1104
1103
  it("pins the standalone kinds", () => {
1105
- expect(standaloneKinds()).toEqual(["lsp", "check", "uncheck", "install", "judgeRun", "strays"])
1104
+ expect(standaloneKinds()).toEqual(["lsp", "check", "uncheck", "guide", "judgeRun", "strays"])
1106
1105
  })
1107
1106
 
1108
1107
  it("needsOf matches none/fs for the standalone kinds and state for everything else", () => {
1109
1108
  expect(needsOf("lsp")).toBe("none")
1110
1109
  expect(needsOf("check")).toBe("fs")
1111
1110
  expect(needsOf("uncheck")).toBe("fs")
1112
- expect(needsOf("install")).toBe("none")
1111
+ expect(needsOf("guide")).toBe("none")
1113
1112
  expect(needsOf("judgeRun")).toBe("none")
1114
1113
  expect(needsOf("strays")).toBe("none")
1115
1114
  for (const kind of [
@@ -1122,8 +1121,6 @@ describe("standaloneKinds / needsOf", () => {
1122
1121
  "ui",
1123
1122
  "judge",
1124
1123
  "judgeAnswer",
1125
- "door",
1126
- "doors",
1127
1124
  "historyList",
1128
1125
  "historyShow",
1129
1126
  "historyMetrics",
@@ -1151,14 +1148,14 @@ describe("renderHelp", () => {
1151
1148
  for (const heading of ["Core loop:", "Start and stop:", "Inspect:", "Driver plumbing:"]) {
1152
1149
  expect(help).toContain(`\n${heading}\n`)
1153
1150
  }
1154
- expect(help).toContain("--workflow <name>")
1155
- expect(help).toContain("--var")
1151
+ expect(help).toContain("start [name] [args...]")
1156
1152
  expect(help).toContain("check <mode> <file>")
1157
- expect(help).toContain("door <name> [args...]")
1158
1153
  expect(help).toContain("-V, --version")
1159
1154
  expect(help).toContain("-h, --help")
1160
1155
  expect(help).toContain("Examples:")
1161
1156
  for (const removed of [
1157
+ "--workflow",
1158
+ "Older spellings",
1162
1159
  "visualize",
1163
1160
  ".gtdrc",
1164
1161
  "--no-open",
@@ -1168,7 +1165,7 @@ describe("renderHelp", () => {
1168
1165
  ]) {
1169
1166
  expect(help).not.toContain(removed)
1170
1167
  }
1171
- expect(help).not.toMatch(/^ {2}(init|serve)\b/m)
1168
+ expect(help).not.toMatch(/^ {2}(init|serve|install|door|doors)\b/m)
1172
1169
  expect(help).toMatch(/\n$/)
1173
1170
  })
1174
1171
 
@@ -1206,7 +1203,10 @@ describe("renderHelp", () => {
1206
1203
 
1207
1204
  it("a flag that means something else per command is described per command", () => {
1208
1205
  expect(helpFor("help", "land")).toContain("With --cost: the model")
1206
+ expect(helpFor("help", "land")).toContain("nothing runs on it")
1209
1207
  expect(helpFor("help", "judge", "run")).toContain("The claude model")
1208
+ expect(helpFor("help", "abandon")).toContain("Print { schema, script }")
1209
+ expect(helpFor("help", "next")).not.toContain("Print { schema, script }")
1210
1210
  })
1211
1211
 
1212
1212
  it("--help after an unknown command still prints the overview", () => {
@@ -1361,17 +1361,18 @@ describe("runCli — a garbled GTD_FLAG_* value", () => {
1361
1361
  })
1362
1362
 
1363
1363
  describe("runCli — StartFlags", () => {
1364
- const CONFIG = `import { agent, human } from "@pmelab/gtd/flows"
1364
+ const CONFIG = `import { agent, human, workflow } from "@pmelab/gtd/flows"
1365
1365
 
1366
1366
  export default async () => {
1367
1367
  await human("idle")
1368
1368
  }
1369
1369
 
1370
- export const side = async () => {
1371
- await agent("working", "w")
1372
- }
1373
-
1374
- export const defaults = { flag_beta: "0" }
1370
+ export const side = workflow(
1371
+ async () => {
1372
+ await agent("working", "w")
1373
+ },
1374
+ { defaults: { flag_beta: "0" } },
1375
+ )
1375
1376
  `
1376
1377
  const BETA = { name: "beta", owner: "pmelab", expiry: "2099-01-01", kind: "release" } as const
1377
1378
  const start = async (
@@ -1383,7 +1384,7 @@ export const defaults = { flag_beta: "0" }
1383
1384
  repo.seedCommit("chore: add workflow")
1384
1385
  const { io, result } = makeCapturingCliIo(repo, env)
1385
1386
  await Effect.runPromise(
1386
- runCli(["node", "gtd.js", "--workflow", "side"], { ...io, ...(flags ? { flags } : {}) }),
1387
+ runCli(["node", "gtd.js", "start", "side"], { ...io, ...(flags ? { flags } : {}) }),
1387
1388
  )
1388
1389
  return result().stdout
1389
1390
  }
@@ -1536,7 +1537,7 @@ describe("runCli — stdout stays byte-empty on every failing surface", () => {
1536
1537
  repo.writeFile(".gtd/TODO.md", "## Open Questions\n\n###\n\nno question text.\n")
1537
1538
  const stdoutCalls: string[] = []
1538
1539
  const io: CliIo = {
1539
- stdout: (chunk) => stdoutCalls.push(chunk),
1540
+ stdout: (chunk) => stdoutCalls.push(String(chunk)),
1540
1541
  stderr: () => {},
1541
1542
  exit: () => {},
1542
1543
  layers: () => testLayers(repo),
@@ -1547,7 +1548,7 @@ describe("runCli — stdout stays byte-empty on every failing surface", () => {
1547
1548
  })
1548
1549
 
1549
1550
  describe("nodeCliIo", () => {
1550
- const io = nodeCliIo(Layer.succeed(ToolCommands, {} as ToolCommands["Type"]))
1551
+ const io = nodeCliIo(Layer.succeed(ToolCommands, {} as ToolCommands["Service"]))
1551
1552
 
1552
1553
  it("exposes stdout/stderr/exit/layers", () => {
1553
1554
  expect(typeof io.stdout).toBe("function")