@pmelab/gtd 52.1.0 → 54.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.
Files changed (94) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +91 -129
  3. package/claude/hooks/drive.ts +0 -3
  4. package/claude/hooks/register.tsx +3 -68
  5. package/claude/hooks/ship.ts +85 -1595
  6. package/dist/chunks/{reserved-DxB5CROj.mjs → reserved-CQ7amcKG.mjs} +45 -132
  7. package/dist/chunks/{review-CKMSdvgW.mjs → review-CXzUXJHL.mjs} +12 -5
  8. package/dist/chunks/{tail-QZc7Uz2U.mjs → tail-JZ9PAOwp.mjs} +2 -14
  9. package/dist/chunks/{wire-BJuZWR3C.mjs → wire-C12dahrH.mjs} +2 -130
  10. package/dist/flows.mjs +1 -16
  11. package/dist/gtd.bundle.mjs +476 -982
  12. package/dist/testkit.mjs +58 -417
  13. package/dist/wire.mjs +2 -2
  14. package/dist/workflows/cleanup.mjs +2 -2
  15. package/dist/workflows/feature.mjs +112 -363
  16. package/dist/workflows/fix.mjs +2 -2
  17. package/dist/workflows/review.mjs +3 -3
  18. package/dist/workflows/rollout.mjs +1 -1
  19. package/dist/workflows.mjs +321 -677
  20. package/package.json +1 -1
  21. package/src/cli/Cli.test.ts +1 -86
  22. package/src/cli/Cli.ts +13 -133
  23. package/src/cli/Metrics.test.ts +21 -31
  24. package/src/cli/Metrics.ts +25 -36
  25. package/src/cli/Program.test.ts +1 -35
  26. package/src/cli/Program.ts +5 -64
  27. package/src/cli/ToolCommands.ts +0 -3
  28. package/src/edge/Edge.test.ts +20 -322
  29. package/src/edge/Edge.ts +10 -181
  30. package/src/edge/index.ts +0 -1
  31. package/src/flows/runtime.test.ts +0 -27
  32. package/src/flows/runtime.ts +1 -49
  33. package/src/install/Install.ts +15 -24
  34. package/src/replay/Replay.test.ts +0 -169
  35. package/src/replay/Replay.ts +4 -93
  36. package/src/replay/index.ts +0 -1
  37. package/src/testkit/Layers.ts +0 -1
  38. package/src/testkit/steps.ts +23 -0
  39. package/src/wire/BeatDocument.ts +2 -12
  40. package/src/wire/BeatStatus.ts +0 -3
  41. package/src/wire/WireSchema.test.ts +0 -18
  42. package/src/wire/index.ts +0 -2
  43. package/src/wire/types.ts +0 -7
  44. package/src/workflow/Workflow.ts +1 -9
  45. package/src/workflows/feature/criteria.test.ts +1 -14
  46. package/src/workflows/feature/criteria.ts +6 -38
  47. package/src/workflows/feature/feature.ts +34 -118
  48. package/src/workflows/feature/hands-on-skip.feature +2 -70
  49. package/src/workflows/feature/index.ts +0 -1
  50. package/src/workflows/feature/lens-review.feature +50 -0
  51. package/src/workflows/feature/one-step-land.feature +0 -4
  52. package/src/workflows/feature/prototype-rounds.feature +1 -1
  53. package/src/workflows/feature/prototype.feature +3 -3
  54. package/src/workflows/feature/prototype.ts +18 -29
  55. package/src/workflows/feature/refactor-absorbs.feature +3 -8
  56. package/src/workflows/feature/refactor-first.feature +0 -5
  57. package/src/workflows/feature/refactor-rebuild.feature +0 -6
  58. package/src/workflows/feature/refactor-review.feature +49 -11
  59. package/src/workflows/feature/refactor.ts +30 -64
  60. package/src/workflows/feature/review-gate.feature +1 -1
  61. package/src/workflows/feature/reviewer-walkthrough.feature +0 -31
  62. package/src/workflows/feature/seam-rejection.feature +0 -4
  63. package/src/workflows/feature/seam.test.ts +7 -26
  64. package/src/workflows/feature/seam.ts +52 -165
  65. package/src/workflows/index.ts +0 -1
  66. package/src/workflows/shared/lenses.ts +0 -13
  67. package/src/workflows/shared/options.test.ts +2 -11
  68. package/src/workflows/shared/options.ts +4 -21
  69. package/src/workflows/shared/review.test.ts +3 -3
  70. package/src/workflows/shared/review.ts +12 -2
  71. package/src/workflows/shared/steps.ts +0 -2
  72. package/src/workflows/shared/text.test.ts +10 -80
  73. package/src/workflows/shared/text.ts +35 -211
  74. package/src/workflows/shared/vars.ts +2 -8
  75. package/src/workflows/shared/workflows.steps.ts +0 -13
  76. package/claude/hooks/context.ts +0 -66
  77. package/src/replay/Slices.test.ts +0 -73
  78. package/src/replay/Slices.ts +0 -45
  79. package/src/wire/ContextLimit.fixture.ts +0 -30
  80. package/src/wire/ContextLimit.test.ts +0 -96
  81. package/src/wire/ContextLimit.ts +0 -169
  82. package/src/wire/Slices.ts +0 -25
  83. package/src/wire/golden/1/slices.json +0 -11
  84. package/src/workflows/feature/context-limit.feature +0 -37
  85. package/src/workflows/feature/continuation-cap.feature +0 -69
  86. package/src/workflows/feature/continuation-rebuild.feature +0 -92
  87. package/src/workflows/feature/continuation-slices.feature +0 -68
  88. package/src/workflows/feature/continuation.feature +0 -209
  89. package/src/workflows/feature/slice-findings.feature +0 -256
  90. package/src/workflows/feature/slicing-cap.feature +0 -23
  91. package/src/workflows/feature/slicing.feature +0 -297
  92. package/src/workflows/feature/slicing.test.ts +0 -49
  93. package/src/workflows/feature/slicing.ts +0 -105
  94. package/src/workflows/shared/continuation.ts +0 -92
@@ -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 SLICED = `import { agent, human, slice, mark } from "@pmelab/gtd/flows"
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 slice("build", async () => {
772
- await agent("building", "build-prompt")
773
- })
617
+ await agent("building", "build-prompt")
774
618
  await human("gate", { message: "gate-message" })
775
- await slice("verify", async () => {
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, verify agent closing. */
646
+ /** Entry (human, idle→building), build agent, human gate, check agent closing. */
806
647
  const finished = async () => {
807
- const repo = repoWith(SLICED)
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
- const built = await landCost(repo, { "a.txt": "a\n" }, 10, "opus")
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, built, gate, closing }
654
+ return { repo, boundary, entry, gate, closing }
814
655
  }
815
- const render = async (repo: InMemRepo, sliceName?: string) => {
656
+ const render = async (repo: InMemRepo) => {
816
657
  const run = await provide(summaryRun, repo)
817
- const { prompt } = await provide(summaryFor(run, sliceName), repo)
658
+ const { prompt } = await provide(summaryFor(run), repo)
818
659
  return JSON.parse(prompt!) as Rendered
819
660
  }
820
661
 
821
- it("resolves each slice with its range, 1-based index and count", async () => {
822
- const { repo, boundary, built, closing } = await finished()
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(build.processCost + verify.processCost).toBe(whole.processCost)
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("refuses a named slice on an empty trace, while the plain call has nothing to render", async () => {
886
- const repo = repoWith(SLICED)
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", SLICED.replace(/export const summary.*\n/, ""))
952
- expect(await provide(summaryFor(run, "build"), repo)).toEqual({ prompt: undefined })
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("plain summaryFor still returns a prompt under a config edited since the close, never replaying", async () => {
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", `${SLICED}\n// edited\n`)
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, SummarySlice } from "../flows/index.js"
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 { trace, costEntries } = narrowToSlice(run, slice)
1494
- const { total, byModel } = costTotals(costEntries)
1364
+ const { total, byModel } = costTotals(run.costEntries)
1495
1365
  return {
1496
1366
  entryCommit,
1497
- processBase: slice?.base ?? run.startParentHash,
1498
- processTip: slice?.tip ?? run.trace[run.trace.length - 1]!.hash,
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
- if (sliceName === undefined && run.trace.length === 0) return { prompt: undefined }
1553
- const located = yield* locateSlice(config, run, sliceName)
1554
- if ("unknownSlice" in located) return { prompt: undefined, ...located }
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 {