@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +48 -32
- 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-BhTey1Do.mjs +30517 -0
- package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +11285 -19864
- package/dist/gtd.core.mjs +593 -0
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +246 -37
- 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 +7 -7
- package/skills/authoring/SKILL.md +66 -60
- package/src/cli/Cli.test.ts +185 -184
- package/src/cli/Cli.ts +219 -224
- 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 +34 -3
- package/src/cli/Program.test.ts +78 -84
- package/src/cli/Program.ts +192 -128
- package/src/cli/ToolCommands.ts +2 -2
- package/src/edge/Edge.test.ts +72 -62
- package/src/edge/Edge.ts +6 -6
- 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 +189 -55
- 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/install/Version.ts +9 -3
- package/src/install/index.ts +1 -1
- package/src/platform/CommandRunner.ts +13 -11
- package/src/platform/Commentary.ts +2 -2
- package/src/platform/Git.test.ts +4 -5
- package/src/platform/Git.ts +42 -38
- package/src/platform/Host.ts +1 -1
- package/src/platform/NodeServices.ts +11 -0
- package/src/platform/Workspace.test.ts +4 -5
- package/src/platform/Workspace.ts +3 -3
- package/src/platform/WorktreeState.test.ts +2 -3
- package/src/platform/WorktreeState.ts +1 -1
- package/src/platform/index.ts +2 -1
- 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 +12 -12
- 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 +5 -3
- package/src/twins/GitTiers.ts +6 -7
- package/src/twins/InMemRepo.ts +1 -0
- package/src/twins/Layers.test.ts +1 -1
- package/src/twins/Layers.ts +10 -3
- package/src/twins/cliIo.ts +9 -4
- 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 +27 -10
- package/src/workflow/discovery.ts +5 -6
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +316 -119
- package/src/workflow/load.ts +478 -305
- package/src/workflow/pin.test.ts +159 -0
- package/src/workflow/pin.ts +72 -0
- package/src/workflow/vars.ts +3 -3
- 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
- package/dist/chunks/testkit-07Z_ENch.mjs +0 -32175
package/src/cli/Cli.test.ts
CHANGED
|
@@ -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
|
|
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
|
-
["
|
|
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
|
-
["
|
|
126
|
-
["
|
|
127
|
-
["
|
|
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
|
|
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
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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("--
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
|
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
|
|
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 —
|
|
439
|
-
it("
|
|
440
|
-
const plan = parseArgv(["node", "gtd.js", "--
|
|
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", "
|
|
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", "
|
|
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
|
|
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 —
|
|
502
|
-
it("
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
expect(plan.command).toMatchObject({ kind: "start",
|
|
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
|
-
["
|
|
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
|
|
733
|
-
it("parses to
|
|
734
|
-
const plan = parseArgv(["node", "gtd.js", "
|
|
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: "
|
|
695
|
+
expect(plan.command).toEqual({ kind: "guide" })
|
|
738
696
|
expect(plan.json).toEqual({ kind: "off" })
|
|
739
697
|
}
|
|
740
698
|
})
|
|
741
699
|
|
|
742
|
-
it("
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
|
749
|
-
const plan = parseArgv(["node", "gtd.js", "
|
|
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
|
|
755
|
-
const plan = parseArgv(["node", "gtd.js", "
|
|
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
|
|
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", "--
|
|
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 --
|
|
1004
|
-
expect(
|
|
1005
|
-
|
|
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", "--
|
|
932
|
+
expect(usage(parse("history", "judges", "--threshold", "toString=0.9"))).toContain(
|
|
1008
933
|
"no judge threshold",
|
|
1009
934
|
)
|
|
1010
|
-
|
|
1011
|
-
"
|
|
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(
|
|
1015
|
-
"
|
|
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
|
|
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")
|
|
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", "
|
|
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("
|
|
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("
|
|
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 =
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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", "
|
|
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["
|
|
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")
|