@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/Program.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect, Option, Schema } from "effect"
|
|
2
2
|
import {
|
|
3
|
+
GtdError,
|
|
3
4
|
GtdUsageError,
|
|
4
5
|
Narrator,
|
|
5
6
|
GitService,
|
|
@@ -15,6 +16,8 @@ import {
|
|
|
15
16
|
import {
|
|
16
17
|
WORKFLOW_MODULE,
|
|
17
18
|
ConfigDiscovery,
|
|
19
|
+
EngineCore,
|
|
20
|
+
pin,
|
|
18
21
|
refuseLegacyRc,
|
|
19
22
|
ConfigService,
|
|
20
23
|
formatDiagnostic,
|
|
@@ -70,7 +73,7 @@ import {
|
|
|
70
73
|
unansweredQuestions,
|
|
71
74
|
formatFinding,
|
|
72
75
|
type SteeringFormat,
|
|
73
|
-
} from "../steering/
|
|
76
|
+
} from "../steering/internal.js"
|
|
74
77
|
import {
|
|
75
78
|
resolveMode,
|
|
76
79
|
validateScriptFor,
|
|
@@ -88,10 +91,13 @@ import {
|
|
|
88
91
|
abandonNoopOutcome,
|
|
89
92
|
restoredOutcome,
|
|
90
93
|
} from "../emit/index.js"
|
|
91
|
-
import { renderBriefing, WORKFLOWS_REPO } from "../install/index.js"
|
|
94
|
+
import { GTD_VERSION, renderBriefing, WORKFLOWS_REPO } from "../install/index.js"
|
|
92
95
|
import { ToolCommands, type SelfGtd } from "./ToolCommands.js"
|
|
93
96
|
import {
|
|
97
|
+
normalizeScriptNewline,
|
|
94
98
|
SelectorUsageError,
|
|
99
|
+
writeJson,
|
|
100
|
+
writeScript,
|
|
95
101
|
writeSelection,
|
|
96
102
|
type ArtifactOut,
|
|
97
103
|
type CommandRequirements,
|
|
@@ -116,7 +122,7 @@ import { runHistoryMetricsCommand } from "./Metrics.js"
|
|
|
116
122
|
* SAME type structurally off `currentRest`'s own success type rather than
|
|
117
123
|
* importing the name.
|
|
118
124
|
*/
|
|
119
|
-
type Rest = Effect.
|
|
125
|
+
type Rest = Effect.Success<typeof currentRest>
|
|
120
126
|
|
|
121
127
|
export type Command =
|
|
122
128
|
| { readonly kind: "lsp" }
|
|
@@ -139,13 +145,11 @@ export type Command =
|
|
|
139
145
|
}
|
|
140
146
|
| {
|
|
141
147
|
readonly kind: "start"
|
|
142
|
-
|
|
143
|
-
readonly
|
|
148
|
+
/** A door or a workflow; absent lists both. */
|
|
149
|
+
readonly name?: string
|
|
150
|
+
readonly args: readonly string[]
|
|
144
151
|
readonly vars: Readonly<Record<string, string>>
|
|
145
|
-
readonly label: string
|
|
146
152
|
}
|
|
147
|
-
| { readonly kind: "door"; readonly name?: string; readonly args: readonly string[] }
|
|
148
|
-
| { readonly kind: "doors" }
|
|
149
153
|
| { readonly kind: "abandon" }
|
|
150
154
|
| { readonly kind: "restore" }
|
|
151
155
|
| { readonly kind: "next" }
|
|
@@ -159,7 +163,8 @@ export type Command =
|
|
|
159
163
|
readonly openThreads?: boolean
|
|
160
164
|
}
|
|
161
165
|
| { readonly kind: "uncheck"; readonly file: string }
|
|
162
|
-
| { readonly kind: "
|
|
166
|
+
| { readonly kind: "guide" }
|
|
167
|
+
| { readonly kind: "bundle" }
|
|
163
168
|
| { readonly kind: "summary" }
|
|
164
169
|
| { readonly kind: "base" }
|
|
165
170
|
| { readonly kind: "judge" }
|
|
@@ -211,22 +216,41 @@ const runLspCommand = (): Effect.Effect<void, Error, ToolCommands> =>
|
|
|
211
216
|
* gtd ships no workflow, so where no `gtd.config.ts` governs it says where to
|
|
212
217
|
* get one; it writes nothing.
|
|
213
218
|
*/
|
|
214
|
-
const
|
|
219
|
+
const runGuideCommand = (
|
|
215
220
|
out: ArtifactOut,
|
|
216
221
|
): Effect.Effect<void, Error, GitService | Host | ConfigDiscovery | Narrator> =>
|
|
217
222
|
Effect.gen(function* () {
|
|
218
223
|
const briefing = renderBriefing()
|
|
219
224
|
out.write(briefing.endsWith("\n") ? briefing : briefing + "\n")
|
|
220
225
|
const host = yield* Host
|
|
221
|
-
const topLevel = yield* Effect.
|
|
222
|
-
if (topLevel._tag === "
|
|
223
|
-
if ((yield* host.realPath(topLevel.
|
|
226
|
+
const topLevel = yield* Effect.result((yield* GitService).topLevel())
|
|
227
|
+
if (topLevel._tag === "Failure") return
|
|
228
|
+
if ((yield* host.realPath(topLevel.success)) !== (yield* host.realPath(host.root))) return
|
|
224
229
|
if ((yield* (yield* ConfigDiscovery).workflowModule(host.root, host.home)) !== undefined) return
|
|
225
230
|
yield* (yield* Narrator).warn(
|
|
226
|
-
`gtd
|
|
231
|
+
`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
232
|
)
|
|
228
233
|
})
|
|
229
234
|
|
|
235
|
+
/** Loads the workflow first, so a pin is never made of one gtd would refuse. */
|
|
236
|
+
const runBundleCommand = (
|
|
237
|
+
out: ArtifactOut,
|
|
238
|
+
): Effect.Effect<
|
|
239
|
+
void,
|
|
240
|
+
Error,
|
|
241
|
+
ConfigService | ConfigDiscovery | EngineCore | Host | Narrator | Workspace
|
|
242
|
+
> =>
|
|
243
|
+
Effect.gen(function* () {
|
|
244
|
+
yield* (yield* ConfigService).load
|
|
245
|
+
const host = yield* Host
|
|
246
|
+
const module = yield* (yield* ConfigDiscovery).workflowModule(host.root, host.home)
|
|
247
|
+
// Inside a pin, load read the pinned workflow, not a config on disk.
|
|
248
|
+
if (module === undefined) {
|
|
249
|
+
return yield* Effect.fail(new GtdError(`gtd bundle: no ${WORKFLOW_MODULE} here or above`))
|
|
250
|
+
}
|
|
251
|
+
out.write(yield* pin(module, host.root, GTD_VERSION))
|
|
252
|
+
})
|
|
253
|
+
|
|
230
254
|
/**
|
|
231
255
|
* `planLanding`'s result — a preview of what `gtd land` would do, plus the
|
|
232
256
|
* combined `script` the driver runs to do it. `script` is
|
|
@@ -244,20 +268,10 @@ interface LandResult {
|
|
|
244
268
|
readonly idle: boolean
|
|
245
269
|
}
|
|
246
270
|
|
|
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
271
|
/**
|
|
258
272
|
* `combinedScript`, narrowed to accept only an already-guarded
|
|
259
273
|
* `RunnableScript` for `required` — the one boundary `ScriptSurface`'s brand
|
|
260
|
-
* exists to protect: `gtd land`/`gtd
|
|
274
|
+
* exists to protect: `gtd land`/`gtd start`'s emitted script. Every OTHER
|
|
261
275
|
* `combinedScript` call (`abandon`/`restore`/steering validate, below) builds
|
|
262
276
|
* from `emitScripts` directly and keeps the unbranded signature — those
|
|
263
277
|
* paths land no guard-gated git write for a brand to protect.
|
|
@@ -425,6 +439,20 @@ const readStdin = (): Effect.Effect<string, Error> =>
|
|
|
425
439
|
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
426
440
|
})
|
|
427
441
|
|
|
442
|
+
/** A stdin that is no judgment is the caller's mistake, a usage error like `gtd judge answer`'s. */
|
|
443
|
+
const judgmentShape = (input: string): Effect.Effect<void, GtdUsageError> => {
|
|
444
|
+
let doc: unknown
|
|
445
|
+
try {
|
|
446
|
+
doc = JSON.parse(input)
|
|
447
|
+
} catch {
|
|
448
|
+
return Effect.fail(new GtdUsageError("gtd judge run: stdin is not valid JSON"))
|
|
449
|
+
}
|
|
450
|
+
const questions = (doc as { questions?: unknown } | null)?.questions
|
|
451
|
+
return Array.isArray(questions)
|
|
452
|
+
? Effect.void
|
|
453
|
+
: Effect.fail(new GtdUsageError('gtd judge run: stdin has no "questions" array'))
|
|
454
|
+
}
|
|
455
|
+
|
|
428
456
|
/** Stdout is written once, after the answerer resolves — a rejection leaves it empty. */
|
|
429
457
|
const runJudgeRunCommand = (
|
|
430
458
|
command: Extract<Command, { kind: "judgeRun" }>,
|
|
@@ -435,6 +463,7 @@ const runJudgeRunCommand = (
|
|
|
435
463
|
const tools = yield* ToolCommands
|
|
436
464
|
yield* refuseLegacyRc
|
|
437
465
|
const input = yield* readStdin()
|
|
466
|
+
yield* judgmentShape(input)
|
|
438
467
|
const verdict = yield* Effect.tryPromise({
|
|
439
468
|
try: () =>
|
|
440
469
|
tools.judge({
|
|
@@ -451,7 +480,7 @@ const runJudgeRunCommand = (
|
|
|
451
480
|
})
|
|
452
481
|
|
|
453
482
|
/** A verdict's own `answer` shape covers every `judge:` primitive (`noul` → boolean, `choice` → string, `score` → number) without gtd itself interpreting which one applies — that's a driver/judge-model concern, not this decode's. */
|
|
454
|
-
const VerdictAnswer = Schema.Union(Schema.String, Schema.Number, Schema.Boolean)
|
|
483
|
+
const VerdictAnswer = Schema.Union([Schema.String, Schema.Number, Schema.Boolean])
|
|
455
484
|
|
|
456
485
|
/**
|
|
457
486
|
* One `gtd judge answer` verdict entry per pending question, `{ id, answer,
|
|
@@ -464,14 +493,14 @@ const VerdictAnswer = Schema.Union(Schema.String, Schema.Number, Schema.Boolean)
|
|
|
464
493
|
const verdictSchemaFor = (ids: readonly string[]) =>
|
|
465
494
|
Schema.Array(
|
|
466
495
|
Schema.Struct({
|
|
467
|
-
id: ids.length > 0 ? Schema.
|
|
496
|
+
id: ids.length > 0 ? Schema.Literals(ids) : Schema.Never,
|
|
468
497
|
answer: VerdictAnswer,
|
|
469
498
|
// Bounded [0, 1] — every OTHER bound in the routes: matching path fails
|
|
470
499
|
// CLOSED (a non-finite rendered minP/maxP never matches); an
|
|
471
500
|
// unvalidated p wouldn't: a driver bug or a miscalibrated model
|
|
472
501
|
// sending p > 1 would clear every minP a workflow declares,
|
|
473
502
|
// unconditionally forcing the judged route on a live gate.
|
|
474
|
-
p: Schema.Number.
|
|
503
|
+
p: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 })),
|
|
475
504
|
}),
|
|
476
505
|
)
|
|
477
506
|
|
|
@@ -503,7 +532,7 @@ const runJudgeAnswerCommand = (
|
|
|
503
532
|
try: () => JSON.parse(raw) as unknown,
|
|
504
533
|
catch: () => new SelectorUsageError("gtd judge answer: stdin is not valid JSON"),
|
|
505
534
|
})
|
|
506
|
-
const verdict = yield* Schema.
|
|
535
|
+
const verdict = yield* Schema.decodeUnknownEffect(verdictSchemaFor(ids))(parsed).pipe(
|
|
507
536
|
Effect.mapError(
|
|
508
537
|
(e) =>
|
|
509
538
|
new SelectorUsageError(
|
|
@@ -556,10 +585,10 @@ const runLandCommand = (
|
|
|
556
585
|
* `base` renders to something that is not an ancestor of (and differs from)
|
|
557
586
|
* HEAD.
|
|
558
587
|
*/
|
|
559
|
-
const
|
|
560
|
-
actor: string,
|
|
588
|
+
const startWorkflow = (
|
|
561
589
|
workflow: string,
|
|
562
590
|
varOverrides: Record<string, string>,
|
|
591
|
+
json: JsonMode,
|
|
563
592
|
out: ArtifactOut,
|
|
564
593
|
commandLabel: string,
|
|
565
594
|
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
@@ -572,7 +601,7 @@ const runStartCommand = (
|
|
|
572
601
|
? `startable: ${config.workflowNames.join(", ")}`
|
|
573
602
|
: "this config exports no workflow besides its default, which an ordinary start runs"
|
|
574
603
|
return yield* Effect.fail(
|
|
575
|
-
new GtdUsageError(
|
|
604
|
+
new GtdUsageError(`${commandLabel}: unknown workflow "${workflow}" — ${startable}`),
|
|
576
605
|
)
|
|
577
606
|
}
|
|
578
607
|
const rest = yield* restAt(undefined, varOverrides)
|
|
@@ -582,7 +611,7 @@ const runStartCommand = (
|
|
|
582
611
|
state: rest.state,
|
|
583
612
|
idle: noProcessUnderway(rest),
|
|
584
613
|
},
|
|
585
|
-
|
|
614
|
+
"human",
|
|
586
615
|
{
|
|
587
616
|
workflow: resolved,
|
|
588
617
|
first: started,
|
|
@@ -594,7 +623,11 @@ const runStartCommand = (
|
|
|
594
623
|
if (plan.kind === "refusal") {
|
|
595
624
|
return yield* Effect.fail(new Error(plan.message))
|
|
596
625
|
}
|
|
597
|
-
|
|
626
|
+
yield* writeScript(
|
|
627
|
+
json,
|
|
628
|
+
out,
|
|
629
|
+
listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes),
|
|
630
|
+
)
|
|
598
631
|
})
|
|
599
632
|
|
|
600
633
|
/**
|
|
@@ -618,40 +651,23 @@ const doorSynopsis = (name: string, door: Door): string =>
|
|
|
618
651
|
...(door.args ?? []).map((arg) => (arg.optional === true ? `[${arg.name}]` : `<${arg.name}>`)),
|
|
619
652
|
].join(" ")
|
|
620
653
|
|
|
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,
|
|
654
|
+
/** A door's start: its args mapped to process settings, `extra` (`--var`) pinned on top. */
|
|
655
|
+
const startThroughDoor = (
|
|
656
|
+
name: string,
|
|
657
|
+
door: Door,
|
|
640
658
|
args: readonly string[],
|
|
659
|
+
extra: Readonly<Record<string, string>>,
|
|
660
|
+
json: JsonMode,
|
|
641
661
|
out: ArtifactOut,
|
|
642
662
|
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
643
663
|
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
|
-
}
|
|
664
|
+
const label = `gtd start ${name}`
|
|
649
665
|
const declared = door.args ?? []
|
|
650
666
|
const required = declared.filter((arg) => arg.optional !== true).length
|
|
651
667
|
if (args.length < required || args.length > declared.length) {
|
|
652
668
|
return yield* Effect.fail(
|
|
653
669
|
new GtdUsageError(
|
|
654
|
-
|
|
670
|
+
`${label}: ${args.length < required ? "missing" : "too many"} arguments — usage: gtd start ${doorSynopsis(name, door)}`,
|
|
655
671
|
),
|
|
656
672
|
)
|
|
657
673
|
}
|
|
@@ -661,35 +677,81 @@ const runDoorCommand = (
|
|
|
661
677
|
})
|
|
662
678
|
const vars = yield* Effect.try({
|
|
663
679
|
try: () => door.vars?.(named) ?? {},
|
|
664
|
-
catch: (e) => new Error(
|
|
680
|
+
catch: (e) => new Error(`${label}: ${e instanceof Error ? e.message : String(e)}`),
|
|
665
681
|
})
|
|
666
|
-
yield*
|
|
682
|
+
yield* startWorkflow(door.workflow, { ...vars, ...extra }, json, out, label)
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* `gtd start <name> [args…]`: a door when `<name>` is one — so a door shadows
|
|
687
|
+
* the workflow it shares a name with — else a workflow, which takes no args.
|
|
688
|
+
* No name lists both.
|
|
689
|
+
*/
|
|
690
|
+
const runStartCommand = (
|
|
691
|
+
command: Extract<Command, { kind: "start" }>,
|
|
692
|
+
json: JsonMode,
|
|
693
|
+
out: ArtifactOut,
|
|
694
|
+
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
695
|
+
Effect.gen(function* () {
|
|
696
|
+
const { name, args, vars } = command
|
|
697
|
+
if (name === undefined) return yield* runStartListCommand(json, out)
|
|
698
|
+
const label = `gtd start ${name}`
|
|
699
|
+
const config = yield* (yield* ConfigService).load
|
|
700
|
+
const door = config.doors.get(name)
|
|
701
|
+
if (door !== undefined) {
|
|
702
|
+
return yield* startThroughDoor(name, door, args, vars, json, out)
|
|
703
|
+
}
|
|
704
|
+
if (config.workflowNamed(name) === undefined) {
|
|
705
|
+
return yield* Effect.fail(
|
|
706
|
+
new GtdUsageError(
|
|
707
|
+
`${label}: no workflow or door is named "${name}" — run \`gtd start\` to list them`,
|
|
708
|
+
),
|
|
709
|
+
)
|
|
710
|
+
}
|
|
711
|
+
if (args.length > 0) {
|
|
712
|
+
return yield* Effect.fail(
|
|
713
|
+
new GtdUsageError(
|
|
714
|
+
`${label}: too many arguments — a workflow takes none, only --var <name>=<value>; got: ${args.join(", ")}`,
|
|
715
|
+
),
|
|
716
|
+
)
|
|
717
|
+
}
|
|
718
|
+
yield* startWorkflow(name, { ...vars }, json, out, label)
|
|
667
719
|
})
|
|
668
720
|
|
|
669
|
-
/**
|
|
670
|
-
|
|
721
|
+
/**
|
|
722
|
+
* Bare `gtd start`: every workflow startable by name, then every door — plain
|
|
723
|
+
* lines, or one JSON array. A workflow a door shadows is left out: its name
|
|
724
|
+
* opens the door.
|
|
725
|
+
*/
|
|
726
|
+
const runStartListCommand = (
|
|
671
727
|
json: JsonMode,
|
|
672
728
|
out: ArtifactOut,
|
|
673
729
|
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
674
730
|
Effect.gen(function* () {
|
|
675
731
|
const config = yield* (yield* ConfigService).load
|
|
732
|
+
const workflows = [...config.workflowNames].filter((name) => !config.doors.has(name)).sort()
|
|
676
733
|
const doors = [...config.doors]
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
734
|
+
const document = [
|
|
735
|
+
...workflows.map((name) => ({ kind: "workflow", name })),
|
|
736
|
+
...doors.map(([name, door]) => ({
|
|
737
|
+
kind: "door",
|
|
738
|
+
name,
|
|
739
|
+
workflow: door.workflow,
|
|
740
|
+
args: (door.args ?? []).map((arg) => ({ name: arg.name, optional: arg.optional === true })),
|
|
741
|
+
})),
|
|
742
|
+
]
|
|
743
|
+
if (yield* writeJson(json, out, document)) return
|
|
744
|
+
if (document.length === 0) {
|
|
745
|
+
yield* (yield* Narrator).warn(
|
|
746
|
+
"gtd start: this config exports no workflow besides its default, which an ordinary start runs, and no door",
|
|
747
|
+
)
|
|
685
748
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
else yield* writeSelection(out, document, json.path)
|
|
749
|
+
out.write(
|
|
750
|
+
[
|
|
751
|
+
...workflows.map((name) => `workflow ${name}\n`),
|
|
752
|
+
...doors.map(([n, d]) => `door ${doorSynopsis(n, d)} → ${d.workflow}\n`),
|
|
753
|
+
].join(""),
|
|
754
|
+
)
|
|
693
755
|
})
|
|
694
756
|
|
|
695
757
|
/**
|
|
@@ -705,7 +767,10 @@ const runDoorsCommand = (
|
|
|
705
767
|
* Idempotent at the initial state. The one refusal is a process whose first
|
|
706
768
|
* commit is the repository's root commit: nothing earlier to rewind to.
|
|
707
769
|
*/
|
|
708
|
-
const runAbandonCommand = (
|
|
770
|
+
const runAbandonCommand = (
|
|
771
|
+
json: JsonMode,
|
|
772
|
+
out: ArtifactOut,
|
|
773
|
+
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
709
774
|
Effect.gen(function* () {
|
|
710
775
|
const git = yield* GitService
|
|
711
776
|
const config = yield* (yield* ConfigService).load
|
|
@@ -716,8 +781,7 @@ const runAbandonCommand = (out: ArtifactOut): Effect.Effect<void, Error, Command
|
|
|
716
781
|
const required = emitScripts([
|
|
717
782
|
{ kind: "outcome", command: abandonNoopOutcome(initial) },
|
|
718
783
|
]).required
|
|
719
|
-
out
|
|
720
|
-
return
|
|
784
|
+
return yield* writeScript(json, out, combinedScript(required, ""))
|
|
721
785
|
}
|
|
722
786
|
const restState = run.trace[run.trace.length - 1]!.state
|
|
723
787
|
|
|
@@ -738,7 +802,7 @@ const runAbandonCommand = (out: ArtifactOut): Effect.Effect<void, Error, Command
|
|
|
738
802
|
{ kind: "outcome", command: abandonedOutcome(restState, run.startParentHash, initial) },
|
|
739
803
|
]
|
|
740
804
|
const required = emitScripts(steps).required
|
|
741
|
-
out
|
|
805
|
+
yield* writeScript(json, out, combinedScript(required, ""))
|
|
742
806
|
})
|
|
743
807
|
|
|
744
808
|
/**
|
|
@@ -748,7 +812,10 @@ const runAbandonCommand = (out: ArtifactOut): Effect.Effect<void, Error, Command
|
|
|
748
812
|
* refuses on a dirty working tree, no retained history, or HEAD having
|
|
749
813
|
* advanced past the retained tip with commits that would be lost.
|
|
750
814
|
*/
|
|
751
|
-
const runRestoreCommand = (
|
|
815
|
+
const runRestoreCommand = (
|
|
816
|
+
json: JsonMode,
|
|
817
|
+
out: ArtifactOut,
|
|
818
|
+
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
752
819
|
Effect.gen(function* () {
|
|
753
820
|
const git = yield* GitService
|
|
754
821
|
|
|
@@ -790,7 +857,7 @@ const runRestoreCommand = (out: ArtifactOut): Effect.Effect<void, Error, Command
|
|
|
790
857
|
{ kind: "outcome", command: restoredOutcome(tip, after.state) },
|
|
791
858
|
]
|
|
792
859
|
const required = emitScripts(steps).required
|
|
793
|
-
out
|
|
860
|
+
yield* writeScript(json, out, combinedScript(required, ""))
|
|
794
861
|
})
|
|
795
862
|
|
|
796
863
|
/**
|
|
@@ -890,14 +957,18 @@ const runNextCommand = (
|
|
|
890
957
|
})
|
|
891
958
|
|
|
892
959
|
/** 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 = (
|
|
960
|
+
const runValidateCommand = (
|
|
961
|
+
json: JsonMode,
|
|
962
|
+
out: ArtifactOut,
|
|
963
|
+
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
894
964
|
Effect.gen(function* () {
|
|
895
965
|
const rest = yield* currentRest
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
966
|
+
const script = (yield* resolveValidateScript(rest))?.script ?? ""
|
|
967
|
+
yield* writeScript(
|
|
968
|
+
json,
|
|
969
|
+
out,
|
|
970
|
+
script,
|
|
971
|
+
script.length > 0 ? `${script}\n` : `nothing to validate at "${rest.state}"\n`,
|
|
901
972
|
)
|
|
902
973
|
})
|
|
903
974
|
|
|
@@ -906,8 +977,10 @@ const runValidateCommand = (out: ArtifactOut): Effect.Effect<void, Error, Comman
|
|
|
906
977
|
* Resolves no workflow state and reads no config — this is what a seeded
|
|
907
978
|
* `validate:` command invokes as a leaf step, and it formats nothing.
|
|
908
979
|
*
|
|
909
|
-
* An
|
|
910
|
-
*
|
|
980
|
+
* An unknown mode is a usage error; an absent file is a refusal (the emitted
|
|
981
|
+
* validate script guards on the file's presence before it calls this).
|
|
982
|
+
* Findings ride the failing Effect's message, so the shared refusal path
|
|
983
|
+
* reports them on stderr.
|
|
911
984
|
*/
|
|
912
985
|
const runCheckCommand = (
|
|
913
986
|
mode: string,
|
|
@@ -919,7 +992,7 @@ const runCheckCommand = (
|
|
|
919
992
|
const format = steeringFormatFor(mode)
|
|
920
993
|
if (format === undefined) {
|
|
921
994
|
return yield* Effect.fail(
|
|
922
|
-
new
|
|
995
|
+
new GtdUsageError(
|
|
923
996
|
`gtd check: unknown mode "${mode}" — known modes: ${BUILT_IN_MODE_NAMES.join(", ")}`,
|
|
924
997
|
),
|
|
925
998
|
)
|
|
@@ -953,12 +1026,8 @@ const runCheckCommand = (
|
|
|
953
1026
|
return yield* runOpenQuestionsCheckCommand(format, file)
|
|
954
1027
|
}
|
|
955
1028
|
|
|
956
|
-
const
|
|
957
|
-
const
|
|
958
|
-
const errors =
|
|
959
|
-
content !== undefined
|
|
960
|
-
? format.validate(content).map((finding) => formatFinding(file, finding))
|
|
961
|
-
: []
|
|
1029
|
+
const content = yield* existingFile(file)
|
|
1030
|
+
const errors = format.validate(content).map((finding) => formatFinding(file, finding))
|
|
962
1031
|
|
|
963
1032
|
if (errors.length === 0) return
|
|
964
1033
|
|
|
@@ -970,6 +1039,14 @@ const runCheckCommand = (
|
|
|
970
1039
|
)
|
|
971
1040
|
})
|
|
972
1041
|
|
|
1042
|
+
const existingFile = (file: string): Effect.Effect<string, Error, Workspace> =>
|
|
1043
|
+
Effect.flatMap(Workspace, (workspace) => {
|
|
1044
|
+
const content = workspace.atPath(file)
|
|
1045
|
+
return content === undefined
|
|
1046
|
+
? Effect.fail(new Error(`gtd check: no such file: ${file}`))
|
|
1047
|
+
: Effect.succeed(content)
|
|
1048
|
+
})
|
|
1049
|
+
|
|
973
1050
|
/**
|
|
974
1051
|
* The bare `gtd check --open-threads`: code-comment threads in the files the
|
|
975
1052
|
* current process changed. The one `check` form that reads process state; no
|
|
@@ -997,18 +1074,13 @@ const runCodeThreadsCheckCommand = (): Effect.Effect<void, Error, CommandRequire
|
|
|
997
1074
|
return yield* Effect.fail(new Error(lines.join("\n")))
|
|
998
1075
|
})
|
|
999
1076
|
|
|
1000
|
-
/** Prints one `file:line: [^name]: <first H entry>` line per open thread
|
|
1077
|
+
/** Prints one `file:line: [^name]: <first H entry>` line per open thread. */
|
|
1001
1078
|
const runOpenThreadsCheckCommand = (
|
|
1002
1079
|
format: SteeringFormat,
|
|
1003
1080
|
file: string,
|
|
1004
1081
|
): Effect.Effect<void, Error, Workspace> =>
|
|
1005
1082
|
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
|
-
|
|
1083
|
+
const content = yield* existingFile(file)
|
|
1012
1084
|
const errors = openThreads(format, content).map(
|
|
1013
1085
|
(t) => `${file}:${t.line + 1}: [^${t.name}]: ${t.firstMe}`,
|
|
1014
1086
|
)
|
|
@@ -1021,20 +1093,14 @@ const runOpenThreadsCheckCommand = (
|
|
|
1021
1093
|
|
|
1022
1094
|
/**
|
|
1023
1095
|
* 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.
|
|
1096
|
+
* reads, so the gate script and the flow's own check stay in sync.
|
|
1026
1097
|
*/
|
|
1027
1098
|
const runOpenQuestionsCheckCommand = (
|
|
1028
1099
|
format: SteeringFormat,
|
|
1029
1100
|
file: string,
|
|
1030
1101
|
): Effect.Effect<void, Error, Workspace> =>
|
|
1031
1102
|
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
|
-
|
|
1103
|
+
const content = yield* existingFile(file)
|
|
1038
1104
|
const errors = unansweredQuestions(format, content).map(
|
|
1039
1105
|
(q) => `${file}:${q.headingLine + 1}: ${q.question}`,
|
|
1040
1106
|
)
|
|
@@ -1094,7 +1160,7 @@ const gatherBeatDocument = (
|
|
|
1094
1160
|
kind === "prompt" ? resolveSession(rendered.memory, rendered.memoryResumed) : undefined
|
|
1095
1161
|
const resolvedValidate =
|
|
1096
1162
|
kind === "prompt"
|
|
1097
|
-
? yield* resolveValidateScript(rest).pipe(Effect.
|
|
1163
|
+
? yield* resolveValidateScript(rest).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
1098
1164
|
: undefined
|
|
1099
1165
|
const validate =
|
|
1100
1166
|
resolvedValidate !== undefined && resolvedValidate.script.length > 0
|
|
@@ -1208,7 +1274,7 @@ const assertRepositoryHasCommits = (git: GitOperations): Effect.Effect<void, Err
|
|
|
1208
1274
|
export const needsOf = (kind: Command["kind"]): Needs => {
|
|
1209
1275
|
switch (kind) {
|
|
1210
1276
|
case "lsp":
|
|
1211
|
-
case "
|
|
1277
|
+
case "guide":
|
|
1212
1278
|
case "judgeRun":
|
|
1213
1279
|
case "strays":
|
|
1214
1280
|
return "none"
|
|
@@ -1225,7 +1291,7 @@ export const standaloneKinds = (): readonly Command["kind"][] => [
|
|
|
1225
1291
|
"lsp",
|
|
1226
1292
|
"check",
|
|
1227
1293
|
"uncheck",
|
|
1228
|
-
"
|
|
1294
|
+
"guide",
|
|
1229
1295
|
"judgeRun",
|
|
1230
1296
|
"strays",
|
|
1231
1297
|
]
|
|
@@ -1266,19 +1332,15 @@ const dispatchVoidCommand = (
|
|
|
1266
1332
|
out,
|
|
1267
1333
|
)
|
|
1268
1334
|
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)
|
|
1335
|
+
return runStartCommand(command, json, out)
|
|
1274
1336
|
case "abandon":
|
|
1275
|
-
return runAbandonCommand(out)
|
|
1337
|
+
return runAbandonCommand(json, out)
|
|
1276
1338
|
case "restore":
|
|
1277
|
-
return runRestoreCommand(out)
|
|
1339
|
+
return runRestoreCommand(json, out)
|
|
1278
1340
|
case "next":
|
|
1279
1341
|
return runNextCommand(json, out)
|
|
1280
1342
|
case "validate":
|
|
1281
|
-
return runValidateCommand(out)
|
|
1343
|
+
return runValidateCommand(json, out)
|
|
1282
1344
|
case "check":
|
|
1283
1345
|
if (command.mode === undefined || command.file === undefined) {
|
|
1284
1346
|
return runCodeThreadsCheckCommand()
|
|
@@ -1291,8 +1353,10 @@ const dispatchVoidCommand = (
|
|
|
1291
1353
|
)
|
|
1292
1354
|
case "uncheck":
|
|
1293
1355
|
return runUncheckCommand(command.file)
|
|
1294
|
-
case "
|
|
1295
|
-
return
|
|
1356
|
+
case "guide":
|
|
1357
|
+
return runGuideCommand(out)
|
|
1358
|
+
case "bundle":
|
|
1359
|
+
return runBundleCommand(out)
|
|
1296
1360
|
case "summary":
|
|
1297
1361
|
return runSummaryCommand(out)
|
|
1298
1362
|
case "base":
|
|
@@ -1312,7 +1376,7 @@ const dispatchVoidCommand = (
|
|
|
1312
1376
|
case "historyJudges":
|
|
1313
1377
|
return runHistoryJudgesCommand(command.refs, command.vars, command.jsonl, json, out)
|
|
1314
1378
|
case "historyFetch":
|
|
1315
|
-
return runHistoryFetchCommand(command.remote, out)
|
|
1379
|
+
return runHistoryFetchCommand(command.remote, json, out)
|
|
1316
1380
|
}
|
|
1317
1381
|
}
|
|
1318
1382
|
|
package/src/cli/ToolCommands.ts
CHANGED
|
@@ -29,7 +29,7 @@ interface JudgeRequest {
|
|
|
29
29
|
* The subcommands backed by a driver or tool. Core may not import those
|
|
30
30
|
* units, so the composition root provides them here.
|
|
31
31
|
*/
|
|
32
|
-
export class ToolCommands extends Context.
|
|
32
|
+
export class ToolCommands extends Context.Service<
|
|
33
33
|
ToolCommands,
|
|
34
34
|
{
|
|
35
35
|
readonly ui: (
|
|
@@ -39,4 +39,4 @@ export class ToolCommands extends Context.Tag("ToolCommands")<
|
|
|
39
39
|
readonly lsp: (gtd: SelfGtd) => Effect.Effect<void, Error>
|
|
40
40
|
readonly judge: (request: JudgeRequest) => Promise<string>
|
|
41
41
|
}
|
|
42
|
-
>() {}
|
|
42
|
+
>()("ToolCommands") {}
|