@pmelab/gtd 59.0.0 → 60.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +42 -38
- 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-CMBAAyRK.mjs → testkit-BvecVjQ0.mjs} +937 -772
- package/dist/chunks/{wire-C12dahrH.mjs → wire-DZ8R1RU9.mjs} +9 -2
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +1101 -1025
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +371 -82
- 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 +68 -61
- package/src/cli/Cli.test.ts +311 -229
- package/src/cli/Cli.ts +486 -585
- package/src/cli/Help.test.ts +47 -0
- package/src/cli/Help.ts +70 -0
- 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 +131 -82
- package/src/cli/Program.ts +197 -111
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +4 -4
- package/src/emit/Emit.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +25 -6
- 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 +481 -98
- package/src/gherkin/messages.test.ts +124 -0
- package/src/gherkin/messages.ts +101 -0
- 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 +289 -6
- package/src/testkit/TestRepo.ts +317 -41
- 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 +7 -14
- package/src/twins/EmittedScripts.test.ts +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- package/src/wire/Land.ts +1 -1
- 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 +175 -152
- package/src/workflow/skills.test.ts +5 -5
- package/src/workflow/skills.ts +1 -1
- 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
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
landFields,
|
|
34
34
|
noopText,
|
|
35
35
|
renderBeatJson,
|
|
36
|
+
DEMAND_BRIEFING,
|
|
36
37
|
renderBeatPlain,
|
|
37
38
|
renderLandJson,
|
|
38
39
|
renderLandPlain,
|
|
@@ -69,7 +70,7 @@ import {
|
|
|
69
70
|
unansweredQuestions,
|
|
70
71
|
formatFinding,
|
|
71
72
|
type SteeringFormat,
|
|
72
|
-
} from "../steering/
|
|
73
|
+
} from "../steering/internal.js"
|
|
73
74
|
import {
|
|
74
75
|
resolveMode,
|
|
75
76
|
validateScriptFor,
|
|
@@ -90,7 +91,10 @@ import {
|
|
|
90
91
|
import { renderBriefing, WORKFLOWS_REPO } from "../install/index.js"
|
|
91
92
|
import { ToolCommands, type SelfGtd } from "./ToolCommands.js"
|
|
92
93
|
import {
|
|
94
|
+
normalizeScriptNewline,
|
|
93
95
|
SelectorUsageError,
|
|
96
|
+
writeJson,
|
|
97
|
+
writeScript,
|
|
94
98
|
writeSelection,
|
|
95
99
|
type ArtifactOut,
|
|
96
100
|
type CommandRequirements,
|
|
@@ -138,13 +142,11 @@ export type Command =
|
|
|
138
142
|
}
|
|
139
143
|
| {
|
|
140
144
|
readonly kind: "start"
|
|
141
|
-
|
|
142
|
-
readonly
|
|
145
|
+
/** A door or a workflow; absent lists both. */
|
|
146
|
+
readonly name?: string
|
|
147
|
+
readonly args: readonly string[]
|
|
143
148
|
readonly vars: Readonly<Record<string, string>>
|
|
144
|
-
readonly label: string
|
|
145
149
|
}
|
|
146
|
-
| { readonly kind: "door"; readonly name: string; readonly args: readonly string[] }
|
|
147
|
-
| { readonly kind: "doors" }
|
|
148
150
|
| { readonly kind: "abandon" }
|
|
149
151
|
| { readonly kind: "restore" }
|
|
150
152
|
| { readonly kind: "next" }
|
|
@@ -158,7 +160,7 @@ export type Command =
|
|
|
158
160
|
readonly openThreads?: boolean
|
|
159
161
|
}
|
|
160
162
|
| { readonly kind: "uncheck"; readonly file: string }
|
|
161
|
-
| { readonly kind: "
|
|
163
|
+
| { readonly kind: "guide" }
|
|
162
164
|
| { readonly kind: "summary" }
|
|
163
165
|
| { readonly kind: "base" }
|
|
164
166
|
| { readonly kind: "judge" }
|
|
@@ -210,7 +212,7 @@ const runLspCommand = (): Effect.Effect<void, Error, ToolCommands> =>
|
|
|
210
212
|
* gtd ships no workflow, so where no `gtd.config.ts` governs it says where to
|
|
211
213
|
* get one; it writes nothing.
|
|
212
214
|
*/
|
|
213
|
-
const
|
|
215
|
+
const runGuideCommand = (
|
|
214
216
|
out: ArtifactOut,
|
|
215
217
|
): Effect.Effect<void, Error, GitService | Host | ConfigDiscovery | Narrator> =>
|
|
216
218
|
Effect.gen(function* () {
|
|
@@ -222,7 +224,7 @@ const runInstallCommand = (
|
|
|
222
224
|
if ((yield* host.realPath(topLevel.right)) !== (yield* host.realPath(host.root))) return
|
|
223
225
|
if ((yield* (yield* ConfigDiscovery).workflowModule(host.root, host.home)) !== undefined) return
|
|
224
226
|
yield* (yield* Narrator).warn(
|
|
225
|
-
`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)`,
|
|
226
228
|
)
|
|
227
229
|
})
|
|
228
230
|
|
|
@@ -243,20 +245,10 @@ interface LandResult {
|
|
|
243
245
|
readonly idle: boolean
|
|
244
246
|
}
|
|
245
247
|
|
|
246
|
-
/**
|
|
247
|
-
* Exactly one trailing newline — duplicated from `Cli.ts`'s own
|
|
248
|
-
* `normalizeTrailingNewline` rather than imported, to avoid a circular value
|
|
249
|
-
* dependency (`Cli.ts` already depends on this module). `LandResult.script`
|
|
250
|
-
* must carry its own trailing newline because it's embedded verbatim inside
|
|
251
|
-
* the `--json` document, never re-normalized at its tail.
|
|
252
|
-
*/
|
|
253
|
-
const normalizeScriptNewline = (script: string): string =>
|
|
254
|
-
script.length === 0 ? script : `${script.replace(/\n+$/, "")}\n`
|
|
255
|
-
|
|
256
248
|
/**
|
|
257
249
|
* `combinedScript`, narrowed to accept only an already-guarded
|
|
258
250
|
* `RunnableScript` for `required` — the one boundary `ScriptSurface`'s brand
|
|
259
|
-
* exists to protect: `gtd land`/`gtd
|
|
251
|
+
* exists to protect: `gtd land`/`gtd start`'s emitted script. Every OTHER
|
|
260
252
|
* `combinedScript` call (`abandon`/`restore`/steering validate, below) builds
|
|
261
253
|
* from `emitScripts` directly and keeps the unbranded signature — those
|
|
262
254
|
* paths land no guard-gated git write for a brand to protect.
|
|
@@ -424,6 +416,20 @@ const readStdin = (): Effect.Effect<string, Error> =>
|
|
|
424
416
|
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
425
417
|
})
|
|
426
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
|
+
|
|
427
433
|
/** Stdout is written once, after the answerer resolves — a rejection leaves it empty. */
|
|
428
434
|
const runJudgeRunCommand = (
|
|
429
435
|
command: Extract<Command, { kind: "judgeRun" }>,
|
|
@@ -434,6 +440,7 @@ const runJudgeRunCommand = (
|
|
|
434
440
|
const tools = yield* ToolCommands
|
|
435
441
|
yield* refuseLegacyRc
|
|
436
442
|
const input = yield* readStdin()
|
|
443
|
+
yield* judgmentShape(input)
|
|
437
444
|
const verdict = yield* Effect.tryPromise({
|
|
438
445
|
try: () =>
|
|
439
446
|
tools.judge({
|
|
@@ -555,10 +562,10 @@ const runLandCommand = (
|
|
|
555
562
|
* `base` renders to something that is not an ancestor of (and differs from)
|
|
556
563
|
* HEAD.
|
|
557
564
|
*/
|
|
558
|
-
const
|
|
559
|
-
actor: string,
|
|
565
|
+
const startWorkflow = (
|
|
560
566
|
workflow: string,
|
|
561
567
|
varOverrides: Record<string, string>,
|
|
568
|
+
json: JsonMode,
|
|
562
569
|
out: ArtifactOut,
|
|
563
570
|
commandLabel: string,
|
|
564
571
|
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
@@ -566,10 +573,12 @@ const runStartCommand = (
|
|
|
566
573
|
const config = yield* (yield* ConfigService).load
|
|
567
574
|
const resolved = config.workflowNamed(workflow)
|
|
568
575
|
if (resolved === undefined) {
|
|
576
|
+
const startable =
|
|
577
|
+
config.workflowNames.length > 0
|
|
578
|
+
? `startable: ${config.workflowNames.join(", ")}`
|
|
579
|
+
: "this config exports no workflow besides its default, which an ordinary start runs"
|
|
569
580
|
return yield* Effect.fail(
|
|
570
|
-
new GtdUsageError(
|
|
571
|
-
`gtd --workflow: unknown workflow "${workflow}" — startable: ${config.workflowNames.join(", ")}`,
|
|
572
|
-
),
|
|
581
|
+
new GtdUsageError(`${commandLabel}: unknown workflow "${workflow}" — ${startable}`),
|
|
573
582
|
)
|
|
574
583
|
}
|
|
575
584
|
const rest = yield* restAt(undefined, varOverrides)
|
|
@@ -579,7 +588,7 @@ const runStartCommand = (
|
|
|
579
588
|
state: rest.state,
|
|
580
589
|
idle: noProcessUnderway(rest),
|
|
581
590
|
},
|
|
582
|
-
|
|
591
|
+
"human",
|
|
583
592
|
{
|
|
584
593
|
workflow: resolved,
|
|
585
594
|
first: started,
|
|
@@ -591,42 +600,51 @@ const runStartCommand = (
|
|
|
591
600
|
if (plan.kind === "refusal") {
|
|
592
601
|
return yield* Effect.fail(new Error(plan.message))
|
|
593
602
|
}
|
|
594
|
-
|
|
603
|
+
yield* writeScript(
|
|
604
|
+
json,
|
|
605
|
+
out,
|
|
606
|
+
listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes),
|
|
607
|
+
)
|
|
595
608
|
})
|
|
596
609
|
|
|
610
|
+
/**
|
|
611
|
+
* A start commits the whole tree (`git add -A`), so its script says up front
|
|
612
|
+
* which paths that is. Comment lines only: control characters are masked so a
|
|
613
|
+
* path can never end the comment and become a command.
|
|
614
|
+
*/
|
|
615
|
+
const listingCommitted = (script: string, changes: readonly PendingChange[]): string => {
|
|
616
|
+
if (changes.length === 0) return script
|
|
617
|
+
const listing = [
|
|
618
|
+
"# commits every change in the working tree (git add -A):",
|
|
619
|
+
...changes.map((change) => `# ${change.status} ${change.path.replace(/\p{Cc}/gu, "?")}`),
|
|
620
|
+
].join("\n")
|
|
621
|
+
const at = script.indexOf("\n\n")
|
|
622
|
+
return at === -1 ? script : `${script.slice(0, at)}\n\n${listing}${script.slice(at)}`
|
|
623
|
+
}
|
|
624
|
+
|
|
597
625
|
const doorSynopsis = (name: string, door: Door): string =>
|
|
598
626
|
[
|
|
599
627
|
name,
|
|
600
628
|
...(door.args ?? []).map((arg) => (arg.optional === true ? `[${arg.name}]` : `<${arg.name}>`)),
|
|
601
629
|
].join(" ")
|
|
602
630
|
|
|
603
|
-
/**
|
|
604
|
-
|
|
605
|
-
* process settings its `vars` derive from the positional args. A wrong name or
|
|
606
|
-
* arity is a usage error naming what would have worked.
|
|
607
|
-
*/
|
|
608
|
-
const runDoorCommand = (
|
|
631
|
+
/** A door's start: its args mapped to process settings, `extra` (`--var`) pinned on top. */
|
|
632
|
+
const startThroughDoor = (
|
|
609
633
|
name: string,
|
|
634
|
+
door: Door,
|
|
610
635
|
args: readonly string[],
|
|
636
|
+
extra: Readonly<Record<string, string>>,
|
|
637
|
+
json: JsonMode,
|
|
611
638
|
out: ArtifactOut,
|
|
612
639
|
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
613
640
|
Effect.gen(function* () {
|
|
614
|
-
const
|
|
615
|
-
const door = config.doors.get(name)
|
|
616
|
-
if (door === undefined) {
|
|
617
|
-
const known = [...config.doors]
|
|
618
|
-
return yield* Effect.fail(
|
|
619
|
-
new GtdUsageError(
|
|
620
|
-
`gtd door: unknown door "${name}" — doors: ${known.map(([n, d]) => doorSynopsis(n, d)).join(", ")}`,
|
|
621
|
-
),
|
|
622
|
-
)
|
|
623
|
-
}
|
|
641
|
+
const label = `gtd start ${name}`
|
|
624
642
|
const declared = door.args ?? []
|
|
625
643
|
const required = declared.filter((arg) => arg.optional !== true).length
|
|
626
644
|
if (args.length < required || args.length > declared.length) {
|
|
627
645
|
return yield* Effect.fail(
|
|
628
646
|
new GtdUsageError(
|
|
629
|
-
|
|
647
|
+
`${label}: ${args.length < required ? "missing" : "too many"} arguments — usage: gtd start ${doorSynopsis(name, door)}`,
|
|
630
648
|
),
|
|
631
649
|
)
|
|
632
650
|
}
|
|
@@ -636,30 +654,81 @@ const runDoorCommand = (
|
|
|
636
654
|
})
|
|
637
655
|
const vars = yield* Effect.try({
|
|
638
656
|
try: () => door.vars?.(named) ?? {},
|
|
639
|
-
catch: (e) => new Error(
|
|
657
|
+
catch: (e) => new Error(`${label}: ${e instanceof Error ? e.message : String(e)}`),
|
|
640
658
|
})
|
|
641
|
-
yield*
|
|
659
|
+
yield* startWorkflow(door.workflow, { ...vars, ...extra }, json, out, label)
|
|
660
|
+
})
|
|
661
|
+
|
|
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" }>,
|
|
669
|
+
json: JsonMode,
|
|
670
|
+
out: ArtifactOut,
|
|
671
|
+
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
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}`
|
|
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)
|
|
642
696
|
})
|
|
643
697
|
|
|
644
|
-
/**
|
|
645
|
-
|
|
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 = (
|
|
646
704
|
json: JsonMode,
|
|
647
705
|
out: ArtifactOut,
|
|
648
706
|
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
649
707
|
Effect.gen(function* () {
|
|
650
708
|
const config = yield* (yield* ConfigService).load
|
|
709
|
+
const workflows = [...config.workflowNames].filter((name) => !config.doors.has(name)).sort()
|
|
651
710
|
const doors = [...config.doors]
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
+
)
|
|
655
725
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
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
|
+
)
|
|
663
732
|
})
|
|
664
733
|
|
|
665
734
|
/**
|
|
@@ -675,7 +744,10 @@ const runDoorsCommand = (
|
|
|
675
744
|
* Idempotent at the initial state. The one refusal is a process whose first
|
|
676
745
|
* commit is the repository's root commit: nothing earlier to rewind to.
|
|
677
746
|
*/
|
|
678
|
-
const runAbandonCommand = (
|
|
747
|
+
const runAbandonCommand = (
|
|
748
|
+
json: JsonMode,
|
|
749
|
+
out: ArtifactOut,
|
|
750
|
+
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
679
751
|
Effect.gen(function* () {
|
|
680
752
|
const git = yield* GitService
|
|
681
753
|
const config = yield* (yield* ConfigService).load
|
|
@@ -686,8 +758,7 @@ const runAbandonCommand = (out: ArtifactOut): Effect.Effect<void, Error, Command
|
|
|
686
758
|
const required = emitScripts([
|
|
687
759
|
{ kind: "outcome", command: abandonNoopOutcome(initial) },
|
|
688
760
|
]).required
|
|
689
|
-
out
|
|
690
|
-
return
|
|
761
|
+
return yield* writeScript(json, out, combinedScript(required, ""))
|
|
691
762
|
}
|
|
692
763
|
const restState = run.trace[run.trace.length - 1]!.state
|
|
693
764
|
|
|
@@ -708,7 +779,7 @@ const runAbandonCommand = (out: ArtifactOut): Effect.Effect<void, Error, Command
|
|
|
708
779
|
{ kind: "outcome", command: abandonedOutcome(restState, run.startParentHash, initial) },
|
|
709
780
|
]
|
|
710
781
|
const required = emitScripts(steps).required
|
|
711
|
-
out
|
|
782
|
+
yield* writeScript(json, out, combinedScript(required, ""))
|
|
712
783
|
})
|
|
713
784
|
|
|
714
785
|
/**
|
|
@@ -718,7 +789,10 @@ const runAbandonCommand = (out: ArtifactOut): Effect.Effect<void, Error, Command
|
|
|
718
789
|
* refuses on a dirty working tree, no retained history, or HEAD having
|
|
719
790
|
* advanced past the retained tip with commits that would be lost.
|
|
720
791
|
*/
|
|
721
|
-
const runRestoreCommand = (
|
|
792
|
+
const runRestoreCommand = (
|
|
793
|
+
json: JsonMode,
|
|
794
|
+
out: ArtifactOut,
|
|
795
|
+
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
722
796
|
Effect.gen(function* () {
|
|
723
797
|
const git = yield* GitService
|
|
724
798
|
|
|
@@ -760,7 +834,7 @@ const runRestoreCommand = (out: ArtifactOut): Effect.Effect<void, Error, Command
|
|
|
760
834
|
{ kind: "outcome", command: restoredOutcome(tip, after.state) },
|
|
761
835
|
]
|
|
762
836
|
const required = emitScripts(steps).required
|
|
763
|
-
out
|
|
837
|
+
yield* writeScript(json, out, combinedScript(required, ""))
|
|
764
838
|
})
|
|
765
839
|
|
|
766
840
|
/**
|
|
@@ -849,19 +923,29 @@ const runNextCommand = (
|
|
|
849
923
|
const validateCommand = emitsValidatablePrompt(rendered)
|
|
850
924
|
? selfValidateCommand(rest.def, rest.state, rendered.mode!, rendered.file!)
|
|
851
925
|
: undefined
|
|
926
|
+
if (DEMAND_BRIEFING[fields.kind].suppressHeader) {
|
|
927
|
+
// A prompt's stdout is the agent's input, so its header goes to stderr;
|
|
928
|
+
// rendered as a header-showing kind to reuse the one header layout.
|
|
929
|
+
const shown = renderBeatPlain({ ...fields, kind: "message" })
|
|
930
|
+
yield* narrator.warn(shown.slice(0, shown.indexOf("\n\n")))
|
|
931
|
+
}
|
|
852
932
|
out.write(renderBeatPlain(fields, validateCommand))
|
|
853
933
|
}
|
|
854
934
|
})
|
|
855
935
|
|
|
856
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. */
|
|
857
|
-
const runValidateCommand = (
|
|
937
|
+
const runValidateCommand = (
|
|
938
|
+
json: JsonMode,
|
|
939
|
+
out: ArtifactOut,
|
|
940
|
+
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
858
941
|
Effect.gen(function* () {
|
|
859
942
|
const rest = yield* currentRest
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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`,
|
|
865
949
|
)
|
|
866
950
|
})
|
|
867
951
|
|
|
@@ -870,8 +954,10 @@ const runValidateCommand = (out: ArtifactOut): Effect.Effect<void, Error, Comman
|
|
|
870
954
|
* Resolves no workflow state and reads no config — this is what a seeded
|
|
871
955
|
* `validate:` command invokes as a leaf step, and it formats nothing.
|
|
872
956
|
*
|
|
873
|
-
* An
|
|
874
|
-
*
|
|
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.
|
|
875
961
|
*/
|
|
876
962
|
const runCheckCommand = (
|
|
877
963
|
mode: string,
|
|
@@ -883,7 +969,7 @@ const runCheckCommand = (
|
|
|
883
969
|
const format = steeringFormatFor(mode)
|
|
884
970
|
if (format === undefined) {
|
|
885
971
|
return yield* Effect.fail(
|
|
886
|
-
new
|
|
972
|
+
new GtdUsageError(
|
|
887
973
|
`gtd check: unknown mode "${mode}" — known modes: ${BUILT_IN_MODE_NAMES.join(", ")}`,
|
|
888
974
|
),
|
|
889
975
|
)
|
|
@@ -917,12 +1003,8 @@ const runCheckCommand = (
|
|
|
917
1003
|
return yield* runOpenQuestionsCheckCommand(format, file)
|
|
918
1004
|
}
|
|
919
1005
|
|
|
920
|
-
const
|
|
921
|
-
const
|
|
922
|
-
const errors =
|
|
923
|
-
content !== undefined
|
|
924
|
-
? format.validate(content).map((finding) => formatFinding(file, finding))
|
|
925
|
-
: []
|
|
1006
|
+
const content = yield* existingFile(file)
|
|
1007
|
+
const errors = format.validate(content).map((finding) => formatFinding(file, finding))
|
|
926
1008
|
|
|
927
1009
|
if (errors.length === 0) return
|
|
928
1010
|
|
|
@@ -934,6 +1016,14 @@ const runCheckCommand = (
|
|
|
934
1016
|
)
|
|
935
1017
|
})
|
|
936
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
|
+
|
|
937
1027
|
/**
|
|
938
1028
|
* The bare `gtd check --open-threads`: code-comment threads in the files the
|
|
939
1029
|
* current process changed. The one `check` form that reads process state; no
|
|
@@ -961,18 +1051,13 @@ const runCodeThreadsCheckCommand = (): Effect.Effect<void, Error, CommandRequire
|
|
|
961
1051
|
return yield* Effect.fail(new Error(lines.join("\n")))
|
|
962
1052
|
})
|
|
963
1053
|
|
|
964
|
-
/** 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. */
|
|
965
1055
|
const runOpenThreadsCheckCommand = (
|
|
966
1056
|
format: SteeringFormat,
|
|
967
1057
|
file: string,
|
|
968
1058
|
): Effect.Effect<void, Error, Workspace> =>
|
|
969
1059
|
Effect.gen(function* () {
|
|
970
|
-
const
|
|
971
|
-
const content = workspace.atPath(file)
|
|
972
|
-
if (content === undefined) {
|
|
973
|
-
return yield* Effect.fail(new Error(`gtd check: ${file} does not exist`))
|
|
974
|
-
}
|
|
975
|
-
|
|
1060
|
+
const content = yield* existingFile(file)
|
|
976
1061
|
const errors = openThreads(format, content).map(
|
|
977
1062
|
(t) => `${file}:${t.line + 1}: [^${t.name}]: ${t.firstMe}`,
|
|
978
1063
|
)
|
|
@@ -985,20 +1070,14 @@ const runOpenThreadsCheckCommand = (
|
|
|
985
1070
|
|
|
986
1071
|
/**
|
|
987
1072
|
* Runs the SAME `unansweredQuestions` predicate a flow's `openQuestions()`
|
|
988
|
-
* reads, so the gate script and the flow's own check stay in sync.
|
|
989
|
-
* 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.
|
|
990
1074
|
*/
|
|
991
1075
|
const runOpenQuestionsCheckCommand = (
|
|
992
1076
|
format: SteeringFormat,
|
|
993
1077
|
file: string,
|
|
994
1078
|
): Effect.Effect<void, Error, Workspace> =>
|
|
995
1079
|
Effect.gen(function* () {
|
|
996
|
-
const
|
|
997
|
-
const content = workspace.atPath(file)
|
|
998
|
-
if (content === undefined) {
|
|
999
|
-
return yield* Effect.fail(new Error(`gtd check: ${file} does not exist`))
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1080
|
+
const content = yield* existingFile(file)
|
|
1002
1081
|
const errors = unansweredQuestions(format, content).map(
|
|
1003
1082
|
(q) => `${file}:${q.headingLine + 1}: ${q.question}`,
|
|
1004
1083
|
)
|
|
@@ -1125,19 +1204,30 @@ const runUiCliCommand = (
|
|
|
1125
1204
|
*/
|
|
1126
1205
|
const assertRunningFromRepoRoot = (git: GitOperations, host: HostOps): Effect.Effect<void, Error> =>
|
|
1127
1206
|
Effect.gen(function* () {
|
|
1128
|
-
const topLevel = yield* git
|
|
1207
|
+
const topLevel = yield* git
|
|
1208
|
+
.topLevel()
|
|
1209
|
+
.pipe(
|
|
1210
|
+
Effect.mapError((e) =>
|
|
1211
|
+
/not a git repository/.test(e.message)
|
|
1212
|
+
? new Error("gtd: not a git repository — run gtd inside one (`git init` makes one)")
|
|
1213
|
+
: e,
|
|
1214
|
+
),
|
|
1215
|
+
)
|
|
1129
1216
|
const topReal = yield* host.realPath(topLevel)
|
|
1130
1217
|
const cwdReal = yield* host.realPath(host.root)
|
|
1131
1218
|
if (topReal !== cwdReal) {
|
|
1132
1219
|
return yield* Effect.fail(
|
|
1133
1220
|
new Error(
|
|
1134
|
-
`gtd
|
|
1135
|
-
`the current directory is ${host.root}`,
|
|
1221
|
+
`gtd: run gtd from the repository root — cd ${shellWord(topLevel)} ` +
|
|
1222
|
+
`(the current directory is ${host.root})`,
|
|
1136
1223
|
),
|
|
1137
1224
|
)
|
|
1138
1225
|
}
|
|
1139
1226
|
})
|
|
1140
1227
|
|
|
1228
|
+
const shellWord = (path: string): string =>
|
|
1229
|
+
/^[\w@%+=:,./-]+$/.test(path) ? path : `'${path.replace(/'/g, "'\\''")}'`
|
|
1230
|
+
|
|
1141
1231
|
/** A command that derives workflow state needs at least one commit too — a commitless repository has no HEAD to resolve a `Rest` at. */
|
|
1142
1232
|
const assertRepositoryHasCommits = (git: GitOperations): Effect.Effect<void, Error> =>
|
|
1143
1233
|
Effect.gen(function* () {
|
|
@@ -1145,7 +1235,7 @@ const assertRepositoryHasCommits = (git: GitOperations): Effect.Effect<void, Err
|
|
|
1145
1235
|
if (!hasCommits) {
|
|
1146
1236
|
return yield* Effect.fail(
|
|
1147
1237
|
new Error(
|
|
1148
|
-
"gtd
|
|
1238
|
+
"gtd: the repository has no commits yet — make an initial commit, then run gtd again",
|
|
1149
1239
|
),
|
|
1150
1240
|
)
|
|
1151
1241
|
}
|
|
@@ -1161,7 +1251,7 @@ const assertRepositoryHasCommits = (git: GitOperations): Effect.Effect<void, Err
|
|
|
1161
1251
|
export const needsOf = (kind: Command["kind"]): Needs => {
|
|
1162
1252
|
switch (kind) {
|
|
1163
1253
|
case "lsp":
|
|
1164
|
-
case "
|
|
1254
|
+
case "guide":
|
|
1165
1255
|
case "judgeRun":
|
|
1166
1256
|
case "strays":
|
|
1167
1257
|
return "none"
|
|
@@ -1178,7 +1268,7 @@ export const standaloneKinds = (): readonly Command["kind"][] => [
|
|
|
1178
1268
|
"lsp",
|
|
1179
1269
|
"check",
|
|
1180
1270
|
"uncheck",
|
|
1181
|
-
"
|
|
1271
|
+
"guide",
|
|
1182
1272
|
"judgeRun",
|
|
1183
1273
|
"strays",
|
|
1184
1274
|
]
|
|
@@ -1219,19 +1309,15 @@ const dispatchVoidCommand = (
|
|
|
1219
1309
|
out,
|
|
1220
1310
|
)
|
|
1221
1311
|
case "start":
|
|
1222
|
-
return runStartCommand(command
|
|
1223
|
-
case "door":
|
|
1224
|
-
return runDoorCommand(command.name, command.args, out)
|
|
1225
|
-
case "doors":
|
|
1226
|
-
return runDoorsCommand(json, out)
|
|
1312
|
+
return runStartCommand(command, json, out)
|
|
1227
1313
|
case "abandon":
|
|
1228
|
-
return runAbandonCommand(out)
|
|
1314
|
+
return runAbandonCommand(json, out)
|
|
1229
1315
|
case "restore":
|
|
1230
|
-
return runRestoreCommand(out)
|
|
1316
|
+
return runRestoreCommand(json, out)
|
|
1231
1317
|
case "next":
|
|
1232
1318
|
return runNextCommand(json, out)
|
|
1233
1319
|
case "validate":
|
|
1234
|
-
return runValidateCommand(out)
|
|
1320
|
+
return runValidateCommand(json, out)
|
|
1235
1321
|
case "check":
|
|
1236
1322
|
if (command.mode === undefined || command.file === undefined) {
|
|
1237
1323
|
return runCodeThreadsCheckCommand()
|
|
@@ -1244,8 +1330,8 @@ const dispatchVoidCommand = (
|
|
|
1244
1330
|
)
|
|
1245
1331
|
case "uncheck":
|
|
1246
1332
|
return runUncheckCommand(command.file)
|
|
1247
|
-
case "
|
|
1248
|
-
return
|
|
1333
|
+
case "guide":
|
|
1334
|
+
return runGuideCommand(out)
|
|
1249
1335
|
case "summary":
|
|
1250
1336
|
return runSummaryCommand(out)
|
|
1251
1337
|
case "base":
|
|
@@ -1265,7 +1351,7 @@ const dispatchVoidCommand = (
|
|
|
1265
1351
|
case "historyJudges":
|
|
1266
1352
|
return runHistoryJudgesCommand(command.refs, command.vars, command.jsonl, json, out)
|
|
1267
1353
|
case "historyFetch":
|
|
1268
|
-
return runHistoryFetchCommand(command.remote, out)
|
|
1354
|
+
return runHistoryFetchCommand(command.remote, json, out)
|
|
1269
1355
|
}
|
|
1270
1356
|
}
|
|
1271
1357
|
|