@pmelab/gtd 52.1.0 → 53.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 +91 -129
- package/claude/hooks/drive.ts +0 -3
- package/claude/hooks/register.tsx +3 -68
- package/claude/hooks/ship.ts +85 -1595
- package/dist/chunks/{reserved-DxB5CROj.mjs → reserved-CQ7amcKG.mjs} +45 -132
- package/dist/chunks/{review-CKMSdvgW.mjs → review-CNgN_V8T.mjs} +2 -2
- package/dist/chunks/{tail-QZc7Uz2U.mjs → tail-JZ9PAOwp.mjs} +2 -14
- package/dist/chunks/{wire-BJuZWR3C.mjs → wire-C12dahrH.mjs} +2 -130
- package/dist/flows.mjs +1 -16
- package/dist/gtd.bundle.mjs +476 -982
- package/dist/testkit.mjs +58 -417
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +112 -363
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +311 -674
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +1 -86
- package/src/cli/Cli.ts +13 -133
- package/src/cli/Metrics.test.ts +21 -31
- package/src/cli/Metrics.ts +25 -36
- package/src/cli/Program.test.ts +1 -35
- package/src/cli/Program.ts +5 -64
- package/src/cli/ToolCommands.ts +0 -3
- package/src/edge/Edge.test.ts +20 -322
- package/src/edge/Edge.ts +10 -181
- package/src/edge/index.ts +0 -1
- package/src/flows/runtime.test.ts +0 -27
- package/src/flows/runtime.ts +1 -49
- package/src/install/Install.ts +15 -24
- package/src/replay/Replay.test.ts +0 -169
- package/src/replay/Replay.ts +4 -93
- package/src/replay/index.ts +0 -1
- package/src/testkit/Layers.ts +0 -1
- package/src/wire/BeatDocument.ts +2 -12
- package/src/wire/BeatStatus.ts +0 -3
- package/src/wire/WireSchema.test.ts +0 -18
- package/src/wire/index.ts +0 -2
- package/src/wire/types.ts +0 -7
- package/src/workflow/Workflow.ts +1 -9
- package/src/workflows/feature/criteria.test.ts +1 -14
- package/src/workflows/feature/criteria.ts +6 -38
- package/src/workflows/feature/feature.ts +34 -118
- package/src/workflows/feature/hands-on-skip.feature +2 -70
- package/src/workflows/feature/index.ts +0 -1
- package/src/workflows/feature/one-step-land.feature +0 -4
- package/src/workflows/feature/prototype-rounds.feature +1 -1
- package/src/workflows/feature/prototype.feature +3 -3
- package/src/workflows/feature/prototype.ts +18 -29
- package/src/workflows/feature/refactor-absorbs.feature +3 -8
- package/src/workflows/feature/refactor-first.feature +0 -5
- package/src/workflows/feature/refactor-rebuild.feature +0 -6
- package/src/workflows/feature/refactor-review.feature +11 -11
- package/src/workflows/feature/refactor.ts +30 -64
- package/src/workflows/feature/review-gate.feature +1 -1
- package/src/workflows/feature/seam-rejection.feature +0 -4
- package/src/workflows/feature/seam.test.ts +7 -26
- package/src/workflows/feature/seam.ts +52 -165
- package/src/workflows/index.ts +0 -1
- package/src/workflows/shared/lenses.ts +0 -13
- package/src/workflows/shared/options.test.ts +2 -11
- package/src/workflows/shared/options.ts +4 -21
- package/src/workflows/shared/review.test.ts +3 -3
- package/src/workflows/shared/steps.ts +0 -2
- package/src/workflows/shared/text.test.ts +10 -80
- package/src/workflows/shared/text.ts +35 -211
- package/src/workflows/shared/vars.ts +2 -8
- package/src/workflows/shared/workflows.steps.ts +0 -13
- package/claude/hooks/context.ts +0 -66
- package/src/replay/Slices.test.ts +0 -73
- package/src/replay/Slices.ts +0 -45
- package/src/wire/ContextLimit.fixture.ts +0 -30
- package/src/wire/ContextLimit.test.ts +0 -96
- package/src/wire/ContextLimit.ts +0 -169
- package/src/wire/Slices.ts +0 -25
- package/src/wire/golden/1/slices.json +0 -11
- package/src/workflows/feature/context-limit.feature +0 -37
- package/src/workflows/feature/continuation-cap.feature +0 -69
- package/src/workflows/feature/continuation-rebuild.feature +0 -92
- package/src/workflows/feature/continuation-slices.feature +0 -68
- package/src/workflows/feature/continuation.feature +0 -209
- package/src/workflows/feature/slice-findings.feature +0 -256
- package/src/workflows/feature/slicing-cap.feature +0 -23
- package/src/workflows/feature/slicing.feature +0 -297
- package/src/workflows/feature/slicing.test.ts +0 -49
- package/src/workflows/feature/slicing.ts +0 -105
- package/src/workflows/shared/continuation.ts +0 -92
package/src/edge/Edge.test.ts
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
currentRun,
|
|
7
7
|
historyTrail,
|
|
8
8
|
noProcessUnderway,
|
|
9
|
-
processSlices,
|
|
10
9
|
renderRest,
|
|
11
10
|
restAt,
|
|
12
11
|
restIsIdle,
|
|
@@ -609,172 +608,15 @@ const withTipMessage = (repo: InMemRepo, edit: (message: string) => string): Git
|
|
|
609
608
|
}
|
|
610
609
|
}
|
|
611
610
|
|
|
612
|
-
describe("processSlices", () => {
|
|
613
|
-
const failure = async (repo: InMemRepo): Promise<string> => {
|
|
614
|
-
const exit = await provideExit(processSlices, repo)
|
|
615
|
-
expect(Exit.isFailure(exit)).toBe(true)
|
|
616
|
-
return Exit.isFailure(exit) ? String(exit.cause) : ""
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
it("never reports an open slice for a closed run whose replay no longer ends", async () => {
|
|
620
|
-
const repo = repoWith(LINEAR)
|
|
621
|
-
await land(repo, { "a.txt": "a\n" })
|
|
622
|
-
await land(repo, { "b.txt": "b\n" })
|
|
623
|
-
await land(repo, { "c.txt": "c\n" })
|
|
624
|
-
repo.writeFile(
|
|
625
|
-
"gtd.config.ts",
|
|
626
|
-
LINEAR.replace(
|
|
627
|
-
'await agent("checking", "check-prompt")',
|
|
628
|
-
'await agent("checking", "check-prompt")\n await human("idle", { message: "again" })\n await agent("polishing", "polish-prompt")',
|
|
629
|
-
),
|
|
630
|
-
)
|
|
631
|
-
// Stands for a flow change with no in-repo config edit (an upgraded
|
|
632
|
-
// out-of-repo import): every ref reads as the working tree, so the
|
|
633
|
-
// config-differs refusal stays quiet and only replay drift remains.
|
|
634
|
-
const git: GitOperations = {
|
|
635
|
-
...fakeGitOperations(repo),
|
|
636
|
-
readFileAtRef: (_ref, path) => Effect.succeed(repo.readFile(path) ?? ""),
|
|
637
|
-
}
|
|
638
|
-
const exit = await Effect.runPromiseExit(
|
|
639
|
-
processSlices.pipe(
|
|
640
|
-
Effect.provideService(GitService, git),
|
|
641
|
-
Effect.provide(testLayers(repo, {})),
|
|
642
|
-
),
|
|
643
|
-
)
|
|
644
|
-
expect(Exit.isFailure(exit)).toBe(true)
|
|
645
|
-
const message = Exit.isFailure(exit) ? String(exit.cause) : ""
|
|
646
|
-
expect(message).toContain(
|
|
647
|
-
`gtd slices: refused — ${repo.resolveRef("HEAD")!.slice(0, 7)} closes a process that does not end under today's config`,
|
|
648
|
-
)
|
|
649
|
-
})
|
|
650
|
-
|
|
651
|
-
it("never reports a closed slice for an underway run whose replay now ends", async () => {
|
|
652
|
-
const repo = repoWith(LINEAR)
|
|
653
|
-
await land(repo, { "a.txt": "a\n" })
|
|
654
|
-
await land(repo, { "b.txt": "b\n" })
|
|
655
|
-
repo.writeFile(
|
|
656
|
-
"gtd.config.ts",
|
|
657
|
-
LINEAR.replace(' await agent("checking", "check-prompt")\n', ""),
|
|
658
|
-
)
|
|
659
|
-
const message = await failure(repo)
|
|
660
|
-
expect(message).toContain("gtd slices: refused")
|
|
661
|
-
expect(message).toContain("the workflow changed under this process")
|
|
662
|
-
expect(message).toContain("gtd abandon")
|
|
663
|
-
expect(message).not.toContain("gtd slices: gtd:")
|
|
664
|
-
})
|
|
665
|
-
|
|
666
|
-
it("refuses with no process at HEAD, naming that HEAD neither closes nor sits inside one", async () => {
|
|
667
|
-
const repo = repoWith(LINEAR)
|
|
668
|
-
expect(await failure(repo)).toContain(
|
|
669
|
-
"gtd slices: refused — HEAD neither closes nor sits inside a process",
|
|
670
|
-
)
|
|
671
|
-
})
|
|
672
|
-
|
|
673
|
-
it("refuses a renamed step with replay's message, prefixed and without a gtd: lead", async () => {
|
|
674
|
-
const repo = repoWith(LINEAR)
|
|
675
|
-
await land(repo, { "a.txt": "a\n" })
|
|
676
|
-
repo.writeFile("gtd.config.ts", LINEAR.replace('agent("building"', 'agent("constructing"'))
|
|
677
|
-
const message = await failure(repo)
|
|
678
|
-
expect(message).toContain("gtd slices: refused — ")
|
|
679
|
-
expect(message).not.toContain("HEAD neither closes nor sits inside a process")
|
|
680
|
-
expect(message).not.toContain("refused — gtd: ")
|
|
681
|
-
expect(message).not.toContain("gtd slices: gtd:")
|
|
682
|
-
})
|
|
683
|
-
|
|
684
|
-
it("refuses a finished process whose config file was edited since", async () => {
|
|
685
|
-
const repo = repoWith(LINEAR)
|
|
686
|
-
await land(repo, { "a.txt": "a\n" })
|
|
687
|
-
await land(repo, { "b.txt": "b\n" })
|
|
688
|
-
await land(repo, { "c.txt": "c\n" })
|
|
689
|
-
repo.writeFile("gtd.config.ts", `${LINEAR}\n// edited\n`)
|
|
690
|
-
const message = await failure(repo)
|
|
691
|
-
expect(message).toContain(
|
|
692
|
-
`gtd slices: refused — config differs at ${repo.resolveRef("HEAD")!.slice(0, 7)}: gtd.config.ts. HEAD's process replays under today's config; revert or stash the edit to list its slices`,
|
|
693
|
-
)
|
|
694
|
-
})
|
|
695
|
-
|
|
696
|
-
const finishedProcess = async () => {
|
|
697
|
-
const repo = repoWith(LINEAR)
|
|
698
|
-
await land(repo, { "a.txt": "a\n" })
|
|
699
|
-
await land(repo, { "b.txt": "b\n" })
|
|
700
|
-
await land(repo, { "c.txt": "c\n" })
|
|
701
|
-
return repo
|
|
702
|
-
}
|
|
703
|
-
const slicesWith = (repo: InMemRepo, git: GitOperations) =>
|
|
704
|
-
Effect.runPromise(
|
|
705
|
-
processSlices.pipe(
|
|
706
|
-
Effect.provideService(GitService, git),
|
|
707
|
-
Effect.provide(testLayers(repo, {})),
|
|
708
|
-
),
|
|
709
|
-
)
|
|
710
|
-
|
|
711
|
-
it("warns about a closed run recorded by another version, slices unchanged", async () => {
|
|
712
|
-
const repo = await finishedProcess()
|
|
713
|
-
const plain = await provide(processSlices, repo)
|
|
714
|
-
const git = withTipMessage(repo, (m) =>
|
|
715
|
-
m.replace(`Gtd-Version: ${GTD_VERSION}`, "Gtd-Version: 0.0.1"),
|
|
716
|
-
)
|
|
717
|
-
const result = await slicesWith(repo, git)
|
|
718
|
-
expect(result.versionWarning).toContain("0.0.1")
|
|
719
|
-
expect(result.versionWarning).toContain(GTD_VERSION)
|
|
720
|
-
expect(result.slices).toEqual(plain.slices)
|
|
721
|
-
})
|
|
722
|
-
|
|
723
|
-
it("warns about an unknown version when the closing commit records none", async () => {
|
|
724
|
-
const repo = await finishedProcess()
|
|
725
|
-
const git = withTipMessage(repo, (m) => m.replace(/Gtd-Version: .*\n?/, ""))
|
|
726
|
-
expect((await slicesWith(repo, git)).versionWarning).toContain("an unknown gtd version")
|
|
727
|
-
})
|
|
728
|
-
|
|
729
|
-
it("does not warn for a closed run landed at the running version", async () => {
|
|
730
|
-
const repo = await finishedProcess()
|
|
731
|
-
expect((await provide(processSlices, repo)).versionWarning).toBeUndefined()
|
|
732
|
-
})
|
|
733
|
-
|
|
734
|
-
it("does not warn for an underway process", async () => {
|
|
735
|
-
const repo = repoWith(LINEAR)
|
|
736
|
-
await land(repo, { "a.txt": "a\n" })
|
|
737
|
-
const git = withTipMessage(repo, (m) =>
|
|
738
|
-
m.replace(`Gtd-Version: ${GTD_VERSION}`, "Gtd-Version: 0.0.1"),
|
|
739
|
-
)
|
|
740
|
-
expect((await slicesWith(repo, git)).versionWarning).toBeUndefined()
|
|
741
|
-
})
|
|
742
|
-
|
|
743
|
-
it("refuses on config drift before replay-ends drift", async () => {
|
|
744
|
-
const repo = await finishedProcess()
|
|
745
|
-
repo.writeFile(
|
|
746
|
-
"gtd.config.ts",
|
|
747
|
-
LINEAR.replace(
|
|
748
|
-
'await agent("checking", "check-prompt")',
|
|
749
|
-
'await agent("checking", "check-prompt")\n await human("idle", { message: "again" })\n await agent("polishing", "polish-prompt")',
|
|
750
|
-
),
|
|
751
|
-
)
|
|
752
|
-
const message = await failure(repo)
|
|
753
|
-
expect(message).toContain("config differs at")
|
|
754
|
-
expect(message).not.toContain("does not end")
|
|
755
|
-
})
|
|
756
|
-
|
|
757
|
-
it("still lists an underway process's slices after the same config edit", async () => {
|
|
758
|
-
const repo = repoWith(LINEAR)
|
|
759
|
-
await land(repo, { "a.txt": "a\n" })
|
|
760
|
-
repo.writeFile("gtd.config.ts", `${LINEAR}\n// edited\n`)
|
|
761
|
-
expect((await provide(processSlices, repo)).slices).toHaveLength(1)
|
|
762
|
-
})
|
|
763
|
-
})
|
|
764
|
-
|
|
765
611
|
describe("summaryFor", () => {
|
|
766
|
-
const
|
|
612
|
+
const SUMMARISED_FLOW = `import { agent, human, mark } from "@pmelab/gtd/flows"
|
|
767
613
|
|
|
768
614
|
export default async () => {
|
|
769
615
|
await human("idle", { message: "idle-message" })
|
|
770
616
|
mark("risk-tier", "high")
|
|
771
|
-
await
|
|
772
|
-
await agent("building", "build-prompt")
|
|
773
|
-
})
|
|
617
|
+
await agent("building", "build-prompt")
|
|
774
618
|
await human("gate", { message: "gate-message" })
|
|
775
|
-
await
|
|
776
|
-
await agent("checking", "check-prompt")
|
|
777
|
-
})
|
|
619
|
+
await agent("checking", "check-prompt")
|
|
778
620
|
}
|
|
779
621
|
|
|
780
622
|
export const summary = (it) => JSON.stringify(it)
|
|
@@ -799,197 +641,53 @@ export const summary = (it) => JSON.stringify(it)
|
|
|
799
641
|
processCost: number
|
|
800
642
|
processCostByModel: { model: string; cost: number }[]
|
|
801
643
|
marks: Mark[]
|
|
802
|
-
slice?: unknown
|
|
803
644
|
}
|
|
804
645
|
|
|
805
|
-
/** Entry (human, idle→building), build agent, human gate,
|
|
646
|
+
/** Entry (human, idle→building), build agent, human gate, check agent closing. */
|
|
806
647
|
const finished = async () => {
|
|
807
|
-
const repo = repoWith(
|
|
648
|
+
const repo = repoWith(SUMMARISED_FLOW)
|
|
808
649
|
const boundary = headOf(repo)
|
|
809
650
|
const entry = await landCost(repo, { "NOTE.md": "n\n" }, 1, "opus")
|
|
810
|
-
|
|
651
|
+
await landCost(repo, { "a.txt": "a\n" }, 10, "opus")
|
|
811
652
|
const gate = await landCost(repo, { "b.txt": "b\n" }, 100, "haiku")
|
|
812
653
|
const closing = await landCost(repo, { "c.txt": "c\n" }, 1000, "haiku")
|
|
813
|
-
return { repo, boundary, entry,
|
|
654
|
+
return { repo, boundary, entry, gate, closing }
|
|
814
655
|
}
|
|
815
|
-
const render = async (repo: InMemRepo
|
|
656
|
+
const render = async (repo: InMemRepo) => {
|
|
816
657
|
const run = await provide(summaryRun, repo)
|
|
817
|
-
const { prompt } = await provide(summaryFor(run
|
|
658
|
+
const { prompt } = await provide(summaryFor(run), repo)
|
|
818
659
|
return JSON.parse(prompt!) as Rendered
|
|
819
660
|
}
|
|
820
661
|
|
|
821
|
-
it("
|
|
822
|
-
const { repo, boundary,
|
|
823
|
-
expect((await render(repo, "build")).slice).toEqual({
|
|
824
|
-
name: "build",
|
|
825
|
-
base: boundary,
|
|
826
|
-
tip: built,
|
|
827
|
-
index: 1,
|
|
828
|
-
count: 2,
|
|
829
|
-
})
|
|
830
|
-
expect((await render(repo, "verify")).slice).toEqual({
|
|
831
|
-
name: "verify",
|
|
832
|
-
base: built,
|
|
833
|
-
tip: closing,
|
|
834
|
-
index: 2,
|
|
835
|
-
count: 2,
|
|
836
|
-
})
|
|
837
|
-
})
|
|
838
|
-
|
|
839
|
-
it("renders no slice key for the plain call", async () => {
|
|
840
|
-
const { repo } = await finished()
|
|
841
|
-
expect("slice" in (await render(repo))).toBe(false)
|
|
842
|
-
})
|
|
843
|
-
|
|
844
|
-
it("narrows range and human commits to the slice's window, base exclusive and tip inclusive", async () => {
|
|
845
|
-
const { repo, boundary, built, gate, closing } = await finished()
|
|
662
|
+
it("renders the whole process: its range, human commits, cost and marks", async () => {
|
|
663
|
+
const { repo, boundary, entry, gate, closing } = await finished()
|
|
846
664
|
const whole = await render(repo)
|
|
665
|
+
expect(whole.entryCommit).toBe(entry)
|
|
666
|
+
expect([whole.processBase, whole.processTip]).toEqual([boundary, closing])
|
|
847
667
|
expect(whole.humanCommits.map((c) => c.hash)).toEqual([gate])
|
|
848
|
-
const build = await render(repo, "build")
|
|
849
|
-
const verify = await render(repo, "verify")
|
|
850
|
-
// the gate commit falls in the gap between the slices and folds into the later one
|
|
851
|
-
expect([build.processBase, build.processTip]).toEqual([boundary, built])
|
|
852
|
-
expect([verify.processBase, verify.processTip]).toEqual([built, closing])
|
|
853
|
-
expect(build.humanCommits).toEqual([])
|
|
854
|
-
expect(verify.humanCommits.map((c) => c.hash)).toEqual([gate])
|
|
855
|
-
})
|
|
856
|
-
|
|
857
|
-
it("splits cost by slice so the parts sum to the whole, gap commits included", async () => {
|
|
858
|
-
const { repo } = await finished()
|
|
859
|
-
const whole = await render(repo)
|
|
860
|
-
const build = await render(repo, "build")
|
|
861
|
-
const verify = await render(repo, "verify")
|
|
862
668
|
expect(whole.processCost).toBe(1111)
|
|
863
|
-
expect(
|
|
864
|
-
expect(build.processCost).toBe(11)
|
|
865
|
-
expect(verify.processCost).toBe(1100)
|
|
866
|
-
const byModel = new Map<string, number>()
|
|
867
|
-
for (const part of [build, verify])
|
|
868
|
-
for (const m of part.processCostByModel)
|
|
869
|
-
byModel.set(m.model, (byModel.get(m.model) ?? 0) + m.cost)
|
|
870
|
-
expect([...byModel.entries()].sort()).toEqual(
|
|
871
|
-
whole.processCostByModel.map((m) => [m.model, m.cost] as const).sort(),
|
|
872
|
-
)
|
|
873
|
-
})
|
|
874
|
-
|
|
875
|
-
it("keeps entryCommit and marks whole-process under a slice", async () => {
|
|
876
|
-
const { repo, entry } = await finished()
|
|
877
|
-
const whole = await render(repo)
|
|
878
|
-
const verify = await render(repo, "verify")
|
|
879
|
-
expect(verify.entryCommit).toBe(entry)
|
|
880
|
-
expect(verify.entryCommit).toBe(whole.entryCommit)
|
|
881
|
-
expect(verify.marks).toEqual(whole.marks)
|
|
882
|
-
expect(verify.marks.length).toBeGreaterThan(0)
|
|
669
|
+
expect(whole.marks.length).toBeGreaterThan(0)
|
|
883
670
|
})
|
|
884
671
|
|
|
885
|
-
it("
|
|
886
|
-
const repo = repoWith(
|
|
672
|
+
it("has nothing to render on an empty trace", async () => {
|
|
673
|
+
const repo = repoWith(SUMMARISED_FLOW)
|
|
887
674
|
const run = await provide(summaryRun, repo)
|
|
888
|
-
const exit = await provideExit(summaryFor(run, "build"), repo)
|
|
889
|
-
expect(Exit.isFailure(exit) ? String(exit.cause) : "").toContain(
|
|
890
|
-
"gtd summary: refused — HEAD neither closes nor sits inside a process",
|
|
891
|
-
)
|
|
892
675
|
expect(await provide(summaryFor(run), repo)).toEqual({ prompt: undefined })
|
|
893
676
|
})
|
|
894
677
|
|
|
895
|
-
it("refuses config drift at a closing HEAD with the summary prefix", async () => {
|
|
896
|
-
const { repo } = await finished()
|
|
897
|
-
const run = await provide(summaryRun, repo)
|
|
898
|
-
repo.writeFile("gtd.config.ts", `${SLICED}\n// edited\n`)
|
|
899
|
-
const exit = await provideExit(summaryFor(run, "build"), repo)
|
|
900
|
-
const message = Exit.isFailure(exit) ? String(exit.cause) : ""
|
|
901
|
-
expect(message).toContain("gtd summary: refused — config differs at")
|
|
902
|
-
expect(message).toContain("revert or stash")
|
|
903
|
-
})
|
|
904
|
-
|
|
905
|
-
it.each([
|
|
906
|
-
{ recorded: GTD_VERSION, warned: false },
|
|
907
|
-
{ recorded: "0.0.1", warned: true },
|
|
908
|
-
])(
|
|
909
|
-
"refuses a closed process that does not end under today's config (recorded $recorded)",
|
|
910
|
-
async ({ recorded, warned }) => {
|
|
911
|
-
const { repo } = await finished()
|
|
912
|
-
const tip = withTipMessage(repo, (m) =>
|
|
913
|
-
m.replace(`Gtd-Version: ${GTD_VERSION}`, `Gtd-Version: ${recorded}`),
|
|
914
|
-
)
|
|
915
|
-
const run = await Effect.runPromise(
|
|
916
|
-
summaryRun.pipe(
|
|
917
|
-
Effect.provideService(GitService, tip),
|
|
918
|
-
Effect.provide(testLayers(repo, {})),
|
|
919
|
-
),
|
|
920
|
-
)
|
|
921
|
-
repo.writeFile(
|
|
922
|
-
"gtd.config.ts",
|
|
923
|
-
SLICED.replace(
|
|
924
|
-
'await agent("checking", "check-prompt")',
|
|
925
|
-
'await agent("checking", "check-prompt")\n await human("idle", { message: "again" })\n await agent("polishing", "polish-prompt")',
|
|
926
|
-
),
|
|
927
|
-
)
|
|
928
|
-
// Every ref reads as the working tree: only replay divergence remains.
|
|
929
|
-
const git: GitOperations = {
|
|
930
|
-
...tip,
|
|
931
|
-
readFileAtRef: (_ref, path) => Effect.succeed(repo.readFile(path) ?? ""),
|
|
932
|
-
}
|
|
933
|
-
const exit = await Effect.runPromiseExit(
|
|
934
|
-
summaryFor(run, "build").pipe(
|
|
935
|
-
Effect.provideService(GitService, git),
|
|
936
|
-
Effect.provide(testLayers(repo, {})),
|
|
937
|
-
),
|
|
938
|
-
)
|
|
939
|
-
const message = Exit.isFailure(exit) ? String(exit.cause) : ""
|
|
940
|
-
expect(message).toContain(
|
|
941
|
-
`gtd summary: refused — ${repo.resolveRef("HEAD")!.slice(0, 7)} closes a process that does not end under today's config`,
|
|
942
|
-
)
|
|
943
|
-
if (warned) expect(message).toContain(`recorded by gtd ${recorded}`)
|
|
944
|
-
else expect(message).not.toContain("recorded by gtd")
|
|
945
|
-
},
|
|
946
|
-
)
|
|
947
|
-
|
|
948
678
|
it("returns no prompt, not a failure, when an edited config no longer exports a summary", async () => {
|
|
949
679
|
const { repo } = await finished()
|
|
950
680
|
const run = await provide(summaryRun, repo)
|
|
951
|
-
repo.writeFile("gtd.config.ts",
|
|
952
|
-
expect(await provide(summaryFor(run
|
|
681
|
+
repo.writeFile("gtd.config.ts", SUMMARISED_FLOW.replace(/export const summary.*\n/, ""))
|
|
682
|
+
expect(await provide(summaryFor(run), repo)).toEqual({ prompt: undefined })
|
|
953
683
|
})
|
|
954
684
|
|
|
955
|
-
it("
|
|
685
|
+
it("still returns a prompt under a config edited since the close, never replaying", async () => {
|
|
956
686
|
const { repo } = await finished()
|
|
957
687
|
const run = await provide(summaryRun, repo)
|
|
958
|
-
repo.writeFile("gtd.config.ts", `${
|
|
688
|
+
repo.writeFile("gtd.config.ts", `${SUMMARISED_FLOW}\n// edited\n`)
|
|
959
689
|
expect((await provide(summaryFor(run), repo)).prompt).toBeDefined()
|
|
960
690
|
})
|
|
961
|
-
|
|
962
|
-
it("returns a version warning for a closing commit by another gtd version, not a failure", async () => {
|
|
963
|
-
const { repo } = await finished()
|
|
964
|
-
const git = withTipMessage(repo, (m) =>
|
|
965
|
-
m.replace(`Gtd-Version: ${GTD_VERSION}`, "Gtd-Version: 0.0.1"),
|
|
966
|
-
)
|
|
967
|
-
const result = await Effect.runPromise(
|
|
968
|
-
Effect.flatMap(summaryRun, (run) => summaryFor(run, "build")).pipe(
|
|
969
|
-
Effect.provideService(GitService, git),
|
|
970
|
-
Effect.provide(testLayers(repo, {})),
|
|
971
|
-
),
|
|
972
|
-
)
|
|
973
|
-
expect(result.versionWarning).toContain("0.0.1")
|
|
974
|
-
expect(result.prompt).toBeDefined()
|
|
975
|
-
})
|
|
976
|
-
|
|
977
|
-
it("an unknown slice name returns unknownSlice naming every slice, and keeps the version warning", async () => {
|
|
978
|
-
const { repo } = await finished()
|
|
979
|
-
const git = withTipMessage(repo, (m) =>
|
|
980
|
-
m.replace(`Gtd-Version: ${GTD_VERSION}`, "Gtd-Version: 0.0.1"),
|
|
981
|
-
)
|
|
982
|
-
const result = await Effect.runPromise(
|
|
983
|
-
Effect.flatMap(summaryRun, (run) => summaryFor(run, "nope")).pipe(
|
|
984
|
-
Effect.provideService(GitService, git),
|
|
985
|
-
Effect.provide(testLayers(repo, {})),
|
|
986
|
-
),
|
|
987
|
-
)
|
|
988
|
-
expect(result.prompt).toBeUndefined()
|
|
989
|
-
expect(result.unknownSlice).toContain('no slice named "nope"')
|
|
990
|
-
expect(result.unknownSlice).toContain("build, verify")
|
|
991
|
-
expect(result.versionWarning).toContain("0.0.1")
|
|
992
|
-
})
|
|
993
691
|
})
|
|
994
692
|
|
|
995
693
|
describe("restAt", () => {
|
package/src/edge/Edge.ts
CHANGED
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
parseCommitMessage,
|
|
39
39
|
replay,
|
|
40
40
|
scopeRunStart,
|
|
41
|
-
sliceRanges,
|
|
42
41
|
treeFromRecord,
|
|
43
42
|
type EpisodeCommit,
|
|
44
43
|
type JudgeVerdict,
|
|
@@ -46,16 +45,14 @@ import {
|
|
|
46
45
|
type ReplayOutcome,
|
|
47
46
|
type TreeView,
|
|
48
47
|
} from "../replay/index.js"
|
|
49
|
-
import type { Mark, ScopeAccess, SummaryContext
|
|
48
|
+
import type { Mark, ScopeAccess, SummaryContext } from "../flows/index.js"
|
|
50
49
|
import { steeringFormatFor } from "../steering/index.js"
|
|
51
50
|
import {
|
|
52
51
|
judgeJson,
|
|
53
52
|
UNATTRIBUTED_MODEL,
|
|
54
53
|
type ApprovalEntry,
|
|
55
54
|
type ModelCost,
|
|
56
|
-
type SliceEntry,
|
|
57
55
|
type StepAccess,
|
|
58
|
-
type StepContextLimit,
|
|
59
56
|
} from "../wire/index.js"
|
|
60
57
|
|
|
61
58
|
import { GTD_VERSION } from "../install/index.js"
|
|
@@ -724,13 +721,6 @@ type RequestOf<K extends ReachedStep["request"]["kind"]> = Extract<
|
|
|
724
721
|
{ kind: K }
|
|
725
722
|
>
|
|
726
723
|
|
|
727
|
-
const stepContext = (
|
|
728
|
-
context: { readonly soft?: number | undefined; readonly hard?: number | undefined } | undefined,
|
|
729
|
-
): StepContextLimit | undefined =>
|
|
730
|
-
context === undefined
|
|
731
|
-
? undefined
|
|
732
|
-
: { ...optional("soft", context.soft), ...optional("hard", context.hard) }
|
|
733
|
-
|
|
734
724
|
const promptDef = (
|
|
735
725
|
common: StepCommon,
|
|
736
726
|
request: RequestOf<"agent">,
|
|
@@ -744,7 +734,6 @@ const promptDef = (
|
|
|
744
734
|
...optional("system", request.options.system),
|
|
745
735
|
...optional("allowEmpty", request.options.allowEmpty),
|
|
746
736
|
...optional("keep", request.options.keep),
|
|
747
|
-
...optional("context", stepContext(request.options.context)),
|
|
748
737
|
...optional("skills", skills),
|
|
749
738
|
...optional("access", access),
|
|
750
739
|
})
|
|
@@ -830,7 +819,6 @@ export interface RestHints {
|
|
|
830
819
|
readonly mode?: string
|
|
831
820
|
readonly skills?: readonly string[]
|
|
832
821
|
readonly access?: StepAccess
|
|
833
|
-
readonly context?: StepContextLimit
|
|
834
822
|
}
|
|
835
823
|
|
|
836
824
|
const hintsOf = (def: StepDef): RestHints => ({
|
|
@@ -842,7 +830,6 @@ const hintsOf = (def: StepDef): RestHints => ({
|
|
|
842
830
|
...optional("mode", def.mode),
|
|
843
831
|
...optional("skills", def.skills),
|
|
844
832
|
...optional("access", def.access),
|
|
845
|
-
...optional("context", def.context),
|
|
846
833
|
})
|
|
847
834
|
|
|
848
835
|
const normalizeStatus = (raw: string): ChangeStatus => (raw === "A" ? "A" : raw === "D" ? "D" : "M")
|
|
@@ -973,15 +960,6 @@ const pendingChanges = (git: GitOperations, withIndex: boolean) =>
|
|
|
973
960
|
return [...worktree, ...(yield* git.stagedPaths()).filter((e) => !seen.has(e.path))]
|
|
974
961
|
})
|
|
975
962
|
|
|
976
|
-
const sliceRangesOf = (
|
|
977
|
-
slices: Parameters<typeof sliceRanges>[0],
|
|
978
|
-
run: { readonly startParentHash: string; readonly trace: readonly { readonly hash: string }[] },
|
|
979
|
-
) =>
|
|
980
|
-
sliceRanges(slices, {
|
|
981
|
-
base: run.startParentHash,
|
|
982
|
-
tip: run.trace.at(-1)?.hash ?? run.startParentHash,
|
|
983
|
-
})
|
|
984
|
-
|
|
985
963
|
/** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
|
|
986
964
|
export const restAt = (
|
|
987
965
|
ref: string | undefined,
|
|
@@ -1057,99 +1035,9 @@ export const restAt = (
|
|
|
1057
1035
|
|
|
1058
1036
|
export const currentRest: Effect.Effect<Rest, Error, RestRequirements> = restAt(undefined)
|
|
1059
1037
|
|
|
1060
|
-
export interface ProcessSlices {
|
|
1061
|
-
readonly slices: readonly SliceEntry[]
|
|
1062
|
-
readonly versionWarning?: string
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
1038
|
type EndedOutcome = Extract<ReplayOutcome, { kind: "ended" }>
|
|
1066
1039
|
type UnendedOutcome = Exclude<ReplayOutcome, { kind: "ended" }>
|
|
1067
1040
|
|
|
1068
|
-
/** Every slice refusal reads `<command>: refused — <reason>`; replay's own `gtd: ` lead is dropped. */
|
|
1069
|
-
const slicesDivergence = (command: string, run: ProcessRun, outcome: ReplayOutcome): Error => {
|
|
1070
|
-
const reason =
|
|
1071
|
-
outcome.kind === "rest" && run.closingHash !== undefined
|
|
1072
|
-
? `${run.closingHash.slice(0, 7)} closes a process that does not end under today's config`
|
|
1073
|
-
: (replayError(outcome)?.message.replace(/^gtd: /, "") ?? "replay failed")
|
|
1074
|
-
return new Error(`${command}: refused — ${reason}`)
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
/**
|
|
1078
|
-
* The replay `gtd slices` and `gtd summary --slice` share: the slice ranges of
|
|
1079
|
-
* `run`, plus whether HEAD closes it. Reads like `historyTrail`, never
|
|
1080
|
-
* `restAt`: it narrates and writes nothing.
|
|
1081
|
-
*/
|
|
1082
|
-
const replayedSlices = (
|
|
1083
|
-
config: ConfigOperations,
|
|
1084
|
-
run: ProcessRun,
|
|
1085
|
-
how: { readonly command: string; readonly configReason: string },
|
|
1086
|
-
): Effect.Effect<
|
|
1087
|
-
{
|
|
1088
|
-
readonly ranges: ReturnType<typeof sliceRangesOf>
|
|
1089
|
-
readonly closed: boolean
|
|
1090
|
-
readonly versionWarning?: string
|
|
1091
|
-
},
|
|
1092
|
-
Error,
|
|
1093
|
-
RestRequirements
|
|
1094
|
-
> =>
|
|
1095
|
-
Effect.gen(function* () {
|
|
1096
|
-
const workspace = yield* Workspace
|
|
1097
|
-
const host = yield* Host
|
|
1098
|
-
if (run.trace.length === 0) {
|
|
1099
|
-
return yield* Effect.fail(
|
|
1100
|
-
new Error(`${how.command}: refused — HEAD neither closes nor sits inside a process`),
|
|
1101
|
-
)
|
|
1102
|
-
}
|
|
1103
|
-
const closing = run.closingHash
|
|
1104
|
-
let outcome: ReplayOutcome
|
|
1105
|
-
let versionWarning: string | undefined
|
|
1106
|
-
if (closing !== undefined) {
|
|
1107
|
-
const replayed = yield* replayFinishedProcess(config, run, closing, {
|
|
1108
|
-
command: how.command,
|
|
1109
|
-
configReason: how.configReason,
|
|
1110
|
-
notEnded: (o) => slicesDivergence(how.command, run, o),
|
|
1111
|
-
})
|
|
1112
|
-
outcome = replayed.outcome
|
|
1113
|
-
versionWarning = replayed.versionWarning
|
|
1114
|
-
} else {
|
|
1115
|
-
const { setup } = yield* setupFor(config, run, workspace, host)
|
|
1116
|
-
outcome = yield* Effect.promise(() => replayFor(setup))
|
|
1117
|
-
if (outcome.kind !== "rest")
|
|
1118
|
-
return yield* Effect.fail(slicesDivergence(how.command, run, outcome))
|
|
1119
|
-
}
|
|
1120
|
-
return {
|
|
1121
|
-
ranges: sliceRangesOf(outcome.slices, run),
|
|
1122
|
-
closed: closing !== undefined,
|
|
1123
|
-
...(versionWarning !== undefined ? { versionWarning } : {}),
|
|
1124
|
-
}
|
|
1125
|
-
})
|
|
1126
|
-
|
|
1127
|
-
/**
|
|
1128
|
-
* The slices of the process HEAD closes or sits inside, current episode only,
|
|
1129
|
-
* stack order. Returns a version warning for a closed run.
|
|
1130
|
-
*/
|
|
1131
|
-
export const processSlices: Effect.Effect<ProcessSlices, Error, RestRequirements> = Effect.gen(
|
|
1132
|
-
function* () {
|
|
1133
|
-
const git = yield* GitService
|
|
1134
|
-
const config = yield* (yield* ConfigService).load
|
|
1135
|
-
const run = yield* runAtHead(git, config.workflow)
|
|
1136
|
-
const { ranges, closed, versionWarning } = yield* replayedSlices(config, run, {
|
|
1137
|
-
command: "gtd slices",
|
|
1138
|
-
configReason:
|
|
1139
|
-
"HEAD's process replays under today's config; revert or stash the edit to list its slices",
|
|
1140
|
-
})
|
|
1141
|
-
return {
|
|
1142
|
-
slices: ranges.map(({ name, base, tip, open }) => ({
|
|
1143
|
-
name,
|
|
1144
|
-
base,
|
|
1145
|
-
tip,
|
|
1146
|
-
closed: closed || !open,
|
|
1147
|
-
})),
|
|
1148
|
-
...(versionWarning !== undefined ? { versionWarning } : {}),
|
|
1149
|
-
}
|
|
1150
|
-
},
|
|
1151
|
-
)
|
|
1152
|
-
|
|
1153
1041
|
/**
|
|
1154
1042
|
* Whether `gtd --workflow <name>` can start a process here: the workflow must
|
|
1155
1043
|
* reach a step from the tree the opening commit would capture. That step is
|
|
@@ -1456,24 +1344,8 @@ export const snapshotFromRest = (
|
|
|
1456
1344
|
|
|
1457
1345
|
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
1458
1346
|
|
|
1459
|
-
const narrowToSlice = (
|
|
1460
|
-
run: ProcessRun,
|
|
1461
|
-
slice: SummarySlice | undefined,
|
|
1462
|
-
): Pick<ProcessRun, "trace" | "costEntries"> => {
|
|
1463
|
-
if (slice === undefined) return { trace: run.trace, costEntries: run.costEntries }
|
|
1464
|
-
// `base` is exclusive: -1 when it is the episode's start parent, before the first entry.
|
|
1465
|
-
const from =
|
|
1466
|
-
slice.base === run.startParentHash ? -1 : run.trace.findIndex((e) => e.hash === slice.base)
|
|
1467
|
-
const to = run.trace.findIndex((e) => e.hash === slice.tip)
|
|
1468
|
-
const trace = run.trace.slice(from + 1, to + 1)
|
|
1469
|
-
const hashes = new Set(trace.map((e) => e.hash))
|
|
1470
|
-
return { trace, costEntries: run.costEntries.filter((c) => hashes.has(c.hash)) }
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
1347
|
interface SummaryResult {
|
|
1474
1348
|
readonly prompt: string | undefined
|
|
1475
|
-
readonly versionWarning?: string
|
|
1476
|
-
readonly unknownSlice?: string
|
|
1477
1349
|
}
|
|
1478
1350
|
|
|
1479
1351
|
const humanCommitsOf = (
|
|
@@ -1486,77 +1358,34 @@ const humanCommitsOf = (
|
|
|
1486
1358
|
|
|
1487
1359
|
const summaryInput = (
|
|
1488
1360
|
run: ProcessRun,
|
|
1489
|
-
slice: SummarySlice | undefined,
|
|
1490
1361
|
settings: Pick<SummaryContext, "vars" | "env">,
|
|
1491
|
-
) => {
|
|
1362
|
+
): SummaryContext => {
|
|
1492
1363
|
const entryCommit = run.trace[0]!.hash
|
|
1493
|
-
const {
|
|
1494
|
-
const { total, byModel } = costTotals(costEntries)
|
|
1364
|
+
const { total, byModel } = costTotals(run.costEntries)
|
|
1495
1365
|
return {
|
|
1496
1366
|
entryCommit,
|
|
1497
|
-
processBase:
|
|
1498
|
-
processTip:
|
|
1499
|
-
humanCommits: humanCommitsOf(trace, entryCommit),
|
|
1367
|
+
processBase: run.startParentHash,
|
|
1368
|
+
processTip: run.trace[run.trace.length - 1]!.hash,
|
|
1369
|
+
humanCommits: humanCommitsOf(run.trace, entryCommit),
|
|
1500
1370
|
processCost: total,
|
|
1501
1371
|
processCostByModel: byModel,
|
|
1502
1372
|
marks: run.marks,
|
|
1503
1373
|
...settings,
|
|
1504
|
-
...(slice !== undefined ? { slice } : {}),
|
|
1505
1374
|
}
|
|
1506
1375
|
}
|
|
1507
1376
|
|
|
1508
|
-
const locateSlice = (
|
|
1509
|
-
config: ConfigOperations,
|
|
1510
|
-
run: ProcessRun,
|
|
1511
|
-
sliceName: string | undefined,
|
|
1512
|
-
): Effect.Effect<
|
|
1513
|
-
| { readonly slice?: SummarySlice; readonly versionWarning?: string }
|
|
1514
|
-
| { readonly unknownSlice: string; readonly versionWarning?: string },
|
|
1515
|
-
Error,
|
|
1516
|
-
RestRequirements
|
|
1517
|
-
> =>
|
|
1518
|
-
Effect.gen(function* () {
|
|
1519
|
-
if (sliceName === undefined) return {}
|
|
1520
|
-
const replayed = yield* replayedSlices(config, run, {
|
|
1521
|
-
command: "gtd summary",
|
|
1522
|
-
configReason:
|
|
1523
|
-
"HEAD's process replays under today's config; revert or stash the edit to summarize its slices",
|
|
1524
|
-
})
|
|
1525
|
-
const { ranges, versionWarning } = replayed
|
|
1526
|
-
const warn = versionWarning !== undefined ? { versionWarning } : {}
|
|
1527
|
-
const at = ranges.findIndex((r) => r.name === sliceName)
|
|
1528
|
-
if (at < 0) {
|
|
1529
|
-
return {
|
|
1530
|
-
unknownSlice: `gtd summary: refused — no slice named "${sliceName}"; the slices, in stack order: ${ranges.map((r) => r.name).join(", ")}`,
|
|
1531
|
-
...warn,
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
const { base, tip } = ranges[at]!
|
|
1535
|
-
return { slice: { name: sliceName, base, tip, index: at + 1, count: ranges.length }, ...warn }
|
|
1536
|
-
})
|
|
1537
|
-
|
|
1538
|
-
/**
|
|
1539
|
-
* A plain call never replays; under a slice, `entryCommit` and `marks` stay
|
|
1540
|
-
* whole-process.
|
|
1541
|
-
*/
|
|
1542
1377
|
export const summaryFor = (
|
|
1543
1378
|
run: ProcessRun,
|
|
1544
|
-
sliceName?: string,
|
|
1545
1379
|
): Effect.Effect<SummaryResult, Error, RestRequirements> =>
|
|
1546
1380
|
Effect.gen(function* () {
|
|
1547
1381
|
const config = yield* (yield* ConfigService).load
|
|
1548
1382
|
const host = yield* Host
|
|
1549
1383
|
const resolved = yield* workflowOf(config, run)
|
|
1550
1384
|
const summary = resolved.def.summary
|
|
1551
|
-
if (summary === undefined) return { prompt: undefined }
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
const { slice, versionWarning } = located
|
|
1556
|
-
const prompt = summary(
|
|
1557
|
-
summaryInput(run, slice, settingsFor(resolved, run.pinnedVars, host.env, {}, {})),
|
|
1558
|
-
)
|
|
1559
|
-
return { prompt, ...(versionWarning !== undefined ? { versionWarning } : {}) }
|
|
1385
|
+
if (summary === undefined || run.trace.length === 0) return { prompt: undefined }
|
|
1386
|
+
return {
|
|
1387
|
+
prompt: summary(summaryInput(run, settingsFor(resolved, run.pinnedVars, host.env, {}, {}))),
|
|
1388
|
+
}
|
|
1560
1389
|
})
|
|
1561
1390
|
|
|
1562
1391
|
export interface HistoryStep {
|