@pmelab/gtd 60.0.0 → 61.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 +21 -2
- package/dist/chunks/testkit-C9sq0x07.mjs +30758 -0
- package/dist/chunks/{wire-DZ8R1RU9.mjs → wire-CPNSm_9N.mjs} +27 -12
- package/dist/gtd.bundle.mjs +12238 -20706
- package/dist/gtd.core.mjs +593 -0
- package/dist/testkit/gherkin.mjs +2 -2
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +2 -2
- package/package.json +7 -7
- package/src/cli/Cli.test.ts +2 -2
- package/src/cli/Cli.ts +68 -51
- package/src/cli/History.test.ts +10 -1
- package/src/cli/Metrics.test.ts +10 -1
- package/src/cli/Output.test.ts +1 -1
- package/src/cli/Output.ts +3 -2
- package/src/cli/Program.test.ts +44 -25
- package/src/cli/Program.ts +39 -11
- package/src/cli/Strays.ts +3 -2
- package/src/cli/ToolCommands.ts +2 -2
- package/src/edge/Edge.test.ts +86 -31
- package/src/edge/Edge.ts +164 -31
- package/src/emit/GitScript.ts +21 -3
- package/src/emit/index.ts +3 -0
- package/src/flows/runtime.ts +1 -1
- package/src/gherkin/messages.test.ts +1 -0
- package/src/gherkin/module-config.feature +1 -0
- package/src/gherkin/more-steps.feature +1 -0
- package/src/gherkin/testkit.feature +2 -1
- package/src/install/Install.ts +9 -2
- 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/Format.test.ts +35 -8
- package/src/replay/Format.ts +7 -4
- package/src/replay/Replay.test.ts +54 -0
- package/src/replay/Replay.ts +61 -17
- package/src/replay/Trailers.test.ts +12 -12
- package/src/replay/Trailers.ts +35 -20
- package/src/replay/Tree.ts +11 -0
- package/src/replay/index.ts +3 -0
- package/src/step/LandStep.ts +1 -0
- package/src/step/RepoSnapshot.ts +2 -0
- package/src/step/planStart.test.ts +2 -2
- package/src/step/planStart.ts +3 -3
- package/src/step/planStep.test.ts +4 -4
- package/src/step/planStep.ts +10 -8
- package/src/testkit/TestRepo.test.ts +4 -1
- package/src/testkit/TestRepo.ts +3 -1
- package/src/twins/EmittedScriptRecognizer.ts +32 -1
- package/src/twins/EmittedScripts.test.ts +1 -1
- package/src/twins/GitTiers.ts +29 -7
- package/src/twins/InMemRepo.ts +14 -0
- package/src/twins/Layers.test.ts +1 -1
- package/src/twins/Layers.ts +10 -3
- package/src/twins/PinBeats.ts +33 -0
- package/src/twins/cliIo.ts +9 -4
- package/src/twins/index.ts +1 -0
- package/src/wire/BeatDocument.ts +26 -5
- package/src/wire/Demand.ts +16 -6
- package/src/wire/constants.ts +3 -0
- package/src/wire/types.ts +4 -0
- package/src/workflow/discovery.test.ts +24 -7
- package/src/workflow/discovery.ts +5 -6
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +60 -3
- package/src/workflow/load.ts +316 -166
- package/src/workflow/pin.test.ts +159 -0
- package/src/workflow/pin.ts +72 -0
- package/src/workflow/vars.ts +3 -3
- package/dist/chunks/testkit-BvecVjQ0.mjs +0 -32355
package/src/cli/Program.test.ts
CHANGED
|
@@ -18,7 +18,13 @@ vi.mock("node:os", async (importOriginal) => ({
|
|
|
18
18
|
import { runCli, type Command } from "./index.js"
|
|
19
19
|
import { EXIT_USAGE_ERROR, stallDiagnosis, noopText, WIRE_SCHEMA } from "../wire/index.js"
|
|
20
20
|
import { needsOf, runCommand, SelectorUsageError, type CommandRequirements } from "./Program.js"
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
InMemRepo,
|
|
23
|
+
makeCapturingCliIo,
|
|
24
|
+
testLayers,
|
|
25
|
+
applyEmittedScript,
|
|
26
|
+
takePinBeat,
|
|
27
|
+
} from "../twins/index.js"
|
|
22
28
|
import {
|
|
23
29
|
commitAll,
|
|
24
30
|
DID_NOT_RUN_COMMENT,
|
|
@@ -39,13 +45,14 @@ const run = async (
|
|
|
39
45
|
return result()
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
/** Lands via `gtd land --json` and applies the emitted script, as a driver would. */
|
|
48
|
+
/** Lands via `gtd land --json` and applies the emitted script, then takes the pin or unpin beat it leads to, as a driver would. */
|
|
43
49
|
const landAndApply = async (
|
|
44
50
|
repo: InMemRepo,
|
|
45
51
|
): Promise<{ readonly exitCode: number; readonly script: string }> => {
|
|
46
52
|
const { stdout, exitCode } = await run(repo, "land", "--json")
|
|
47
53
|
const script = exitCode === 0 ? (JSON.parse(stdout) as { readonly script: string }).script : ""
|
|
48
54
|
if (exitCode === 0) expect(applyEmittedScript(repo, new Map(), script).ok).toBe(true)
|
|
55
|
+
if (exitCode === 0) await takePinBeat(repo)
|
|
49
56
|
return { exitCode, script }
|
|
50
57
|
}
|
|
51
58
|
|
|
@@ -104,7 +111,7 @@ export const hotfix = workflow(
|
|
|
104
111
|
expect(exitCode).toBe(0)
|
|
105
112
|
expect(stdout).toContain(
|
|
106
113
|
commitAll(
|
|
107
|
-
`gtd(human): working\n\nGtd-Workflow: hotfix\nGtd-Var: greeting=hello\nGtd-Config: ${contentHash(WORKFLOW)} gtd.config.ts\nGtd-Format:
|
|
114
|
+
`gtd(human): working\n\nGtd-Workflow: hotfix\nGtd-Var: greeting=hello\nGtd-Config: ${contentHash(WORKFLOW)} gtd.config.ts\nGtd-Format: 2`,
|
|
108
115
|
),
|
|
109
116
|
)
|
|
110
117
|
expect(repo.commitHistory()).toHaveLength(before)
|
|
@@ -190,6 +197,9 @@ export const hotfix = workflow(
|
|
|
190
197
|
const { stdout } = await run(repo, "start", "hotfix")
|
|
191
198
|
expect(applyEmittedScript(repo, new Map(), stdout).ok).toBe(true)
|
|
192
199
|
expect(repo.lastCommitSubject()).toBe("gtd(human): working")
|
|
200
|
+
const pin = JSON.parse((await run(repo, "next", "--json")).stdout) as Record<string, unknown>
|
|
201
|
+
expect(pin).toMatchObject({ kind: "script", state: "pin", actor: "check", pin: "add" })
|
|
202
|
+
expect(await takePinBeat(repo, "add")).toBe(true)
|
|
193
203
|
const next = JSON.parse((await run(repo, "next", "--json")).stdout) as Record<string, unknown>
|
|
194
204
|
expect(next.kind).toBe("prompt")
|
|
195
205
|
expect(next.state).toBe("working")
|
|
@@ -496,6 +506,7 @@ export const resume = workflow(work)
|
|
|
496
506
|
const repo = seed(WORKFLOW)
|
|
497
507
|
const { stdout: entry } = await run(repo, "start", "resume")
|
|
498
508
|
expect(applyEmittedScript(repo, new Map(), entry).ok).toBe(true)
|
|
509
|
+
await takePinBeat(repo, "add")
|
|
499
510
|
const { stdout, exitCode } = await run(repo, "next", "--json")
|
|
500
511
|
expect(exitCode).toBe(0)
|
|
501
512
|
expect(JSON.parse(stdout).kind).toBe("prompt")
|
|
@@ -775,20 +786,20 @@ describe("gtd next — refuses when history diverges from what replay reaches",
|
|
|
775
786
|
expect(repo.commitHistory()).toHaveLength(before)
|
|
776
787
|
|
|
777
788
|
expect(applyEmittedScript(repo, new Map(), stdout).ok).toBe(true)
|
|
778
|
-
expect(repo.commitHistory()).toHaveLength(before -
|
|
789
|
+
expect(repo.commitHistory()).toHaveLength(before - 3)
|
|
779
790
|
})
|
|
780
791
|
})
|
|
781
792
|
|
|
782
793
|
describe("a history written in a format this gtd does not read", () => {
|
|
783
794
|
// The exact wording is pinned in Format.test.ts; this is the part that proves the format gate fired.
|
|
784
|
-
const FOREIGN = "is written in history format
|
|
795
|
+
const FOREIGN = "is written in history format 3, but this gtd reads formats 1 to 2"
|
|
785
796
|
|
|
786
797
|
/** A step commit that would also diverge, so only the format gate can explain the refusal. */
|
|
787
798
|
const foreignStep = async (): Promise<InMemRepo> => {
|
|
788
799
|
const repo = seed(IDLE_THEN_WORKING)
|
|
789
800
|
await landTurn(repo, { "NOTE.md": "a note\n" })
|
|
790
801
|
repo.writeFile("WORK.md", "done\n")
|
|
791
|
-
repo.seedCommit("gtd(agent): elsewhere → review\n\nGtd-Step: elsewhere#1\nGtd-Format:
|
|
802
|
+
repo.seedCommit("gtd(agent): elsewhere → review\n\nGtd-Step: elsewhere#1\nGtd-Format: 3")
|
|
792
803
|
return repo
|
|
793
804
|
}
|
|
794
805
|
|
|
@@ -813,7 +824,7 @@ describe("a history written in a format this gtd does not read", () => {
|
|
|
813
824
|
it("refuses even when a foreign subject hides the commit from the episode", async () => {
|
|
814
825
|
const repo = seed(IDLE_THEN_WORKING)
|
|
815
826
|
repo.writeFile("NOTE.md", "a note\n")
|
|
816
|
-
repo.seedCommit("gtd2[human] idle -> working\n\nGtd-Format:
|
|
827
|
+
repo.seedCommit("gtd2[human] idle -> working\n\nGtd-Format: 3")
|
|
817
828
|
for (const command of ["next", "abandon"]) {
|
|
818
829
|
const { exitCode, stderr } = await run(repo, command)
|
|
819
830
|
expect(exitCode).toBe(1)
|
|
@@ -824,7 +835,7 @@ describe("a history written in a format this gtd does not read", () => {
|
|
|
824
835
|
it("a commit gtd never wrote is not read for a format, even quoting the trailer", async () => {
|
|
825
836
|
const repo = seed(IDLE_THEN_WORKING)
|
|
826
837
|
repo.writeFile("NOTE.md", "a note\n")
|
|
827
|
-
repo.seedCommit("feat: x\n\nGtd-Format:
|
|
838
|
+
repo.seedCommit("feat: x\n\nGtd-Format: 3")
|
|
828
839
|
expect((await run(repo, "next")).exitCode).toBe(0)
|
|
829
840
|
})
|
|
830
841
|
|
|
@@ -832,7 +843,7 @@ describe("a history written in a format this gtd does not read", () => {
|
|
|
832
843
|
const repo = seed(IDLE_THEN_WORKING)
|
|
833
844
|
const base = repo.resolveRef("HEAD")!
|
|
834
845
|
repo.writeFile("NOTE.md", "a note\n")
|
|
835
|
-
repo.seedCommit("gtd(human): idle → working\n\nGtd-Step: idle#1\nGtd-Format:
|
|
846
|
+
repo.seedCommit("gtd(human): idle → working\n\nGtd-Step: idle#1\nGtd-Format: 3")
|
|
836
847
|
repo.updateRef(HISTORY_REF, repo.resolveRef("HEAD")!)
|
|
837
848
|
repo.hardResetTo(base)
|
|
838
849
|
const { exitCode, stderr } = await run(repo, "restore")
|
|
@@ -1390,10 +1401,10 @@ export default async () => {
|
|
|
1390
1401
|
expect(await nextOf(repo)).toEqual({ target: "accepted" })
|
|
1391
1402
|
})
|
|
1392
1403
|
|
|
1393
|
-
it("a turn that finishes
|
|
1404
|
+
it("a turn that finishes a pinned flow targets the unpin", async () => {
|
|
1394
1405
|
const repo = await atWorking()
|
|
1395
1406
|
repo.writeFile("REVIEW.md", "an updated review\n")
|
|
1396
|
-
expect(await nextOf(repo)).toMatchObject({ target: "
|
|
1407
|
+
expect(await nextOf(repo)).toMatchObject({ target: "unpin" })
|
|
1397
1408
|
})
|
|
1398
1409
|
|
|
1399
1410
|
it("plain gtd next shows the same preview as a `Next:` line", async () => {
|
|
@@ -1476,7 +1487,7 @@ export default async () => {
|
|
|
1476
1487
|
expect((JSON.parse(stdout) as { readonly script: string }).script).toContain("git commit")
|
|
1477
1488
|
})
|
|
1478
1489
|
|
|
1479
|
-
it("a clean run that finishes
|
|
1490
|
+
it("a clean run that finishes a pinned flow lands an ordinary commit at the unpin", async () => {
|
|
1480
1491
|
const repo = await atChecking(REENTRY_WORKFLOW)
|
|
1481
1492
|
const { stdout, exitCode } = await run(repo, "land", "--json")
|
|
1482
1493
|
expect(exitCode).toBe(0)
|
|
@@ -1485,7 +1496,7 @@ export default async () => {
|
|
|
1485
1496
|
expect(script).not.toContain("git reset --mixed")
|
|
1486
1497
|
expect(script).not.toContain("nothing to retain")
|
|
1487
1498
|
expect(applyEmittedScript(repo, new Map(), script).ok).toBe(true)
|
|
1488
|
-
expect(repo.lastCommitSubject()).toBe("gtd(check): checking →
|
|
1499
|
+
expect(repo.lastCommitSubject()).toBe("gtd(check): checking → unpin")
|
|
1489
1500
|
})
|
|
1490
1501
|
|
|
1491
1502
|
it("the same rest with a pending change lands an ordinary commit too", async () => {
|
|
@@ -1523,7 +1534,7 @@ export default async () => {
|
|
|
1523
1534
|
expect(parsed.script).toContain("nothing to do")
|
|
1524
1535
|
})
|
|
1525
1536
|
|
|
1526
|
-
it("gtd land --json a decision that
|
|
1537
|
+
it("gtd land --json a decision that ends a pinned flow reports settled:false, idle:false, state:unpin", async () => {
|
|
1527
1538
|
const repo = await atChecking(REENTRY_WORKFLOW)
|
|
1528
1539
|
const { stdout, exitCode } = await run(repo, "land", "--json")
|
|
1529
1540
|
expect(exitCode).toBe(0)
|
|
@@ -1533,8 +1544,8 @@ export default async () => {
|
|
|
1533
1544
|
readonly state: string
|
|
1534
1545
|
}
|
|
1535
1546
|
expect(parsed.settled).toBe(false)
|
|
1536
|
-
expect(parsed.idle).toBe(
|
|
1537
|
-
expect(parsed.state).toBe("
|
|
1547
|
+
expect(parsed.idle).toBe(false)
|
|
1548
|
+
expect(parsed.state).toBe("unpin")
|
|
1538
1549
|
})
|
|
1539
1550
|
|
|
1540
1551
|
it("gtd land --json an ordinary commit reports settled:false", async () => {
|
|
@@ -1683,7 +1694,7 @@ export default async () => {
|
|
|
1683
1694
|
const { exitCode, script } = await landAndApply(repo)
|
|
1684
1695
|
expect(exitCode).toBe(0)
|
|
1685
1696
|
expect(script).not.toContain("fmt-notes")
|
|
1686
|
-
expect(repo.lastCommitSubject()).toBe("gtd(agent): drafting →
|
|
1697
|
+
expect(repo.lastCommitSubject("HEAD~1")).toBe("gtd(agent): drafting → unpin")
|
|
1687
1698
|
})
|
|
1688
1699
|
|
|
1689
1700
|
it("`gtd next --json`'s `validate` field carries the mode's format command", async () => {
|
|
@@ -1712,6 +1723,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1712
1723
|
check: { kind: "check", mode: "qa", file: ".gtd/TODO.md" },
|
|
1713
1724
|
uncheck: { kind: "uncheck", file: ".gtd/REVIEW.md" },
|
|
1714
1725
|
guide: { kind: "guide" },
|
|
1726
|
+
bundle: { kind: "bundle" },
|
|
1715
1727
|
summary: { kind: "summary" },
|
|
1716
1728
|
base: { kind: "base" },
|
|
1717
1729
|
judge: { kind: "judge" },
|
|
@@ -1732,11 +1744,12 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1732
1744
|
const NO_COMMITS_MESSAGE =
|
|
1733
1745
|
"gtd: the repository has no commits yet — make an initial commit, then run gtd again"
|
|
1734
1746
|
|
|
1735
|
-
it("derives exactly the
|
|
1747
|
+
it("derives exactly the seventeen non-standalone kinds — a canary for the table-driven cases below", () => {
|
|
1736
1748
|
expect(stateKinds.sort()).toEqual(
|
|
1737
1749
|
[
|
|
1738
1750
|
"abandon",
|
|
1739
1751
|
"base",
|
|
1752
|
+
"bundle",
|
|
1740
1753
|
"start",
|
|
1741
1754
|
"land",
|
|
1742
1755
|
"next",
|
|
@@ -1761,7 +1774,10 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1761
1774
|
const repo = new InMemRepo()
|
|
1762
1775
|
const before = repo.commitHistory().length
|
|
1763
1776
|
const written: string[] = []
|
|
1764
|
-
const out = {
|
|
1777
|
+
const out = {
|
|
1778
|
+
write: (chunk: string | Uint8Array) => written.push(String(chunk)),
|
|
1779
|
+
flush: () => {},
|
|
1780
|
+
}
|
|
1765
1781
|
|
|
1766
1782
|
const exit = await Effect.runPromiseExit(
|
|
1767
1783
|
runCommand(commandFor[kind], { kind: "off" }, out).pipe(Effect.provide(testLayers(repo))),
|
|
@@ -1780,7 +1796,10 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1780
1796
|
it("a repository with a commit passes the guard and dispatches normally", async () => {
|
|
1781
1797
|
const repo = seedIdle()
|
|
1782
1798
|
const written: string[] = []
|
|
1783
|
-
const out = {
|
|
1799
|
+
const out = {
|
|
1800
|
+
write: (chunk: string | Uint8Array) => written.push(String(chunk)),
|
|
1801
|
+
flush: () => {},
|
|
1802
|
+
}
|
|
1784
1803
|
|
|
1785
1804
|
const exit = await Effect.runPromiseExit(
|
|
1786
1805
|
runCommand({ kind: "next" }, { kind: "off" }, out).pipe(Effect.provide(testLayers(repo))),
|
|
@@ -1859,7 +1878,7 @@ export default workflow(
|
|
|
1859
1878
|
it("writes the rendered summary, with a trailing newline, counting the human turns after the entry commit", async () => {
|
|
1860
1879
|
const repo = await reviewed()
|
|
1861
1880
|
const history = repo.commitHistory()
|
|
1862
|
-
const entryHash = history[history.length -
|
|
1881
|
+
const entryHash = history[history.length - 4]!.hash
|
|
1863
1882
|
const tipHash = history[history.length - 1]!.hash
|
|
1864
1883
|
|
|
1865
1884
|
const { exitCode, stdout } = await run(repo, "summary")
|
|
@@ -1953,7 +1972,7 @@ export default async () => {
|
|
|
1953
1972
|
it("refuses once the process has closed and HEAD rests at the initial step again", async () => {
|
|
1954
1973
|
const repo = await atDeciding()
|
|
1955
1974
|
await landTurn(repo)
|
|
1956
|
-
expect(repo.lastCommitSubject()).toBe("gtd(
|
|
1975
|
+
expect(repo.lastCommitSubject()).toBe("gtd(check): unpin → idle")
|
|
1957
1976
|
|
|
1958
1977
|
const { exitCode, stdout, stderr } = await run(repo, "base")
|
|
1959
1978
|
expect(exitCode).toBe(1)
|
|
@@ -2090,7 +2109,7 @@ describe("gtd next/land --json=<path> — the select branch", () => {
|
|
|
2090
2109
|
)
|
|
2091
2110
|
expect(Exit.isFailure(exit)).toBe(true)
|
|
2092
2111
|
if (Exit.isFailure(exit)) {
|
|
2093
|
-
const failure = Cause.
|
|
2112
|
+
const failure = Cause.findErrorOption(exit.cause)
|
|
2094
2113
|
expect(failure._tag).toBe("Some")
|
|
2095
2114
|
if (failure._tag === "Some") {
|
|
2096
2115
|
expect(failure.value).toBeInstanceOf(SelectorUsageError)
|
|
@@ -2297,8 +2316,8 @@ export default workflow(
|
|
|
2297
2316
|
describe("runCommand — the repository-root guard's messages", () => {
|
|
2298
2317
|
const guarded = async (
|
|
2299
2318
|
override: (
|
|
2300
|
-
git: Context.
|
|
2301
|
-
host: Context.
|
|
2319
|
+
git: Context.Service.Shape<typeof GitService>,
|
|
2320
|
+
host: Context.Service.Shape<typeof Host>,
|
|
2302
2321
|
) => Effect.Effect<unknown, Error, CommandRequirements>,
|
|
2303
2322
|
): Promise<string> => {
|
|
2304
2323
|
const exit = await Effect.runPromiseExit(
|
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,
|
|
@@ -44,6 +47,7 @@ import {
|
|
|
44
47
|
type StatusChange,
|
|
45
48
|
selectPath,
|
|
46
49
|
EXIT_OK,
|
|
50
|
+
PIN_PATH,
|
|
47
51
|
} from "../wire/index.js"
|
|
48
52
|
import {
|
|
49
53
|
currentRest,
|
|
@@ -77,6 +81,7 @@ import {
|
|
|
77
81
|
deleteRef,
|
|
78
82
|
hardResetTo,
|
|
79
83
|
mixedResetTo,
|
|
84
|
+
unpinScript,
|
|
80
85
|
ScriptSurface,
|
|
81
86
|
updateRef,
|
|
82
87
|
type RunnableScript,
|
|
@@ -88,7 +93,7 @@ import {
|
|
|
88
93
|
abandonNoopOutcome,
|
|
89
94
|
restoredOutcome,
|
|
90
95
|
} from "../emit/index.js"
|
|
91
|
-
import { renderBriefing, WORKFLOWS_REPO } from "../install/index.js"
|
|
96
|
+
import { GTD_VERSION, renderBriefing, WORKFLOWS_REPO } from "../install/index.js"
|
|
92
97
|
import { ToolCommands, type SelfGtd } from "./ToolCommands.js"
|
|
93
98
|
import {
|
|
94
99
|
normalizeScriptNewline,
|
|
@@ -119,7 +124,7 @@ import { runHistoryMetricsCommand } from "./Metrics.js"
|
|
|
119
124
|
* SAME type structurally off `currentRest`'s own success type rather than
|
|
120
125
|
* importing the name.
|
|
121
126
|
*/
|
|
122
|
-
type Rest = Effect.
|
|
127
|
+
type Rest = Effect.Success<typeof currentRest>
|
|
123
128
|
|
|
124
129
|
export type Command =
|
|
125
130
|
| { readonly kind: "lsp" }
|
|
@@ -161,6 +166,7 @@ export type Command =
|
|
|
161
166
|
}
|
|
162
167
|
| { readonly kind: "uncheck"; readonly file: string }
|
|
163
168
|
| { readonly kind: "guide" }
|
|
169
|
+
| { readonly kind: "bundle" }
|
|
164
170
|
| { readonly kind: "summary" }
|
|
165
171
|
| { readonly kind: "base" }
|
|
166
172
|
| { readonly kind: "judge" }
|
|
@@ -219,15 +225,34 @@ const runGuideCommand = (
|
|
|
219
225
|
const briefing = renderBriefing()
|
|
220
226
|
out.write(briefing.endsWith("\n") ? briefing : briefing + "\n")
|
|
221
227
|
const host = yield* Host
|
|
222
|
-
const topLevel = yield* Effect.
|
|
223
|
-
if (topLevel._tag === "
|
|
224
|
-
if ((yield* host.realPath(topLevel.
|
|
228
|
+
const topLevel = yield* Effect.result((yield* GitService).topLevel())
|
|
229
|
+
if (topLevel._tag === "Failure") return
|
|
230
|
+
if ((yield* host.realPath(topLevel.success)) !== (yield* host.realPath(host.root))) return
|
|
225
231
|
if ((yield* (yield* ConfigDiscovery).workflowModule(host.root, host.home)) !== undefined) return
|
|
226
232
|
yield* (yield* Narrator).warn(
|
|
227
233
|
`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)`,
|
|
228
234
|
)
|
|
229
235
|
})
|
|
230
236
|
|
|
237
|
+
/** Loads the workflow first, so a pin is never made of one gtd would refuse. */
|
|
238
|
+
const runBundleCommand = (
|
|
239
|
+
out: ArtifactOut,
|
|
240
|
+
): Effect.Effect<
|
|
241
|
+
void,
|
|
242
|
+
Error,
|
|
243
|
+
ConfigService | ConfigDiscovery | EngineCore | Host | Narrator | Workspace
|
|
244
|
+
> =>
|
|
245
|
+
Effect.gen(function* () {
|
|
246
|
+
yield* (yield* ConfigService).load
|
|
247
|
+
const host = yield* Host
|
|
248
|
+
const module = yield* (yield* ConfigDiscovery).workflowModule(host.root, host.home)
|
|
249
|
+
// Inside a pin, load read the pinned workflow, not a config on disk.
|
|
250
|
+
if (module === undefined) {
|
|
251
|
+
return yield* Effect.fail(new GtdError(`gtd bundle: no ${WORKFLOW_MODULE} here or above`))
|
|
252
|
+
}
|
|
253
|
+
out.write(yield* pin(module, host.root, GTD_VERSION))
|
|
254
|
+
})
|
|
255
|
+
|
|
231
256
|
/**
|
|
232
257
|
* `planLanding`'s result — a preview of what `gtd land` would do, plus the
|
|
233
258
|
* combined `script` the driver runs to do it. `script` is
|
|
@@ -457,7 +482,7 @@ const runJudgeRunCommand = (
|
|
|
457
482
|
})
|
|
458
483
|
|
|
459
484
|
/** 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. */
|
|
460
|
-
const VerdictAnswer = Schema.Union(Schema.String, Schema.Number, Schema.Boolean)
|
|
485
|
+
const VerdictAnswer = Schema.Union([Schema.String, Schema.Number, Schema.Boolean])
|
|
461
486
|
|
|
462
487
|
/**
|
|
463
488
|
* One `gtd judge answer` verdict entry per pending question, `{ id, answer,
|
|
@@ -470,14 +495,14 @@ const VerdictAnswer = Schema.Union(Schema.String, Schema.Number, Schema.Boolean)
|
|
|
470
495
|
const verdictSchemaFor = (ids: readonly string[]) =>
|
|
471
496
|
Schema.Array(
|
|
472
497
|
Schema.Struct({
|
|
473
|
-
id: ids.length > 0 ? Schema.
|
|
498
|
+
id: ids.length > 0 ? Schema.Literals(ids) : Schema.Never,
|
|
474
499
|
answer: VerdictAnswer,
|
|
475
500
|
// Bounded [0, 1] — every OTHER bound in the routes: matching path fails
|
|
476
501
|
// CLOSED (a non-finite rendered minP/maxP never matches); an
|
|
477
502
|
// unvalidated p wouldn't: a driver bug or a miscalibrated model
|
|
478
503
|
// sending p > 1 would clear every minP a workflow declares,
|
|
479
504
|
// unconditionally forcing the judged route on a live gate.
|
|
480
|
-
p: Schema.Number.
|
|
505
|
+
p: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 })),
|
|
481
506
|
}),
|
|
482
507
|
)
|
|
483
508
|
|
|
@@ -509,7 +534,7 @@ const runJudgeAnswerCommand = (
|
|
|
509
534
|
try: () => JSON.parse(raw) as unknown,
|
|
510
535
|
catch: () => new SelectorUsageError("gtd judge answer: stdin is not valid JSON"),
|
|
511
536
|
})
|
|
512
|
-
const verdict = yield* Schema.
|
|
537
|
+
const verdict = yield* Schema.decodeUnknownEffect(verdictSchemaFor(ids))(parsed).pipe(
|
|
513
538
|
Effect.mapError(
|
|
514
539
|
(e) =>
|
|
515
540
|
new SelectorUsageError(
|
|
@@ -776,6 +801,7 @@ const runAbandonCommand = (
|
|
|
776
801
|
const steps: EmitStep[] = [
|
|
777
802
|
{ kind: "gitWrite", command: updateRef(HISTORY_REF, tip) },
|
|
778
803
|
{ kind: "gitWrite", command: mixedResetTo(run.startParentHash) },
|
|
804
|
+
{ kind: "gitWrite", command: unpinScript(PIN_PATH) },
|
|
779
805
|
{ kind: "outcome", command: abandonedOutcome(restState, run.startParentHash, initial) },
|
|
780
806
|
]
|
|
781
807
|
const required = emitScripts(steps).required
|
|
@@ -1037,7 +1063,7 @@ const runCodeThreadsCheckCommand = (): Effect.Effect<void, Error, CommandRequire
|
|
|
1037
1063
|
const workspace = yield* Workspace
|
|
1038
1064
|
const changed = yield* git.changedPaths(rest.run.diffBase)
|
|
1039
1065
|
const lines = changed
|
|
1040
|
-
.filter((c) => c.status !== "D")
|
|
1066
|
+
.filter((c) => c.status !== "D" && c.path !== PIN_PATH)
|
|
1041
1067
|
.flatMap(({ path }) => {
|
|
1042
1068
|
const { threads, findings } = parseCodeThreads(path, workspace.atPath(path) ?? "")
|
|
1043
1069
|
return [
|
|
@@ -1137,7 +1163,7 @@ const gatherBeatDocument = (
|
|
|
1137
1163
|
kind === "prompt" ? resolveSession(rendered.memory, rendered.memoryResumed) : undefined
|
|
1138
1164
|
const resolvedValidate =
|
|
1139
1165
|
kind === "prompt"
|
|
1140
|
-
? yield* resolveValidateScript(rest).pipe(Effect.
|
|
1166
|
+
? yield* resolveValidateScript(rest).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
1141
1167
|
: undefined
|
|
1142
1168
|
const validate =
|
|
1143
1169
|
resolvedValidate !== undefined && resolvedValidate.script.length > 0
|
|
@@ -1332,6 +1358,8 @@ const dispatchVoidCommand = (
|
|
|
1332
1358
|
return runUncheckCommand(command.file)
|
|
1333
1359
|
case "guide":
|
|
1334
1360
|
return runGuideCommand(out)
|
|
1361
|
+
case "bundle":
|
|
1362
|
+
return runBundleCommand(out)
|
|
1335
1363
|
case "summary":
|
|
1336
1364
|
return runSummaryCommand(out)
|
|
1337
1365
|
case "base":
|
package/src/cli/Strays.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import { GitService, GtdError, Narrator, type GitOperations } from "../platform/index.js"
|
|
3
3
|
import { strayPaths } from "../step/index.js"
|
|
4
|
-
import { EXIT_OK, EXIT_STRAYS_FOUND } from "../wire/index.js"
|
|
4
|
+
import { EXIT_OK, EXIT_STRAYS_FOUND, PIN_PATH } from "../wire/index.js"
|
|
5
5
|
import {
|
|
6
6
|
SelectorUsageError,
|
|
7
7
|
writeSelection,
|
|
@@ -31,7 +31,8 @@ export const runStraysCommand = (
|
|
|
31
31
|
const narrator = yield* Narrator
|
|
32
32
|
const base = yield* resolveSide(git, command.base)
|
|
33
33
|
const tip = yield* resolveSide(git, command.tip)
|
|
34
|
-
|
|
34
|
+
// The pin is the engine's, never a turn's, so it is never a stray either.
|
|
35
|
+
const touched = (yield* git.diffPaths(base, tip)).filter((path) => path !== PIN_PATH)
|
|
35
36
|
const strays = strayPaths(touched, command.globs)
|
|
36
37
|
const total = touched.length
|
|
37
38
|
|
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") {}
|