@pmelab/gtd 59.0.1 → 60.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +37 -33
  3. package/claude/hooks/doors.ts +8 -4
  4. package/dist/chunks/flows-DENt0uWV.mjs +707 -0
  5. package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
  6. package/dist/chunks/{testkit-07Z_ENch.mjs → testkit-BvecVjQ0.mjs} +570 -390
  7. package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
  8. package/dist/flows.mjs +2 -676
  9. package/dist/gtd.bundle.mjs +751 -645
  10. package/dist/steering.mjs +2 -2
  11. package/dist/testkit/gherkin.mjs +245 -36
  12. package/dist/testkit.mjs +3 -2
  13. package/dist/web/main.js +234 -232
  14. package/dist/wire.mjs +2 -2
  15. package/dist/workflow-kit.mjs +3 -3
  16. package/package.json +1 -1
  17. package/skills/authoring/SKILL.md +66 -60
  18. package/src/cli/Cli.test.ts +183 -182
  19. package/src/cli/Cli.ts +164 -186
  20. package/src/cli/History.ts +9 -4
  21. package/src/cli/Judges.ts +1 -1
  22. package/src/cli/Output.test.ts +35 -0
  23. package/src/cli/Output.ts +31 -1
  24. package/src/cli/Program.test.ts +65 -79
  25. package/src/cli/Program.ts +157 -118
  26. package/src/edge/Edge.test.ts +64 -56
  27. package/src/edge/Edge.ts +3 -3
  28. package/src/flows/factory.test.ts +14 -0
  29. package/src/flows/factory.ts +22 -5
  30. package/src/flows/helpers.test.ts +35 -0
  31. package/src/flows/helpers.ts +19 -10
  32. package/src/flows/index.test.ts +66 -0
  33. package/src/flows/index.ts +109 -5
  34. package/src/flows/internal.ts +7 -0
  35. package/src/flows/runtime.test.ts +192 -1
  36. package/src/flows/runtime.ts +188 -54
  37. package/src/gherkin/git-control.feature +6 -6
  38. package/src/gherkin/index.test.ts +127 -0
  39. package/src/gherkin/index.ts +370 -51
  40. package/src/gherkin/messages.test.ts +38 -4
  41. package/src/gherkin/messages.ts +21 -4
  42. package/src/gherkin/more-steps.feature +170 -0
  43. package/src/gherkin/scenario-end.feature +101 -0
  44. package/src/gherkin/testkit.feature +83 -7
  45. package/src/install/Install.test.ts +7 -7
  46. package/src/install/Install.ts +8 -8
  47. package/src/replay/Replay.test.ts +13 -2
  48. package/src/replay/Replay.ts +19 -30
  49. package/src/replay/judgeDiffShape.test.ts +1 -1
  50. package/src/steering/Footnotes.test.ts +1 -1
  51. package/src/steering/SteeringFormats.test.ts +1 -1
  52. package/src/steering/freeform.test.ts +1 -1
  53. package/src/steering/index.test.ts +27 -1
  54. package/src/steering/index.ts +6 -104
  55. package/src/steering/internal.ts +11 -0
  56. package/src/steering/qa.test.ts +7 -2
  57. package/src/steering/reads.test.ts +42 -0
  58. package/src/steering/reads.ts +43 -0
  59. package/src/steering/registry.ts +93 -0
  60. package/src/steering/review.test.ts +2 -2
  61. package/src/step/planStart.test.ts +18 -16
  62. package/src/step/planStart.ts +9 -9
  63. package/src/testkit/TestRepo.test.ts +245 -7
  64. package/src/testkit/TestRepo.ts +271 -40
  65. package/src/testkit/imports.test.ts +58 -0
  66. package/src/testkit/imports.ts +102 -0
  67. package/src/testkit/index.test.ts +28 -0
  68. package/src/testkit/index.ts +14 -2
  69. package/src/twins/EmittedScriptRecognizer.ts +2 -6
  70. package/src/twins/EmittedScripts.test.ts +4 -2
  71. package/src/twins/InMemRepo.ts +1 -0
  72. package/src/wire/Script.ts +11 -0
  73. package/src/wire/WireSchema.test.ts +3 -0
  74. package/src/wire/golden/1/script.json +4 -0
  75. package/src/wire/index.ts +1 -0
  76. package/src/workflow/SteeringFormats.test.ts +1 -1
  77. package/src/workflow/compile.ts +2 -2
  78. package/src/workflow/discovery.test.ts +3 -3
  79. package/src/workflow/load.test.ts +256 -116
  80. package/src/workflow/load.ts +169 -146
  81. package/src/workflow-kit/index.test.ts +25 -0
  82. package/src/workflow-kit/index.ts +37 -4
  83. package/src/workflow-kit/review.ts +2 -2
@@ -70,7 +70,7 @@ import {
70
70
  unansweredQuestions,
71
71
  formatFinding,
72
72
  type SteeringFormat,
73
- } from "../steering/index.js"
73
+ } from "../steering/internal.js"
74
74
  import {
75
75
  resolveMode,
76
76
  validateScriptFor,
@@ -91,7 +91,10 @@ import {
91
91
  import { renderBriefing, WORKFLOWS_REPO } from "../install/index.js"
92
92
  import { ToolCommands, type SelfGtd } from "./ToolCommands.js"
93
93
  import {
94
+ normalizeScriptNewline,
94
95
  SelectorUsageError,
96
+ writeJson,
97
+ writeScript,
95
98
  writeSelection,
96
99
  type ArtifactOut,
97
100
  type CommandRequirements,
@@ -139,13 +142,11 @@ export type Command =
139
142
  }
140
143
  | {
141
144
  readonly kind: "start"
142
- readonly actor: string
143
- readonly workflow: string
145
+ /** A door or a workflow; absent lists both. */
146
+ readonly name?: string
147
+ readonly args: readonly string[]
144
148
  readonly vars: Readonly<Record<string, string>>
145
- readonly label: string
146
149
  }
147
- | { readonly kind: "door"; readonly name?: string; readonly args: readonly string[] }
148
- | { readonly kind: "doors" }
149
150
  | { readonly kind: "abandon" }
150
151
  | { readonly kind: "restore" }
151
152
  | { readonly kind: "next" }
@@ -159,7 +160,7 @@ export type Command =
159
160
  readonly openThreads?: boolean
160
161
  }
161
162
  | { readonly kind: "uncheck"; readonly file: string }
162
- | { readonly kind: "install" }
163
+ | { readonly kind: "guide" }
163
164
  | { readonly kind: "summary" }
164
165
  | { readonly kind: "base" }
165
166
  | { readonly kind: "judge" }
@@ -211,7 +212,7 @@ const runLspCommand = (): Effect.Effect<void, Error, ToolCommands> =>
211
212
  * gtd ships no workflow, so where no `gtd.config.ts` governs it says where to
212
213
  * get one; it writes nothing.
213
214
  */
214
- const runInstallCommand = (
215
+ const runGuideCommand = (
215
216
  out: ArtifactOut,
216
217
  ): Effect.Effect<void, Error, GitService | Host | ConfigDiscovery | Narrator> =>
217
218
  Effect.gen(function* () {
@@ -223,7 +224,7 @@ const runInstallCommand = (
223
224
  if ((yield* host.realPath(topLevel.right)) !== (yield* host.realPath(host.root))) return
224
225
  if ((yield* (yield* ConfigDiscovery).workflowModule(host.root, host.home)) !== undefined) return
225
226
  yield* (yield* Narrator).warn(
226
- `gtd install: no ${WORKFLOW_MODULE} governs here, and gtd ships no workflow — get one from ${WORKFLOWS_REPO} or write your own (step 2 of the briefing above)`,
227
+ `gtd guide: no ${WORKFLOW_MODULE} governs here, and gtd ships no workflow — get one from ${WORKFLOWS_REPO} or write your own (step 2 of the briefing above)`,
227
228
  )
228
229
  })
229
230
 
@@ -244,20 +245,10 @@ interface LandResult {
244
245
  readonly idle: boolean
245
246
  }
246
247
 
247
- /**
248
- * Exactly one trailing newline — duplicated from `Cli.ts`'s own
249
- * `normalizeTrailingNewline` rather than imported, to avoid a circular value
250
- * dependency (`Cli.ts` already depends on this module). `LandResult.script`
251
- * must carry its own trailing newline because it's embedded verbatim inside
252
- * the `--json` document, never re-normalized at its tail.
253
- */
254
- const normalizeScriptNewline = (script: string): string =>
255
- script.length === 0 ? script : `${script.replace(/\n+$/, "")}\n`
256
-
257
248
  /**
258
249
  * `combinedScript`, narrowed to accept only an already-guarded
259
250
  * `RunnableScript` for `required` — the one boundary `ScriptSurface`'s brand
260
- * exists to protect: `gtd land`/`gtd --workflow`'s emitted script. Every OTHER
251
+ * exists to protect: `gtd land`/`gtd start`'s emitted script. Every OTHER
261
252
  * `combinedScript` call (`abandon`/`restore`/steering validate, below) builds
262
253
  * from `emitScripts` directly and keeps the unbranded signature — those
263
254
  * paths land no guard-gated git write for a brand to protect.
@@ -425,6 +416,20 @@ const readStdin = (): Effect.Effect<string, Error> =>
425
416
  catch: (e) => (e instanceof Error ? e : new Error(String(e))),
426
417
  })
427
418
 
419
+ /** A stdin that is no judgment is the caller's mistake, a usage error like `gtd judge answer`'s. */
420
+ const judgmentShape = (input: string): Effect.Effect<void, GtdUsageError> => {
421
+ let doc: unknown
422
+ try {
423
+ doc = JSON.parse(input)
424
+ } catch {
425
+ return Effect.fail(new GtdUsageError("gtd judge run: stdin is not valid JSON"))
426
+ }
427
+ const questions = (doc as { questions?: unknown } | null)?.questions
428
+ return Array.isArray(questions)
429
+ ? Effect.void
430
+ : Effect.fail(new GtdUsageError('gtd judge run: stdin has no "questions" array'))
431
+ }
432
+
428
433
  /** Stdout is written once, after the answerer resolves — a rejection leaves it empty. */
429
434
  const runJudgeRunCommand = (
430
435
  command: Extract<Command, { kind: "judgeRun" }>,
@@ -435,6 +440,7 @@ const runJudgeRunCommand = (
435
440
  const tools = yield* ToolCommands
436
441
  yield* refuseLegacyRc
437
442
  const input = yield* readStdin()
443
+ yield* judgmentShape(input)
438
444
  const verdict = yield* Effect.tryPromise({
439
445
  try: () =>
440
446
  tools.judge({
@@ -556,10 +562,10 @@ const runLandCommand = (
556
562
  * `base` renders to something that is not an ancestor of (and differs from)
557
563
  * HEAD.
558
564
  */
559
- const runStartCommand = (
560
- actor: string,
565
+ const startWorkflow = (
561
566
  workflow: string,
562
567
  varOverrides: Record<string, string>,
568
+ json: JsonMode,
563
569
  out: ArtifactOut,
564
570
  commandLabel: string,
565
571
  ): Effect.Effect<void, Error, CommandRequirements> =>
@@ -572,7 +578,7 @@ const runStartCommand = (
572
578
  ? `startable: ${config.workflowNames.join(", ")}`
573
579
  : "this config exports no workflow besides its default, which an ordinary start runs"
574
580
  return yield* Effect.fail(
575
- new GtdUsageError(`gtd --workflow: unknown workflow "${workflow}" — ${startable}`),
581
+ new GtdUsageError(`${commandLabel}: unknown workflow "${workflow}" — ${startable}`),
576
582
  )
577
583
  }
578
584
  const rest = yield* restAt(undefined, varOverrides)
@@ -582,7 +588,7 @@ const runStartCommand = (
582
588
  state: rest.state,
583
589
  idle: noProcessUnderway(rest),
584
590
  },
585
- actor,
591
+ "human",
586
592
  {
587
593
  workflow: resolved,
588
594
  first: started,
@@ -594,7 +600,11 @@ const runStartCommand = (
594
600
  if (plan.kind === "refusal") {
595
601
  return yield* Effect.fail(new Error(plan.message))
596
602
  }
597
- out.write(listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes))
603
+ yield* writeScript(
604
+ json,
605
+ out,
606
+ listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes),
607
+ )
598
608
  })
599
609
 
600
610
  /**
@@ -618,40 +628,23 @@ const doorSynopsis = (name: string, door: Door): string =>
618
628
  ...(door.args ?? []).map((arg) => (arg.optional === true ? `[${arg.name}]` : `<${arg.name}>`)),
619
629
  ].join(" ")
620
630
 
621
- const noSuchDoor = (name: string | undefined, doors: ReadonlyMap<string, Door>): GtdUsageError => {
622
- const listing =
623
- doors.size > 0
624
- ? `doors: ${[...doors].map(([n, d]) => doorSynopsis(n, d)).join(", ")}`
625
- : "this config has no doors"
626
- return new GtdUsageError(
627
- name === undefined
628
- ? `gtd door: missing door name — ${listing}`
629
- : `gtd door: unknown door "${name}" — ${listing}`,
630
- )
631
- }
632
-
633
- /**
634
- * `gtd door <name> [args…]`: `gtd --workflow` with the door's workflow and the
635
- * process settings its `vars` derive from the positional args. A wrong name or
636
- * arity is a usage error naming what would have worked.
637
- */
638
- const runDoorCommand = (
639
- name: string | undefined,
631
+ /** A door's start: its args mapped to process settings, `extra` (`--var`) pinned on top. */
632
+ const startThroughDoor = (
633
+ name: string,
634
+ door: Door,
640
635
  args: readonly string[],
636
+ extra: Readonly<Record<string, string>>,
637
+ json: JsonMode,
641
638
  out: ArtifactOut,
642
639
  ): Effect.Effect<void, Error, CommandRequirements> =>
643
640
  Effect.gen(function* () {
644
- const config = yield* (yield* ConfigService).load
645
- const door = name === undefined ? undefined : config.doors.get(name)
646
- if (name === undefined || door === undefined) {
647
- return yield* Effect.fail(noSuchDoor(name, config.doors))
648
- }
641
+ const label = `gtd start ${name}`
649
642
  const declared = door.args ?? []
650
643
  const required = declared.filter((arg) => arg.optional !== true).length
651
644
  if (args.length < required || args.length > declared.length) {
652
645
  return yield* Effect.fail(
653
646
  new GtdUsageError(
654
- `gtd door ${name}: ${args.length < required ? "missing" : "too many"} arguments — usage: gtd door ${doorSynopsis(name, door)}`,
647
+ `${label}: ${args.length < required ? "missing" : "too many"} arguments — usage: gtd start ${doorSynopsis(name, door)}`,
655
648
  ),
656
649
  )
657
650
  }
@@ -661,35 +654,81 @@ const runDoorCommand = (
661
654
  })
662
655
  const vars = yield* Effect.try({
663
656
  try: () => door.vars?.(named) ?? {},
664
- catch: (e) => new Error(`gtd door ${name}: ${e instanceof Error ? e.message : String(e)}`),
657
+ catch: (e) => new Error(`${label}: ${e instanceof Error ? e.message : String(e)}`),
665
658
  })
666
- yield* runStartCommand("human", door.workflow, { ...vars }, out, `gtd door ${name}`)
659
+ yield* startWorkflow(door.workflow, { ...vars, ...extra }, json, out, label)
667
660
  })
668
661
 
669
- /** `gtd doors`: every door and its args, sorted by name — plain lines, or one JSON array. */
670
- const runDoorsCommand = (
662
+ /**
663
+ * `gtd start <name> [args…]`: a door when `<name>` is one — so a door shadows
664
+ * the workflow it shares a name with — else a workflow, which takes no args.
665
+ * No name lists both.
666
+ */
667
+ const runStartCommand = (
668
+ command: Extract<Command, { kind: "start" }>,
671
669
  json: JsonMode,
672
670
  out: ArtifactOut,
673
671
  ): Effect.Effect<void, Error, CommandRequirements> =>
674
672
  Effect.gen(function* () {
673
+ const { name, args, vars } = command
674
+ if (name === undefined) return yield* runStartListCommand(json, out)
675
+ const label = `gtd start ${name}`
675
676
  const config = yield* (yield* ConfigService).load
677
+ const door = config.doors.get(name)
678
+ if (door !== undefined) {
679
+ return yield* startThroughDoor(name, door, args, vars, json, out)
680
+ }
681
+ if (config.workflowNamed(name) === undefined) {
682
+ return yield* Effect.fail(
683
+ new GtdUsageError(
684
+ `${label}: no workflow or door is named "${name}" — run \`gtd start\` to list them`,
685
+ ),
686
+ )
687
+ }
688
+ if (args.length > 0) {
689
+ return yield* Effect.fail(
690
+ new GtdUsageError(
691
+ `${label}: too many arguments — a workflow takes none, only --var <name>=<value>; got: ${args.join(", ")}`,
692
+ ),
693
+ )
694
+ }
695
+ yield* startWorkflow(name, { ...vars }, json, out, label)
696
+ })
697
+
698
+ /**
699
+ * Bare `gtd start`: every workflow startable by name, then every door — plain
700
+ * lines, or one JSON array. A workflow a door shadows is left out: its name
701
+ * opens the door.
702
+ */
703
+ const runStartListCommand = (
704
+ json: JsonMode,
705
+ out: ArtifactOut,
706
+ ): Effect.Effect<void, Error, CommandRequirements> =>
707
+ Effect.gen(function* () {
708
+ const config = yield* (yield* ConfigService).load
709
+ const workflows = [...config.workflowNames].filter((name) => !config.doors.has(name)).sort()
676
710
  const doors = [...config.doors]
677
- if (json.kind === "off") {
678
- if (doors.length === 0) {
679
- yield* (yield* Narrator).warn(
680
- "gtd doors: this config has no doors — a workflow factory's `doors` option or the config's `doors` export adds one",
681
- )
682
- }
683
- out.write(doors.map(([n, d]) => `${doorSynopsis(n, d)} → ${d.workflow}\n`).join(""))
684
- return
711
+ const document = [
712
+ ...workflows.map((name) => ({ kind: "workflow", name })),
713
+ ...doors.map(([name, door]) => ({
714
+ kind: "door",
715
+ name,
716
+ workflow: door.workflow,
717
+ args: (door.args ?? []).map((arg) => ({ name: arg.name, optional: arg.optional === true })),
718
+ })),
719
+ ]
720
+ if (yield* writeJson(json, out, document)) return
721
+ if (document.length === 0) {
722
+ yield* (yield* Narrator).warn(
723
+ "gtd start: this config exports no workflow besides its default, which an ordinary start runs, and no door",
724
+ )
685
725
  }
686
- const document = doors.map(([n, d]) => ({
687
- name: n,
688
- workflow: d.workflow,
689
- args: (d.args ?? []).map((arg) => ({ name: arg.name, optional: arg.optional === true })),
690
- }))
691
- if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`)
692
- else yield* writeSelection(out, document, json.path)
726
+ out.write(
727
+ [
728
+ ...workflows.map((name) => `workflow ${name}\n`),
729
+ ...doors.map(([n, d]) => `door ${doorSynopsis(n, d)} → ${d.workflow}\n`),
730
+ ].join(""),
731
+ )
693
732
  })
694
733
 
695
734
  /**
@@ -705,7 +744,10 @@ const runDoorsCommand = (
705
744
  * Idempotent at the initial state. The one refusal is a process whose first
706
745
  * commit is the repository's root commit: nothing earlier to rewind to.
707
746
  */
708
- const runAbandonCommand = (out: ArtifactOut): Effect.Effect<void, Error, CommandRequirements> =>
747
+ const runAbandonCommand = (
748
+ json: JsonMode,
749
+ out: ArtifactOut,
750
+ ): Effect.Effect<void, Error, CommandRequirements> =>
709
751
  Effect.gen(function* () {
710
752
  const git = yield* GitService
711
753
  const config = yield* (yield* ConfigService).load
@@ -716,8 +758,7 @@ const runAbandonCommand = (out: ArtifactOut): Effect.Effect<void, Error, Command
716
758
  const required = emitScripts([
717
759
  { kind: "outcome", command: abandonNoopOutcome(initial) },
718
760
  ]).required
719
- out.write(combinedScript(required, ""))
720
- return
761
+ return yield* writeScript(json, out, combinedScript(required, ""))
721
762
  }
722
763
  const restState = run.trace[run.trace.length - 1]!.state
723
764
 
@@ -738,7 +779,7 @@ const runAbandonCommand = (out: ArtifactOut): Effect.Effect<void, Error, Command
738
779
  { kind: "outcome", command: abandonedOutcome(restState, run.startParentHash, initial) },
739
780
  ]
740
781
  const required = emitScripts(steps).required
741
- out.write(combinedScript(required, ""))
782
+ yield* writeScript(json, out, combinedScript(required, ""))
742
783
  })
743
784
 
744
785
  /**
@@ -748,7 +789,10 @@ const runAbandonCommand = (out: ArtifactOut): Effect.Effect<void, Error, Command
748
789
  * refuses on a dirty working tree, no retained history, or HEAD having
749
790
  * advanced past the retained tip with commits that would be lost.
750
791
  */
751
- const runRestoreCommand = (out: ArtifactOut): Effect.Effect<void, Error, CommandRequirements> =>
792
+ const runRestoreCommand = (
793
+ json: JsonMode,
794
+ out: ArtifactOut,
795
+ ): Effect.Effect<void, Error, CommandRequirements> =>
752
796
  Effect.gen(function* () {
753
797
  const git = yield* GitService
754
798
 
@@ -790,7 +834,7 @@ const runRestoreCommand = (out: ArtifactOut): Effect.Effect<void, Error, Command
790
834
  { kind: "outcome", command: restoredOutcome(tip, after.state) },
791
835
  ]
792
836
  const required = emitScripts(steps).required
793
- out.write(combinedScript(required, ""))
837
+ yield* writeScript(json, out, combinedScript(required, ""))
794
838
  })
795
839
 
796
840
  /**
@@ -890,14 +934,18 @@ const runNextCommand = (
890
934
  })
891
935
 
892
936
  /** Emits the format-then-validate script for the driver to run; reads no file and executes nothing itself. The verdict lives in that script's future exit code, which prints the complete fix prompt rather than bare findings. */
893
- const runValidateCommand = (out: ArtifactOut): Effect.Effect<void, Error, CommandRequirements> =>
937
+ const runValidateCommand = (
938
+ json: JsonMode,
939
+ out: ArtifactOut,
940
+ ): Effect.Effect<void, Error, CommandRequirements> =>
894
941
  Effect.gen(function* () {
895
942
  const rest = yield* currentRest
896
- const resolved = yield* resolveValidateScript(rest)
897
- out.write(
898
- resolved !== undefined && resolved.script.length > 0
899
- ? `${resolved.script}\n`
900
- : `nothing to validate at "${rest.state}"\n`,
943
+ const script = (yield* resolveValidateScript(rest))?.script ?? ""
944
+ yield* writeScript(
945
+ json,
946
+ out,
947
+ script,
948
+ script.length > 0 ? `${script}\n` : `nothing to validate at "${rest.state}"\n`,
901
949
  )
902
950
  })
903
951
 
@@ -906,8 +954,10 @@ const runValidateCommand = (out: ArtifactOut): Effect.Effect<void, Error, Comman
906
954
  * Resolves no workflow state and reads no config — this is what a seeded
907
955
  * `validate:` command invokes as a leaf step, and it formats nothing.
908
956
  *
909
- * An absent file exits 0, like `gtd validate`. Findings ride the failing
910
- * Effect's message, so the shared refusal path reports them on stderr.
957
+ * An unknown mode is a usage error; an absent file is a refusal (the emitted
958
+ * validate script guards on the file's presence before it calls this).
959
+ * Findings ride the failing Effect's message, so the shared refusal path
960
+ * reports them on stderr.
911
961
  */
912
962
  const runCheckCommand = (
913
963
  mode: string,
@@ -919,7 +969,7 @@ const runCheckCommand = (
919
969
  const format = steeringFormatFor(mode)
920
970
  if (format === undefined) {
921
971
  return yield* Effect.fail(
922
- new Error(
972
+ new GtdUsageError(
923
973
  `gtd check: unknown mode "${mode}" — known modes: ${BUILT_IN_MODE_NAMES.join(", ")}`,
924
974
  ),
925
975
  )
@@ -953,12 +1003,8 @@ const runCheckCommand = (
953
1003
  return yield* runOpenQuestionsCheckCommand(format, file)
954
1004
  }
955
1005
 
956
- const workspace = yield* Workspace
957
- const content = workspace.atPath(file)
958
- const errors =
959
- content !== undefined
960
- ? format.validate(content).map((finding) => formatFinding(file, finding))
961
- : []
1006
+ const content = yield* existingFile(file)
1007
+ const errors = format.validate(content).map((finding) => formatFinding(file, finding))
962
1008
 
963
1009
  if (errors.length === 0) return
964
1010
 
@@ -970,6 +1016,14 @@ const runCheckCommand = (
970
1016
  )
971
1017
  })
972
1018
 
1019
+ const existingFile = (file: string): Effect.Effect<string, Error, Workspace> =>
1020
+ Effect.flatMap(Workspace, (workspace) => {
1021
+ const content = workspace.atPath(file)
1022
+ return content === undefined
1023
+ ? Effect.fail(new Error(`gtd check: no such file: ${file}`))
1024
+ : Effect.succeed(content)
1025
+ })
1026
+
973
1027
  /**
974
1028
  * The bare `gtd check --open-threads`: code-comment threads in the files the
975
1029
  * current process changed. The one `check` form that reads process state; no
@@ -997,18 +1051,13 @@ const runCodeThreadsCheckCommand = (): Effect.Effect<void, Error, CommandRequire
997
1051
  return yield* Effect.fail(new Error(lines.join("\n")))
998
1052
  })
999
1053
 
1000
- /** Prints one `file:line: [^name]: <first H entry>` line per open thread; a missing file fails, like `--open-questions`. */
1054
+ /** Prints one `file:line: [^name]: <first H entry>` line per open thread. */
1001
1055
  const runOpenThreadsCheckCommand = (
1002
1056
  format: SteeringFormat,
1003
1057
  file: string,
1004
1058
  ): Effect.Effect<void, Error, Workspace> =>
1005
1059
  Effect.gen(function* () {
1006
- const workspace = yield* Workspace
1007
- const content = workspace.atPath(file)
1008
- if (content === undefined) {
1009
- return yield* Effect.fail(new Error(`gtd check: ${file} does not exist`))
1010
- }
1011
-
1060
+ const content = yield* existingFile(file)
1012
1061
  const errors = openThreads(format, content).map(
1013
1062
  (t) => `${file}:${t.line + 1}: [^${t.name}]: ${t.firstMe}`,
1014
1063
  )
@@ -1021,20 +1070,14 @@ const runOpenThreadsCheckCommand = (
1021
1070
 
1022
1071
  /**
1023
1072
  * Runs the SAME `unansweredQuestions` predicate a flow's `openQuestions()`
1024
- * reads, so the gate script and the flow's own check stay in sync. A missing
1025
- * file is a non-zero exit here, unlike the structural path above.
1073
+ * reads, so the gate script and the flow's own check stay in sync.
1026
1074
  */
1027
1075
  const runOpenQuestionsCheckCommand = (
1028
1076
  format: SteeringFormat,
1029
1077
  file: string,
1030
1078
  ): Effect.Effect<void, Error, Workspace> =>
1031
1079
  Effect.gen(function* () {
1032
- const workspace = yield* Workspace
1033
- const content = workspace.atPath(file)
1034
- if (content === undefined) {
1035
- return yield* Effect.fail(new Error(`gtd check: ${file} does not exist`))
1036
- }
1037
-
1080
+ const content = yield* existingFile(file)
1038
1081
  const errors = unansweredQuestions(format, content).map(
1039
1082
  (q) => `${file}:${q.headingLine + 1}: ${q.question}`,
1040
1083
  )
@@ -1208,7 +1251,7 @@ const assertRepositoryHasCommits = (git: GitOperations): Effect.Effect<void, Err
1208
1251
  export const needsOf = (kind: Command["kind"]): Needs => {
1209
1252
  switch (kind) {
1210
1253
  case "lsp":
1211
- case "install":
1254
+ case "guide":
1212
1255
  case "judgeRun":
1213
1256
  case "strays":
1214
1257
  return "none"
@@ -1225,7 +1268,7 @@ export const standaloneKinds = (): readonly Command["kind"][] => [
1225
1268
  "lsp",
1226
1269
  "check",
1227
1270
  "uncheck",
1228
- "install",
1271
+ "guide",
1229
1272
  "judgeRun",
1230
1273
  "strays",
1231
1274
  ]
@@ -1266,19 +1309,15 @@ const dispatchVoidCommand = (
1266
1309
  out,
1267
1310
  )
1268
1311
  case "start":
1269
- return runStartCommand(command.actor, command.workflow, command.vars, out, command.label)
1270
- case "door":
1271
- return runDoorCommand(command.name, command.args, out)
1272
- case "doors":
1273
- return runDoorsCommand(json, out)
1312
+ return runStartCommand(command, json, out)
1274
1313
  case "abandon":
1275
- return runAbandonCommand(out)
1314
+ return runAbandonCommand(json, out)
1276
1315
  case "restore":
1277
- return runRestoreCommand(out)
1316
+ return runRestoreCommand(json, out)
1278
1317
  case "next":
1279
1318
  return runNextCommand(json, out)
1280
1319
  case "validate":
1281
- return runValidateCommand(out)
1320
+ return runValidateCommand(json, out)
1282
1321
  case "check":
1283
1322
  if (command.mode === undefined || command.file === undefined) {
1284
1323
  return runCodeThreadsCheckCommand()
@@ -1291,8 +1330,8 @@ const dispatchVoidCommand = (
1291
1330
  )
1292
1331
  case "uncheck":
1293
1332
  return runUncheckCommand(command.file)
1294
- case "install":
1295
- return runInstallCommand(out)
1333
+ case "guide":
1334
+ return runGuideCommand(out)
1296
1335
  case "summary":
1297
1336
  return runSummaryCommand(out)
1298
1337
  case "base":
@@ -1312,7 +1351,7 @@ const dispatchVoidCommand = (
1312
1351
  case "historyJudges":
1313
1352
  return runHistoryJudgesCommand(command.refs, command.vars, command.jsonl, json, out)
1314
1353
  case "historyFetch":
1315
- return runHistoryFetchCommand(command.remote, out)
1354
+ return runHistoryFetchCommand(command.remote, json, out)
1316
1355
  }
1317
1356
  }
1318
1357