@pmelab/gtd 59.0.0 → 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 (90) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +42 -38
  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-CMBAAyRK.mjs → testkit-BvecVjQ0.mjs} +937 -772
  7. package/dist/chunks/{wire-C12dahrH.mjs → wire-DZ8R1RU9.mjs} +9 -2
  8. package/dist/flows.mjs +2 -676
  9. package/dist/gtd.bundle.mjs +1101 -1025
  10. package/dist/steering.mjs +2 -2
  11. package/dist/testkit/gherkin.mjs +371 -82
  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 +68 -61
  18. package/src/cli/Cli.test.ts +311 -229
  19. package/src/cli/Cli.ts +486 -585
  20. package/src/cli/Help.test.ts +47 -0
  21. package/src/cli/Help.ts +70 -0
  22. package/src/cli/History.ts +9 -4
  23. package/src/cli/Judges.ts +1 -1
  24. package/src/cli/Output.test.ts +35 -0
  25. package/src/cli/Output.ts +31 -1
  26. package/src/cli/Program.test.ts +131 -82
  27. package/src/cli/Program.ts +197 -111
  28. package/src/edge/Edge.test.ts +64 -56
  29. package/src/edge/Edge.ts +4 -4
  30. package/src/emit/Emit.test.ts +1 -1
  31. package/src/emit/Emit.ts +1 -1
  32. package/src/flows/factory.test.ts +14 -0
  33. package/src/flows/factory.ts +25 -6
  34. package/src/flows/helpers.test.ts +35 -0
  35. package/src/flows/helpers.ts +19 -10
  36. package/src/flows/index.test.ts +66 -0
  37. package/src/flows/index.ts +109 -5
  38. package/src/flows/internal.ts +7 -0
  39. package/src/flows/runtime.test.ts +192 -1
  40. package/src/flows/runtime.ts +188 -54
  41. package/src/gherkin/git-control.feature +6 -6
  42. package/src/gherkin/index.test.ts +127 -0
  43. package/src/gherkin/index.ts +481 -98
  44. package/src/gherkin/messages.test.ts +124 -0
  45. package/src/gherkin/messages.ts +101 -0
  46. package/src/gherkin/more-steps.feature +170 -0
  47. package/src/gherkin/scenario-end.feature +101 -0
  48. package/src/gherkin/testkit.feature +83 -7
  49. package/src/install/Install.test.ts +7 -7
  50. package/src/install/Install.ts +8 -8
  51. package/src/replay/Replay.test.ts +13 -2
  52. package/src/replay/Replay.ts +19 -30
  53. package/src/replay/judgeDiffShape.test.ts +1 -1
  54. package/src/steering/Footnotes.test.ts +1 -1
  55. package/src/steering/SteeringFormats.test.ts +1 -1
  56. package/src/steering/freeform.test.ts +1 -1
  57. package/src/steering/index.test.ts +27 -1
  58. package/src/steering/index.ts +6 -104
  59. package/src/steering/internal.ts +11 -0
  60. package/src/steering/qa.test.ts +7 -2
  61. package/src/steering/reads.test.ts +42 -0
  62. package/src/steering/reads.ts +43 -0
  63. package/src/steering/registry.ts +93 -0
  64. package/src/steering/review.test.ts +2 -2
  65. package/src/step/planStart.test.ts +18 -16
  66. package/src/step/planStart.ts +9 -9
  67. package/src/testkit/TestRepo.test.ts +289 -6
  68. package/src/testkit/TestRepo.ts +317 -41
  69. package/src/testkit/imports.test.ts +58 -0
  70. package/src/testkit/imports.ts +102 -0
  71. package/src/testkit/index.test.ts +28 -0
  72. package/src/testkit/index.ts +14 -2
  73. package/src/twins/EmittedScriptRecognizer.ts +7 -14
  74. package/src/twins/EmittedScripts.test.ts +4 -2
  75. package/src/twins/InMemRepo.ts +1 -0
  76. package/src/wire/Land.ts +1 -1
  77. package/src/wire/Script.ts +11 -0
  78. package/src/wire/WireSchema.test.ts +3 -0
  79. package/src/wire/golden/1/script.json +4 -0
  80. package/src/wire/index.ts +1 -0
  81. package/src/workflow/SteeringFormats.test.ts +1 -1
  82. package/src/workflow/compile.ts +2 -2
  83. package/src/workflow/discovery.test.ts +3 -3
  84. package/src/workflow/load.test.ts +256 -116
  85. package/src/workflow/load.ts +175 -152
  86. package/src/workflow/skills.test.ts +5 -5
  87. package/src/workflow/skills.ts +1 -1
  88. package/src/workflow-kit/index.test.ts +25 -0
  89. package/src/workflow-kit/index.ts +37 -4
  90. package/src/workflow-kit/review.ts +2 -2
@@ -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",
@@ -71,7 +71,8 @@ describe("parseArgv — unknown options", () => {
71
71
  expect(plan).toEqual({
72
72
  kind: "usage",
73
73
  stdout: "",
74
- message: "gtd: unknown option '--jsn' — see `gtd --help`",
74
+ message:
75
+ "gtd: unknown option '--jsn' — did you mean `--json`? Run `gtd help <command>` for its options",
75
76
  json: false,
76
77
  })
77
78
  })
@@ -104,13 +105,18 @@ describe("parseArgv — scope", () => {
104
105
  if (plan.kind === "usage") expect(plan.message).toContain("only valid for `gtd land`")
105
106
  })
106
107
 
107
- 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", () => {
108
109
  for (const args of [
109
110
  ["next", "--json"],
110
111
  ["land", "--json"],
111
112
  ["judge", "--json"],
112
113
  ["judge", "answer", "--json"],
113
- ["doors", "--json"],
114
+ ["validate", "--json"],
115
+ ["abandon", "--json"],
116
+ ["restore", "--json"],
117
+ ["start", "--json"],
118
+ ["start", "fix", "--json"],
119
+ ["history", "fetch", "--json"],
114
120
  ]) {
115
121
  const ok = parseArgv(["node", "gtd.js", ...args])
116
122
  expect(ok.kind).toBe("command")
@@ -119,18 +125,16 @@ describe("parseArgv — scope", () => {
119
125
 
120
126
  for (const args of [
121
127
  ["lsp", "--json"],
122
- ["validate", "--json"],
123
128
  ["check", "qa", "TODO.md", "--json"],
124
- ["install", "--json"],
125
- ["abandon", "--json"],
126
- ["restore", "--json"],
127
- ["--workflow", "some-state", "--json"],
129
+ ["guide", "--json"],
130
+ ["summary", "--json"],
131
+ ["base", "--json"],
128
132
  ]) {
129
133
  const plan = parseArgv(["node", "gtd.js", ...args])
130
134
  expect(plan.kind).toBe("usage")
131
135
  if (plan.kind === "usage") {
132
136
  expect(plan.message).toContain("only valid for `gtd next`/`gtd land`")
133
- expect(plan.message).toContain("gtd install")
137
+ expect(plan.message).toContain("gtd guide")
134
138
  }
135
139
  }
136
140
  })
@@ -166,38 +170,31 @@ describe("parseArgv — scope", () => {
166
170
  }
167
171
  })
168
172
 
169
- it("--var without --workflow is rejected", () => {
170
- const plan = parseArgv(["node", "gtd.js", "land", "--var", "a=1"])
171
- expect(plan.kind).toBe("usage")
172
- if (plan.kind === "usage")
173
- expect(plan.message).toBe("gtd: --var requires --workflow (or `gtd history judges`)")
174
- })
175
-
176
- it("--cost with --workflow is rejected (landing and starting are different verbs)", () => {
177
- const plan = parseArgv(["node", "gtd.js", "--workflow", "foo", "--cost=5"])
178
- expect(plan.kind).toBe("usage")
179
- 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
+ }
180
183
  })
181
184
 
182
- it("--model with --workflow is rejected the same way", () => {
183
- const plan = parseArgv(["node", "gtd.js", "--workflow", "foo", "--model=gpt"])
184
- expect(plan.kind).toBe("usage")
185
- 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
+ }
186
191
  })
187
192
 
188
- it("--workflow on `gtd land` (or any other command) is rejected — landing and starting are different verbs", () => {
189
- for (const args of [
190
- ["validate", "--workflow", "e"],
191
- ["land", "--workflow", "e"],
192
- ]) {
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"]]) {
193
195
  const plan = parseArgv(["node", "gtd.js", ...args])
194
196
  expect(plan.kind).toBe("usage")
195
- if (plan.kind === "usage") {
196
- expect(plan.message).toBe(
197
- "gtd: --workflow is only valid with no other command — use the bare `gtd --workflow <name>` " +
198
- "form; landing and starting are different verbs",
199
- )
200
- }
197
+ if (plan.kind === "usage") expect(plan.message).toContain("unknown option '--workflow")
201
198
  }
202
199
  })
203
200
 
@@ -369,17 +366,52 @@ describe("parseArgv — bare/unknown command under --json", () => {
369
366
  }
370
367
  })
371
368
 
372
- it("bare gtd's missing-command message (no --json) points at gtd install, not a bundled loop", () => {
369
+ it("bare gtd prints a short usage, not the overview, and points at gtd help and gtd guide", () => {
373
370
  const plan = parseArgv(["node", "gtd.js"])
374
371
  expect(plan.kind).toBe("usage")
375
372
  if (plan.kind === "usage") {
376
373
  expect(plan.message).toContain("missing command")
377
- expect(plan.message).toContain("gtd decides and prints")
378
- expect(plan.message).toContain("gtd install")
379
- expect(plan.message).toContain("A complete minimal driver")
374
+ expect(plan.message).toContain("`gtd help`")
375
+ expect(plan.message).toContain("gtd guide")
376
+ expect(plan.stdout).toMatch(/^Usage: gtd <command>/)
377
+ expect(plan.stdout.split("\n").length).toBeLessThan(10)
380
378
  }
381
379
  })
382
380
 
381
+ it("an unknown command that looks like a typo names the nearest command", () => {
382
+ expect(parseArgv(["node", "gtd.js", "nxt"])).toEqual({
383
+ kind: "usage",
384
+ stdout: "",
385
+ message:
386
+ "gtd: unknown command 'nxt' — did you mean `gtd next`? Run `gtd help` for every command",
387
+ json: false,
388
+ })
389
+ })
390
+
391
+ it("an unknown command nothing resembles points at gtd help only", () => {
392
+ const plan = parseArgv(["node", "gtd.js", "frobnicate"])
393
+ expect(plan.kind === "usage" && plan.message).toBe(
394
+ "gtd: unknown command 'frobnicate' — run `gtd help` for every command",
395
+ )
396
+ })
397
+
398
+ it("a mistyped history subcommand names the nearest one", () => {
399
+ const plan = parseArgv(["node", "gtd.js", "history", "shwo"])
400
+ expect(plan.kind === "usage" && plan.message).toContain("did you mean `gtd history show`?")
401
+ })
402
+
403
+ it("a bare --json followed by a word suggests the = form", () => {
404
+ const plan = parseArgv(["node", "gtd.js", "next", "--json", "kind"])
405
+ expect(plan.kind === "usage" && plan.message).toBe(
406
+ "gtd next: too many arguments — expected none, got: kind — to print one field, write --json=kind",
407
+ )
408
+ })
409
+
410
+ it("an unknown single-dash option is an unknown option, not an extra argument", () => {
411
+ const plan = parseArgv(["node", "gtd.js", "-j", "next"])
412
+ expect(plan.kind === "usage" && plan.message).toContain("unknown option '-j'")
413
+ })
414
+
383
415
  it("an unknown command under --json carries json:true, naming --json's own scope error over the subcommand", () => {
384
416
  const plan = parseArgv(["node", "gtd.js", "bogus", "--json"])
385
417
  expect(plan.kind).toBe("usage")
@@ -399,43 +431,16 @@ describe("parseArgv — bare/unknown command under --json", () => {
399
431
  })
400
432
  })
401
433
 
402
- describe("parseArgv — entry/var flag details (formerly parseEntryFlags/takeFlagValues)", () => {
403
- it("accepts --workflow=<state> (= form)", () => {
404
- const plan = parseArgv(["node", "gtd.js", "--workflow=side-entry"])
405
- expect(plan.kind).toBe("command")
406
- if (plan.kind === "command") {
407
- expect(plan.command).toEqual({
408
- kind: "start",
409
- actor: "human",
410
- workflow: "side-entry",
411
- vars: {},
412
- label: "gtd --workflow side-entry",
413
- })
414
- }
415
- })
416
-
417
- it("a bare --workflow with no value is a usage error", () => {
418
- const plan = parseArgv(["node", "gtd.js", "--workflow"])
419
- expect(plan.kind).toBe("usage")
420
- if (plan.kind === "usage") expect(plan.message).toContain("--workflow requires a value")
421
- })
422
-
423
- it("a second --workflow occurrence is a usage error (not last-wins)", () => {
424
- const plan = parseArgv(["node", "gtd.js", "--workflow", "a", "--workflow", "b"])
425
- expect(plan.kind).toBe("usage")
426
- if (plan.kind === "usage")
427
- expect(plan.message).toContain("--workflow may be given at most once")
428
- })
429
-
430
- it("a duplicate --var NAME is a usage error", () => {
431
- 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"])
432
437
  expect(plan.kind).toBe("usage")
433
438
  if (plan.kind === "usage") expect(plan.message).toContain("--var a")
434
439
  })
435
440
 
436
441
  it("a --var name outside the setting-name rule is a usage error naming it", () => {
437
442
  for (const raw of ["a b=1", "a-b=1", "1a=1", "a\nb=1"]) {
438
- const plan = parseArgv(["node", "gtd.js", "--workflow", "e", "--var", raw])
443
+ const plan = parseArgv(["node", "gtd.js", "start", "e", "--var", raw])
439
444
  expect(plan.kind).toBe("usage")
440
445
  if (plan.kind === "usage") {
441
446
  expect(plan.message).toContain(
@@ -446,36 +451,25 @@ describe("parseArgv — entry/var flag details (formerly parseEntryFlags/takeFla
446
451
  })
447
452
 
448
453
  it("a multiline --var value is a usage error", () => {
449
- 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"])
450
455
  expect(plan.kind).toBe("usage")
451
456
  })
452
457
  })
453
458
 
454
459
  describe("parseArgv — --entry is removed", () => {
455
- it("is an ordinary unknown option, with no hint at --workflow", () => {
460
+ it("is an ordinary unknown option", () => {
456
461
  const plan = parseArgv(["node", "gtd.js", "--entry", "fix-precheck"])
457
462
  expect(plan.kind).toBe("usage")
458
- if (plan.kind === "usage") {
459
- expect(plan.message).toContain("--entry")
460
- expect(plan.message).not.toContain("--workflow")
461
- }
463
+ if (plan.kind === "usage") expect(plan.message).toContain("--entry")
462
464
  })
463
465
  })
464
466
 
465
- describe("parseArgv — the --workflow selector", () => {
466
- it("gtd --workflow version parses to { kind: start, workflow: version } — the regression this RFC fixes", () => {
467
- const plan = parseArgv(["node", "gtd.js", "--workflow", "version"])
468
- expect(plan.kind).toBe("command")
469
- if (plan.kind === "command") {
470
- expect(plan.command).toMatchObject({ kind: "start", workflow: "version" })
471
- }
472
- })
473
-
474
- it("gtd --workflow help likewise parses to a start command, not the help text", () => {
475
- const plan = parseArgv(["node", "gtd.js", "--workflow", "help"])
476
- expect(plan.kind).toBe("command")
477
- if (plan.kind === "command") {
478
- 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 })
479
473
  }
480
474
  })
481
475
  })
@@ -684,7 +678,7 @@ describe("parseArgv — --verbose / -v (the -v/-V swap)", () => {
684
678
  ["land", "--verbose"],
685
679
  ["next", "--verbose", "--json"],
686
680
  ["check", "qa", "TODO.md", "--verbose"],
687
- ["--workflow", "some-state", "--verbose"],
681
+ ["start", "some-state", "--verbose"],
688
682
  ]) {
689
683
  const plan = parseArgv(["node", "gtd.js", ...args])
690
684
  expect(plan.kind).toBe("command")
@@ -693,30 +687,39 @@ describe("parseArgv — --verbose / -v (the -v/-V swap)", () => {
693
687
  })
694
688
  })
695
689
 
696
- describe("parseArgv — gtd install", () => {
697
- it("parses to an install command", () => {
698
- 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"])
699
693
  expect(plan.kind).toBe("command")
700
694
  if (plan.kind === "command") {
701
- expect(plan.command).toEqual({ kind: "install" })
695
+ expect(plan.command).toEqual({ kind: "guide" })
702
696
  expect(plan.json).toEqual({ kind: "off" })
703
697
  }
704
698
  })
705
699
 
706
- it("--json is out of scope for install — the briefing is plain text only", () => {
707
- const plan = parseArgv(["node", "gtd.js", "install", "--json"])
708
- expect(plan.kind).toBe("usage")
709
- 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)
710
713
  })
711
714
 
712
- it("gtd install extra is a usage error (install takes no argument)", () => {
713
- 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"])
714
717
  expect(plan.kind).toBe("usage")
715
718
  if (plan.kind === "usage") expect(plan.message).toContain("too many arguments")
716
719
  })
717
720
 
718
- it("a scoped-out flag (e.g. --port) is rejected on install", () => {
719
- 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"])
720
723
  expect(plan.kind).toBe("usage")
721
724
  if (plan.kind === "usage") expect(plan.message).toContain("only valid for `gtd ui`")
722
725
  })
@@ -859,7 +862,7 @@ describe("parseArgv — gtd judge run", () => {
859
862
  [["judge", "run", "--answers", "a.json"], "--answers is only valid with --provider fixed"],
860
863
  [["judge", "run", "--provider", "jev", "--model", "x"], "--model"],
861
864
  [["judge", "run", "--provider", "fixed", "--model", "x"], "--model"],
862
- [["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)"],
863
866
  [["judge", "run", "--provider", "fixed", "--json"], "--json is only valid"],
864
867
  [["judge", "run", "--provider", "fixed", "--answers="], "requires a value"],
865
868
  [["judge", "--provider", "fixed"], "only valid for `gtd judge run`"],
@@ -872,61 +875,6 @@ describe("parseArgv — gtd judge run", () => {
872
875
  })
873
876
  })
874
877
 
875
- describe("parseArgv — doors", () => {
876
- const parse = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", ...args])
877
-
878
- it("`gtd door <name>` carries the name and no args", () => {
879
- const plan = parse("door", "fix")
880
- expect(plan).toMatchObject({
881
- kind: "command",
882
- command: { kind: "door", name: "fix", args: [] },
883
- })
884
- })
885
-
886
- it("`gtd door <name> a b` carries every positional after the name, in order", () => {
887
- const plan = parse("door", "review", "v1.0", "extra")
888
- expect(plan).toMatchObject({
889
- kind: "command",
890
- command: { kind: "door", name: "review", args: ["v1.0", "extra"] },
891
- })
892
- })
893
-
894
- it("`gtd door` with no name is a usage error", () => {
895
- const plan = parse("door")
896
- expect(plan.kind).toBe("usage")
897
- if (plan.kind === "usage") expect(plan.message).toContain("gtd door: missing name argument")
898
- })
899
-
900
- it("`gtd doors` parses with every --json shape", () => {
901
- expect(parse("doors")).toMatchObject({
902
- kind: "command",
903
- command: { kind: "doors" },
904
- json: { kind: "off" },
905
- })
906
- expect(parse("doors", "--json")).toMatchObject({ json: { kind: "document" } })
907
- expect(parse("doors", "--json=0.name")).toMatchObject({
908
- json: { kind: "select", path: "0.name" },
909
- })
910
- })
911
-
912
- it("`gtd doors` takes no argument", () => {
913
- const plan = parse("doors", "extra")
914
- expect(plan.kind).toBe("usage")
915
- if (plan.kind === "usage") expect(plan.message).toContain("too many arguments")
916
- })
917
-
918
- it("`gtd door` rejects --json and --var — a door's vars come from its args", () => {
919
- expect(parse("door", "fix", "--json").kind).toBe("usage")
920
- expect(parse("door", "fix", "--var", "a=1").kind).toBe("usage")
921
- })
922
-
923
- it("help lists both commands", () => {
924
- const help = renderHelp()
925
- expect(help).toContain("door <name> [args...]")
926
- expect(help).toMatch(/^ {2}doors\b/m)
927
- })
928
- })
929
-
930
878
  describe("parseArgv — gtd history", () => {
931
879
  const parse = (...args: string[]) => parseArgv(["node", "gtd.js", ...args])
932
880
  const usage = (plan: CliPlan): string => (plan.kind === "usage" ? plan.message : "")
@@ -953,7 +901,7 @@ describe("parseArgv — gtd history", () => {
953
901
  expect(parse("history", "metrics", "a", "feat/b")).toMatchObject({
954
902
  command: { kind: "historyMetrics", refs: ["a", "feat/b"] },
955
903
  })
956
- expect(parse("history", "judges", "a", "--var", "qualityTriageMinP=0.9")).toMatchObject({
904
+ expect(parse("history", "judges", "a", "--threshold", "qualityTriageMinP=0.9")).toMatchObject({
957
905
  command: {
958
906
  kind: "historyJudges",
959
907
  refs: ["a"],
@@ -966,21 +914,40 @@ describe("parseArgv — gtd history", () => {
966
914
  })
967
915
  })
968
916
 
969
- it("judges takes only judge thresholds as --var, and --jsonl alone", () => {
970
- expect(usage(parse("history", "judges", "--var", "reviewBase=main"))).toContain(
971
- "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",
972
931
  )
973
- expect(usage(parse("history", "judges", "--var", "toString=0.9"))).toContain(
932
+ expect(usage(parse("history", "judges", "--threshold", "toString=0.9"))).toContain(
974
933
  "no judge threshold",
975
934
  )
976
- expect(usage(parse("history", "judges", "--var", "qualityTriageMinP=0,9"))).toContain(
977
- "must be a number",
978
- )
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
+ }
979
940
  expect(usage(parse("history", "judges", "--jsonl", "--json"))).toContain("mutually exclusive")
980
- expect(usage(parse("history", "judges", "--jsonl", "--var", "qualityTriageMinP=1"))).toContain(
981
- "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`",
982
946
  )
983
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")
984
951
  })
985
952
 
986
953
  it("bare history and an unknown subcommand are usage errors naming list, show, metrics, fetch", () => {
@@ -1001,7 +968,7 @@ describe("parseArgv — gtd history", () => {
1001
968
  expect(usage(parse("history", "fetch", "a", "b"))).toContain("too many arguments")
1002
969
  })
1003
970
 
1004
- it("--json is accepted for list, show and metrics, rejected for fetch", () => {
971
+ it("--json is accepted for list, show, metrics and fetch", () => {
1005
972
  expect(parse("history", "list", "--json")).toMatchObject({ json: { kind: "document" } })
1006
973
  expect(parse("history", "show", "x", "--json=ended")).toMatchObject({
1007
974
  json: { kind: "select", path: "ended" },
@@ -1009,7 +976,9 @@ describe("parseArgv — gtd history", () => {
1009
976
  expect(parse("history", "metrics", "--json=total.refactorFirstRate")).toMatchObject({
1010
977
  json: { kind: "select", path: "total.refactorFirstRate" },
1011
978
  })
1012
- expect(parse("history", "fetch", "--json").kind).toBe("usage")
979
+ expect(parse("history", "fetch", "--json=script")).toMatchObject({
980
+ json: { kind: "select", path: "script" },
981
+ })
1013
982
  })
1014
983
 
1015
984
  it("help lists the four history rows", () => {
@@ -1022,6 +991,70 @@ describe("parseArgv — gtd history", () => {
1022
991
  })
1023
992
  })
1024
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
+
1025
1058
  describe("parseArgv — gtd strays", () => {
1026
1059
  const strays = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", "strays", ...args])
1027
1060
 
@@ -1061,20 +1094,21 @@ describe("parseArgv — gtd strays", () => {
1061
1094
  const plan = parseArgv(["node", "gtd.js", "lsp", "--json"])
1062
1095
  if (plan.kind === "usage") expect(plan.message).toContain("`gtd strays`")
1063
1096
  expect(renderHelp()).toContain("strays <range> [glob...]")
1064
- expect(renderHelp()).toContain("gtd strays only")
1097
+ const help = parseArgv(["node", "gtd.js", "help", "strays"])
1098
+ expect(help.kind === "output" && help.stdout).toContain("--json[=<path>]")
1065
1099
  })
1066
1100
  })
1067
1101
 
1068
1102
  describe("standaloneKinds / needsOf", () => {
1069
1103
  it("pins the standalone kinds", () => {
1070
- expect(standaloneKinds()).toEqual(["lsp", "check", "uncheck", "install", "judgeRun", "strays"])
1104
+ expect(standaloneKinds()).toEqual(["lsp", "check", "uncheck", "guide", "judgeRun", "strays"])
1071
1105
  })
1072
1106
 
1073
1107
  it("needsOf matches none/fs for the standalone kinds and state for everything else", () => {
1074
1108
  expect(needsOf("lsp")).toBe("none")
1075
1109
  expect(needsOf("check")).toBe("fs")
1076
1110
  expect(needsOf("uncheck")).toBe("fs")
1077
- expect(needsOf("install")).toBe("none")
1111
+ expect(needsOf("guide")).toBe("none")
1078
1112
  expect(needsOf("judgeRun")).toBe("none")
1079
1113
  expect(needsOf("strays")).toBe("none")
1080
1114
  for (const kind of [
@@ -1087,8 +1121,6 @@ describe("standaloneKinds / needsOf", () => {
1087
1121
  "ui",
1088
1122
  "judge",
1089
1123
  "judgeAnswer",
1090
- "door",
1091
- "doors",
1092
1124
  "historyList",
1093
1125
  "historyShow",
1094
1126
  "historyMetrics",
@@ -1100,56 +1132,96 @@ describe("standaloneKinds / needsOf", () => {
1100
1132
  })
1101
1133
  })
1102
1134
 
1135
+ const helpFor = (...args: string[]): string => {
1136
+ const plan = parseArgv(["node", "gtd.js", ...args])
1137
+ if (plan.kind !== "output") throw new Error(`no help for ${args.join(" ")}`)
1138
+ return plan.stdout
1139
+ }
1140
+
1141
+ /** The command names the overview lists — ` name ` lines, as the docs pins read them. */
1142
+ const listedCommands = (): string[] =>
1143
+ [...renderHelp().matchAll(/^ {2}([a-z]+(?:-[a-z]+)?(?: [a-z]+)?)(?= )/gm)].map(([, n]) => n!)
1144
+
1103
1145
  describe("renderHelp", () => {
1104
- it("mentions every command and flag, and no removed surface", () => {
1146
+ it("groups the commands and mentions no removed surface", () => {
1105
1147
  const help = renderHelp()
1106
- expect(help).toContain("Usage")
1107
- expect(help).toContain("land")
1108
- expect(help).toContain("--workflow <name>")
1109
- expect(help).toContain("--var")
1110
- expect(help).toContain("abandon")
1111
- expect(help).toContain("restore")
1112
- expect(help).toContain("next")
1113
- expect(help).toContain("validate")
1114
- expect(help).toContain("lsp")
1115
- expect(help).not.toContain("visualize")
1116
- expect(help).not.toMatch(/^ {2}init\b/m)
1117
- expect(help).not.toContain(".gtdrc")
1118
- expect(help).toMatch(/^ {2}ui\b/m)
1148
+ for (const heading of ["Core loop:", "Start and stop:", "Inspect:", "Driver plumbing:"]) {
1149
+ expect(help).toContain(`\n${heading}\n`)
1150
+ }
1151
+ expect(help).toContain("start [name] [args...]")
1119
1152
  expect(help).toContain("check <mode> <file>")
1120
- expect(help).toContain("install")
1121
- expect(help).toContain("base ")
1122
- expect(help).toMatch(/^ {2}judge\b/m)
1123
- expect(help).toContain("judge answer")
1124
- expect(help).toContain("version")
1125
- expect(help).toContain("help")
1126
- expect(help).toContain("--json")
1127
- expect(help).toContain("--port")
1128
- expect(help).not.toContain("--no-open")
1129
- expect(help).toContain("--host")
1130
- expect(help).toContain("--self-signed")
1131
- expect(help).toContain("--dev")
1132
- expect(help).toContain("--cost")
1133
- expect(help).toContain("--model")
1134
- expect(help).toContain("--open-questions")
1135
- expect(help).toContain("--verbose")
1136
- expect(help).toContain("--version, -V")
1137
- expect(help).toContain("--help, -h")
1138
- expect(help).not.toContain("review <commitish>")
1139
- expect(help).not.toContain("format <file>")
1140
- expect(help).not.toContain("--debug")
1141
- expect(help).not.toContain("bin/gtd")
1142
- expect(help).not.toContain("(no command), loop")
1143
- expect(help).not.toContain("--dispatch")
1144
- expect(help).not.toContain("--if-resting")
1145
- expect(help).not.toMatch(/^ {2}serve\b/m)
1153
+ expect(help).toContain("-V, --version")
1154
+ expect(help).toContain("-h, --help")
1155
+ expect(help).toContain("Examples:")
1156
+ for (const removed of [
1157
+ "--workflow",
1158
+ "Older spellings",
1159
+ "visualize",
1160
+ ".gtdrc",
1161
+ "--no-open",
1162
+ "--debug",
1163
+ "--dispatch",
1164
+ "bin/gtd",
1165
+ ]) {
1166
+ expect(help).not.toContain(removed)
1167
+ }
1168
+ expect(help).not.toMatch(/^ {2}(init|serve|install|door|doors)\b/m)
1146
1169
  expect(help).toMatch(/\n$/)
1147
1170
  })
1148
1171
 
1149
- it("every flag row's name appears in the Options: block and vice versa", () => {
1150
- const help = renderHelp()
1151
- const optionsBlock = help.slice(help.indexOf("Options:"))
1152
- for (const name of FLAG_NAMES) expect(optionsBlock).toContain(name)
1172
+ it("keeps every line of every help text within 80 columns", () => {
1173
+ for (const text of [renderHelp(), ...listedCommands().map((name) => helpFor("help", name))]) {
1174
+ for (const line of text.split("\n")) expect(line.length, line).toBeLessThanOrEqual(80)
1175
+ }
1176
+ })
1177
+
1178
+ it("names no internals", () => {
1179
+ const all = [renderHelp(), ...listedCommands().map((name) => helpFor("help", name))].join("")
1180
+ for (const internal of ["it.processCost", "refs/worktree/gtd/history", "Effect Schema"]) {
1181
+ expect(all).not.toContain(internal)
1182
+ }
1183
+ })
1184
+
1185
+ it("every flag is in the help of a command it is valid for", () => {
1186
+ const all = [renderHelp(), ...listedCommands().map((name) => helpFor("help", name))].join("")
1187
+ for (const name of FLAG_NAMES) expect(all).toContain(name)
1188
+ })
1189
+
1190
+ it("gtd <cmd> --help, gtd <cmd> -h and gtd help <cmd> print that command's own help", () => {
1191
+ const help = helpFor("help", "land")
1192
+ expect(help).toMatch(/^Usage: gtd land \[options\]\n/)
1193
+ expect(help).toContain("--cost=<n>")
1194
+ expect(help).not.toContain("--port")
1195
+ expect(helpFor("land", "--help")).toBe(help)
1196
+ expect(helpFor("land", "-h")).toBe(help)
1197
+ })
1198
+
1199
+ it("a two-word command's help is its own", () => {
1200
+ expect(helpFor("judge", "run", "--help")).toMatch(/^Usage: gtd judge run \[options\]/)
1201
+ expect(helpFor("help", "history", "show")).toMatch(/^Usage: gtd history show <ref>/)
1202
+ })
1203
+
1204
+ it("a flag that means something else per command is described per command", () => {
1205
+ expect(helpFor("help", "land")).toContain("With --cost: the model")
1206
+ expect(helpFor("help", "land")).toContain("nothing runs on it")
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
+ })
1211
+
1212
+ it("--help after an unknown command still prints the overview", () => {
1213
+ expect(helpFor("bogus", "--help")).toBe(renderHelp())
1214
+ })
1215
+
1216
+ it("gtd help <unknown> is a usage error naming the nearest command", () => {
1217
+ const plan = parseArgv(["node", "gtd.js", "help", "lnad"])
1218
+ expect(plan).toEqual({
1219
+ kind: "usage",
1220
+ stdout: "",
1221
+ message:
1222
+ "gtd: help: no command 'lnad' — did you mean `gtd help land`? Run `gtd help` for every command",
1223
+ json: false,
1224
+ })
1153
1225
  })
1154
1226
 
1155
1227
  it("the docs/cli.md Commands fenced block equals renderHelp()", () => {
@@ -1159,6 +1231,15 @@ describe("renderHelp", () => {
1159
1231
  expect(match![1] + "\n").toBe(renderHelp())
1160
1232
  })
1161
1233
 
1234
+ it("docs/cli.md's Every command section is every command's help, in the overview's order", () => {
1235
+ const doc = readFileSync(resolve(import.meta.dirname, "../../docs/cli.md"), "utf8")
1236
+ const section = doc.slice(doc.indexOf("\n## Every command\n"))
1237
+ const documented = [...section.matchAll(/^### `gtd ([^`]+)`\n\n```\n([\s\S]*?)```\n/gm)].map(
1238
+ ([, name, help]) => [name, help],
1239
+ )
1240
+ expect(documented).toEqual(listedCommands().map((name) => [name, helpFor("help", name)]))
1241
+ })
1242
+
1162
1243
  it("README.md and COMPATIBILITY.md list exactly the commands", () => {
1163
1244
  const commands = new Set(
1164
1245
  [...renderHelp().matchAll(/^ {2}([a-z]+(?:-[a-z]+)?(?: [a-z]+)?)(?= )/gm)].map(
@@ -1280,17 +1361,18 @@ describe("runCli — a garbled GTD_FLAG_* value", () => {
1280
1361
  })
1281
1362
 
1282
1363
  describe("runCli — StartFlags", () => {
1283
- const CONFIG = `import { agent, human } from "@pmelab/gtd/flows"
1364
+ const CONFIG = `import { agent, human, workflow } from "@pmelab/gtd/flows"
1284
1365
 
1285
1366
  export default async () => {
1286
1367
  await human("idle")
1287
1368
  }
1288
1369
 
1289
- export const side = async () => {
1290
- await agent("working", "w")
1291
- }
1292
-
1293
- 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
+ )
1294
1376
  `
1295
1377
  const BETA = { name: "beta", owner: "pmelab", expiry: "2099-01-01", kind: "release" } as const
1296
1378
  const start = async (
@@ -1302,7 +1384,7 @@ export const defaults = { flag_beta: "0" }
1302
1384
  repo.seedCommit("chore: add workflow")
1303
1385
  const { io, result } = makeCapturingCliIo(repo, env)
1304
1386
  await Effect.runPromise(
1305
- runCli(["node", "gtd.js", "--workflow", "side"], { ...io, ...(flags ? { flags } : {}) }),
1387
+ runCli(["node", "gtd.js", "start", "side"], { ...io, ...(flags ? { flags } : {}) }),
1306
1388
  )
1307
1389
  return result().stdout
1308
1390
  }
@@ -1419,7 +1501,7 @@ describe("runCli — stdout stays byte-empty on every failing surface", () => {
1419
1501
  const { stdout, stderr, exitCode } = captured()
1420
1502
  expect(exitCode).toBe(EXIT_RUNTIME_ERROR)
1421
1503
  expect(stdout).toBe("")
1422
- expect(stderr).toContain("gtd requires a repository with at least one commit")
1504
+ expect(stderr).toContain("gtd: the repository has no commits yet")
1423
1505
  })
1424
1506
 
1425
1507
  it("runtime error: a handler's own Effect.fail (not the shared repo-root/commit guard) leaves stdout byte-empty", async () => {