@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +37 -33
- 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-07Z_ENch.mjs → testkit-BvecVjQ0.mjs} +570 -390
- package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +751 -645
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +245 -36
- 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 +1 -1
- package/skills/authoring/SKILL.md +66 -60
- package/src/cli/Cli.test.ts +183 -182
- package/src/cli/Cli.ts +164 -186
- 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 +31 -1
- package/src/cli/Program.test.ts +65 -79
- package/src/cli/Program.ts +157 -118
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +3 -3
- 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 +188 -54
- 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/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 +9 -9
- 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 +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- 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 +3 -3
- package/src/workflow/load.test.ts +256 -116
- package/src/workflow/load.ts +169 -146
- 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/src/cli/Program.ts
CHANGED
|
@@ -70,7 +70,7 @@ import {
|
|
|
70
70
|
unansweredQuestions,
|
|
71
71
|
formatFinding,
|
|
72
72
|
type SteeringFormat,
|
|
73
|
-
} from "../steering/
|
|
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
|
-
|
|
143
|
-
readonly
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
657
|
+
catch: (e) => new Error(`${label}: ${e instanceof Error ? e.message : String(e)}`),
|
|
665
658
|
})
|
|
666
|
-
yield*
|
|
659
|
+
yield* startWorkflow(door.workflow, { ...vars, ...extra }, json, out, label)
|
|
667
660
|
})
|
|
668
661
|
|
|
669
|
-
/**
|
|
670
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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 = (
|
|
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
|
|
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
|
|
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 = (
|
|
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
|
|
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 = (
|
|
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
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
|
910
|
-
*
|
|
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
|
|
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
|
|
957
|
-
const
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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 "
|
|
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
|
-
"
|
|
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
|
|
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 "
|
|
1295
|
-
return
|
|
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
|
|