gsd-pi 2.13.0 → 2.14.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 (99) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +1 -0
  3. package/dist/loader.js +50 -6
  4. package/dist/resource-loader.d.ts +7 -6
  5. package/dist/resource-loader.js +15 -8
  6. package/dist/resources/extensions/gsd/auto-worktree.ts +29 -183
  7. package/dist/resources/extensions/gsd/auto.ts +252 -370
  8. package/dist/resources/extensions/gsd/commands.ts +118 -34
  9. package/dist/resources/extensions/gsd/doctor.ts +29 -4
  10. package/dist/resources/extensions/gsd/git-self-heal.ts +0 -71
  11. package/dist/resources/extensions/gsd/git-service.ts +8 -431
  12. package/dist/resources/extensions/gsd/gitignore.ts +11 -4
  13. package/dist/resources/extensions/gsd/guided-flow.ts +141 -5
  14. package/dist/resources/extensions/gsd/preferences.ts +18 -17
  15. package/dist/resources/extensions/gsd/prompts/discuss.md +35 -0
  16. package/dist/resources/extensions/gsd/prompts/queue.md +7 -1
  17. package/dist/resources/extensions/gsd/state.ts +26 -8
  18. package/dist/resources/extensions/gsd/templates/state.md +0 -1
  19. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
  20. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
  21. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
  22. package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
  23. package/dist/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
  24. package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
  25. package/dist/resources/extensions/gsd/tests/git-service.test.ts +11 -770
  26. package/dist/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
  27. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
  28. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
  29. package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
  30. package/dist/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
  31. package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -426
  32. package/dist/resources/extensions/gsd/types.ts +0 -1
  33. package/dist/resources/extensions/gsd/worktree-manager.ts +7 -3
  34. package/dist/resources/extensions/gsd/worktree.ts +7 -65
  35. package/dist/resources/extensions/search-the-web/command-search-provider.ts +3 -1
  36. package/package.json +1 -1
  37. package/packages/pi-ai/dist/providers/google.d.ts.map +1 -1
  38. package/packages/pi-ai/dist/providers/google.js +12 -4
  39. package/packages/pi-ai/dist/providers/google.js.map +1 -1
  40. package/packages/pi-ai/dist/providers/mistral.d.ts.map +1 -1
  41. package/packages/pi-ai/dist/providers/mistral.js +10 -2
  42. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  43. package/packages/pi-ai/src/providers/google.ts +20 -8
  44. package/packages/pi-ai/src/providers/mistral.ts +14 -2
  45. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +3 -0
  46. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/extensions/loader.js +10 -7
  48. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  49. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -1
  50. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  51. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +4 -1
  52. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
  53. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  54. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +12 -3
  55. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  56. package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -9
  57. package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +4 -1
  58. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +14 -3
  59. package/packages/pi-tui/dist/components/input.d.ts +1 -0
  60. package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
  61. package/packages/pi-tui/dist/components/input.js +10 -0
  62. package/packages/pi-tui/dist/components/input.js.map +1 -1
  63. package/packages/pi-tui/src/components/input.ts +11 -0
  64. package/src/resources/extensions/gsd/auto-worktree.ts +29 -183
  65. package/src/resources/extensions/gsd/auto.ts +252 -370
  66. package/src/resources/extensions/gsd/commands.ts +118 -34
  67. package/src/resources/extensions/gsd/doctor.ts +29 -4
  68. package/src/resources/extensions/gsd/git-self-heal.ts +0 -71
  69. package/src/resources/extensions/gsd/git-service.ts +8 -431
  70. package/src/resources/extensions/gsd/gitignore.ts +11 -4
  71. package/src/resources/extensions/gsd/guided-flow.ts +141 -5
  72. package/src/resources/extensions/gsd/preferences.ts +18 -17
  73. package/src/resources/extensions/gsd/prompts/discuss.md +35 -0
  74. package/src/resources/extensions/gsd/prompts/queue.md +7 -1
  75. package/src/resources/extensions/gsd/state.ts +26 -8
  76. package/src/resources/extensions/gsd/templates/state.md +0 -1
  77. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
  78. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
  79. package/src/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
  80. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
  81. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
  82. package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
  83. package/src/resources/extensions/gsd/tests/git-service.test.ts +11 -770
  84. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
  85. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
  86. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
  87. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
  88. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
  89. package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -426
  90. package/src/resources/extensions/gsd/types.ts +0 -1
  91. package/src/resources/extensions/gsd/worktree-manager.ts +7 -3
  92. package/src/resources/extensions/gsd/worktree.ts +7 -65
  93. package/src/resources/extensions/search-the-web/command-search-provider.ts +3 -1
  94. package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
  95. package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
  96. package/dist/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
  97. package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
  98. package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
  99. package/src/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
@@ -13,7 +13,6 @@ import {
13
13
  writeIntegrationBranch,
14
14
  type GitPreferences,
15
15
  type CommitOptions,
16
- type MergeSliceResult,
17
16
  type PreMergeCheckResult,
18
17
  } from "../git-service.ts";
19
18
  import { createTestContext } from './test-helpers.ts';
@@ -195,8 +194,8 @@ async function main(): Promise<void> {
195
194
 
196
195
  assertEq(
197
196
  RUNTIME_EXCLUSION_PATHS.length,
198
- 7,
199
- "exactly 7 runtime exclusion paths"
197
+ 9,
198
+ "exactly 9 runtime exclusion paths"
200
199
  );
201
200
 
202
201
  const expectedPaths = [
@@ -207,6 +206,8 @@ async function main(): Promise<void> {
207
206
  ".gsd/metrics.json",
208
207
  ".gsd/completed-units.json",
209
208
  ".gsd/STATE.md",
209
+ ".gsd/gsd.db",
210
+ ".gsd/DISCUSSION-MANIFEST.json",
210
211
  ];
211
212
 
212
213
  assertEq(
@@ -261,10 +262,8 @@ async function main(): Promise<void> {
261
262
  // These are compile-time checks — if we got here, the types import fine
262
263
  const _prefs: GitPreferences = { auto_push: true, remote: "origin" };
263
264
  const _opts: CommitOptions = { message: "test" };
264
- const _result: MergeSliceResult = { branch: "main", mergedCommitMessage: "msg", deletedBranch: false };
265
265
  assertTrue(true, "GitPreferences type exported and usable");
266
266
  assertTrue(true, "CommitOptions type exported and usable");
267
- assertTrue(true, "MergeSliceResult type exported and usable");
268
267
 
269
268
  // Cleanup T01 temp dir
270
269
  rmSync(tempDir, { recursive: true, force: true });
@@ -534,7 +533,7 @@ async function main(): Promise<void> {
534
533
  return dir;
535
534
  }
536
535
 
537
- // ─── getCurrentBranch / isOnSliceBranch / getActiveSliceBranch ─────────
536
+ // ─── getCurrentBranch ────────────────────────────────────────────────
538
537
 
539
538
  console.log("\n=== Branch queries ===");
540
539
 
@@ -542,21 +541,13 @@ async function main(): Promise<void> {
542
541
  const repo = initBranchTestRepo();
543
542
  const svc = new GitServiceImpl(repo);
544
543
 
545
- // On main
546
544
  assertEq(svc.getCurrentBranch(), "main", "getCurrentBranch returns main on main branch");
547
- assertEq(svc.isOnSliceBranch(), false, "isOnSliceBranch returns false on main");
548
- assertEq(svc.getActiveSliceBranch(), null, "getActiveSliceBranch returns null on main");
549
545
 
550
- // Create and checkout a slice branch manually
551
546
  run("git checkout -b gsd/M001/S01", repo);
552
547
  assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "getCurrentBranch returns slice branch name");
553
- assertEq(svc.isOnSliceBranch(), true, "isOnSliceBranch returns true on slice branch");
554
- assertEq(svc.getActiveSliceBranch(), "gsd/M001/S01", "getActiveSliceBranch returns branch name on slice branch");
555
548
 
556
- // Non-slice feature branch
557
549
  run("git checkout -b feature/foo", repo);
558
- assertEq(svc.isOnSliceBranch(), false, "isOnSliceBranch returns false on non-slice branch");
559
- assertEq(svc.getActiveSliceBranch(), null, "getActiveSliceBranch returns null on non-slice branch");
550
+ assertEq(svc.getCurrentBranch(), "feature/foo", "getCurrentBranch returns feature branch name");
560
551
 
561
552
  rmSync(repo, { recursive: true, force: true });
562
553
  }
@@ -591,486 +582,8 @@ async function main(): Promise<void> {
591
582
  rmSync(repo, { recursive: true, force: true });
592
583
  }
593
584
 
594
- // ─── ensureSliceBranch: creates and checks out ────────────────────────
595
-
596
- console.log("\n=== ensureSliceBranch ===");
597
-
598
- {
599
- const repo = initBranchTestRepo();
600
- const svc = new GitServiceImpl(repo);
601
-
602
- const created = svc.ensureSliceBranch("M001", "S01");
603
- assertEq(created, true, "ensureSliceBranch returns true on first call (branch created)");
604
- assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "ensureSliceBranch checks out the slice branch");
605
-
606
- rmSync(repo, { recursive: true, force: true });
607
- }
608
-
609
- // ─── ensureSliceBranch: idempotent ────────────────────────────────────
610
-
611
- console.log("\n=== ensureSliceBranch: idempotent ===");
612
-
613
- {
614
- const repo = initBranchTestRepo();
615
- const svc = new GitServiceImpl(repo);
616
-
617
- svc.ensureSliceBranch("M001", "S01");
618
- const secondCall = svc.ensureSliceBranch("M001", "S01");
619
- assertEq(secondCall, false, "ensureSliceBranch returns false when already on the branch");
620
- assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "still on slice branch after idempotent call");
621
-
622
- rmSync(repo, { recursive: true, force: true });
623
- }
624
-
625
- // ─── ensureSliceBranch: from non-main working branch inherits artifacts ──
626
-
627
- console.log("\n=== ensureSliceBranch: from non-main inherits artifacts ===");
628
-
629
- {
630
- const repo = initBranchTestRepo();
631
- const svc = new GitServiceImpl(repo);
632
-
633
- // Create a feature branch with planning artifacts
634
- run("git checkout -b developer", repo);
635
- createFile(repo, ".gsd/milestones/M001/M001-ROADMAP.md", "# Roadmap");
636
- run("git add -A", repo);
637
- run('git commit -m "add roadmap"', repo);
638
-
639
- // ensureSliceBranch from this non-main, non-slice branch
640
- const created = svc.ensureSliceBranch("M001", "S01");
641
- assertEq(created, true, "branch created from non-main working branch");
642
- assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "checked out to slice branch");
643
-
644
- // The roadmap from developer branch should be present
645
- const logOutput = run("git log --oneline", repo);
646
- assertTrue(logOutput.includes("add roadmap"), "slice branch inherits artifacts from working branch");
647
-
648
- rmSync(repo, { recursive: true, force: true });
649
- }
650
-
651
- // ─── ensureSliceBranch: from another slice branch falls back to main ──
652
-
653
- console.log("\n=== ensureSliceBranch: from slice branch falls back to main ===");
654
-
655
- {
656
- const repo = initBranchTestRepo();
657
- const svc = new GitServiceImpl(repo);
658
-
659
- // Create file only on main
660
- createFile(repo, "main-only.txt", "from main");
661
- run("git add -A", repo);
662
- run('git commit -m "main-only file"', repo);
663
-
664
- // Create and check out S01
665
- svc.ensureSliceBranch("M001", "S01");
666
- // Add a file only on S01
667
- createFile(repo, "s01-only.txt", "from s01");
668
- run("git add -A", repo);
669
- run('git commit -m "S01 work"', repo);
670
-
671
- // Now create S02 from S01 — should fall back to main
672
- const created = svc.ensureSliceBranch("M001", "S02");
673
- assertEq(created, true, "S02 branch created from S01 (fell back to main)");
674
- assertEq(svc.getCurrentBranch(), "gsd/M001/S02", "on S02 branch");
675
-
676
- // S02 should NOT have the S01-only file (it branched from main)
677
- const showFiles = run("git ls-files", repo);
678
- assertTrue(!showFiles.includes("s01-only.txt"), "S02 does not have S01-only files (branched from main)");
679
- assertTrue(showFiles.includes("main-only.txt"), "S02 has main files");
680
-
681
- rmSync(repo, { recursive: true, force: true });
682
- }
683
-
684
- // ─── ensureSliceBranch: auto-commits dirty files via smart staging ────
685
-
686
- console.log("\n=== ensureSliceBranch: auto-commits with smart staging ===");
687
-
688
- {
689
- const repo = initBranchTestRepo();
690
- const svc = new GitServiceImpl(repo);
691
-
692
- // Create dirty files: both real and runtime
693
- createFile(repo, "src/feature.ts", "export const y = 2;");
694
- createFile(repo, ".gsd/activity/session.jsonl", "session data");
695
- createFile(repo, ".gsd/STATE.md", "# Current State");
696
- createFile(repo, ".gsd/metrics.json", '{"tasks":1}');
697
-
698
- // ensureSliceBranch should auto-commit before checkout
699
- svc.ensureSliceBranch("M001", "S01");
700
-
701
- // The auto-commit on main should have src/feature.ts but NOT runtime files
702
- run("git checkout main", repo);
703
- const showStat = run("git show --stat --format= HEAD", repo);
704
- assertTrue(showStat.includes("src/feature.ts"), "auto-commit includes real files");
705
- assertTrue(!showStat.includes(".gsd/activity"), "auto-commit excludes .gsd/activity/ (smart staging)");
706
- assertTrue(!showStat.includes("STATE.md"), "auto-commit excludes .gsd/STATE.md (smart staging)");
707
- assertTrue(!showStat.includes("metrics.json"), "auto-commit excludes .gsd/metrics.json (smart staging)");
708
-
709
- rmSync(repo, { recursive: true, force: true });
710
- }
711
-
712
- // ─── ensureSliceBranch: tracked STATE.md + dirty (regression: "local changes overwritten") ─
713
- //
714
- // Reproduces: "error: Your local changes to the following files would be overwritten
715
- // by checkout: .gsd/STATE.md" that occurred in gsd auto when STATE.md was historically
716
- // committed to the repo (before it was added to .gitignore).
717
-
718
- console.log("\n=== ensureSliceBranch: tracked STATE.md + dirty (checkout conflict regression) ===");
719
-
720
- {
721
- const repo = initBranchTestRepo();
722
- const svc = new GitServiceImpl(repo);
723
-
724
- // Simulate historical state: STATE.md was committed before gitignore was configured
725
- createFile(repo, ".gsd/STATE.md", "# State v1");
726
- run("git add -f .gsd/STATE.md", repo);
727
- run('git commit -m "add state (pre-gitignore)"', repo);
728
-
729
- // STATE.md gets modified during runtime (dirty)
730
- createFile(repo, ".gsd/STATE.md", "# State v2 (modified at runtime)");
731
-
732
- // ensureSliceBranch must not fail with "local changes would be overwritten"
733
- svc.ensureSliceBranch("M001", "S01");
734
- assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "checked out slice branch despite tracked+dirty STATE.md");
735
-
736
- rmSync(repo, { recursive: true, force: true });
737
- }
738
-
739
- // ─── ensureSliceBranch: untracked STATE.md blocks checkout (regression: cleanup-commit edge case) ─
740
- //
741
- // Reproduces: "The following untracked working tree files would be overwritten by checkout:
742
- // .gsd/STATE.md" when the smartStage cleanup commit removes STATE.md from the current
743
- // branch's HEAD but the target branch was already created from the old HEAD (so it still
744
- // has STATE.md tracked). Without discardUntrackedRuntimeFiles(), the untracked STATE.md
745
- // on disk would block the checkout.
746
-
747
- console.log("\n=== ensureSliceBranch: untracked runtime files blocked by target branch (cleanup-commit edge case) ===");
748
-
749
- {
750
- const repo = initBranchTestRepo();
751
-
752
- // Simulate: STATE.md is tracked in main's HEAD (historical state)
753
- createFile(repo, ".gsd/STATE.md", "# State original");
754
- run("git add -f .gsd/STATE.md", repo);
755
- run('git commit -m "initial with tracked STATE.md"', repo);
756
-
757
- // Simulate what smartStage one-time cleanup does: remove STATE.md from index and commit.
758
- // This leaves STATE.md on disk but removes it from main's HEAD.
759
- run("git rm --cached .gsd/STATE.md", repo);
760
- run('git commit -m "chore: untrack runtime files"', repo);
761
-
762
- // STATE.md exists on disk (modified) but is now untracked in main's HEAD
763
- createFile(repo, ".gsd/STATE.md", "# State modified after cleanup");
764
-
765
- // Create slice branch — this is what ensureSliceBranch does internally but we
766
- // simulate a GitServiceImpl that has already done the cleanup commit.
767
- // The slice branch is created from the OLD HEAD (before cleanup commit) so it HAS
768
- // STATE.md tracked. Without discardUntrackedRuntimeFiles(), the checkout would fail.
769
- run("git branch gsd/M001/S01 HEAD~1", repo); // branch from HEAD~1 = the commit that had STATE.md
770
-
771
- // Now use GitServiceImpl to switch to the already-existing slice branch
772
- const svc = new GitServiceImpl(repo);
773
-
774
- // ensureSliceBranch must succeed despite the untracked STATE.md on disk
775
- // conflicting with the tracked STATE.md in the target branch
776
- svc.ensureSliceBranch("M001", "S01");
777
- assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "checked out slice branch (untracked runtime file removed before checkout)");
778
-
779
- rmSync(repo, { recursive: true, force: true });
780
- }
781
-
782
- // ─── switchToMain: tracked STATE.md + dirty (regression) ─────────────
783
-
784
- console.log("\n=== switchToMain: tracked STATE.md + dirty (checkout conflict regression) ===");
785
-
786
- {
787
- const repo = initBranchTestRepo();
788
- const svc = new GitServiceImpl(repo);
789
-
790
- // Track STATE.md on main (historical pre-gitignore state)
791
- createFile(repo, ".gsd/STATE.md", "# State on main");
792
- run("git add -f .gsd/STATE.md", repo);
793
- run('git commit -m "add state (pre-gitignore)"', repo);
794
-
795
- // Create slice branch (inherits STATE.md from main)
796
- svc.ensureSliceBranch("M001", "S01");
797
- assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "on slice branch before switchToMain");
798
-
799
- // Modify STATE.md on slice branch (runtime update)
800
- createFile(repo, ".gsd/STATE.md", "# State updated on slice branch");
801
-
802
- // switchToMain must not fail with "local changes would be overwritten"
803
- svc.switchToMain();
804
- assertEq(svc.getCurrentBranch(), svc.getMainBranch(), "back on main after switchToMain despite tracked+dirty STATE.md");
805
-
806
- rmSync(repo, { recursive: true, force: true });
807
- }
808
-
809
- // ─── switchToMain ─────────────────────────────────────────────────────
810
-
811
- console.log("\n=== switchToMain ===");
812
-
813
- {
814
- const repo = initBranchTestRepo();
815
- const svc = new GitServiceImpl(repo);
816
-
817
- // Switch to a slice branch first
818
- svc.ensureSliceBranch("M001", "S01");
819
- assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "on slice branch before switchToMain");
820
-
821
- // Create dirty files
822
- createFile(repo, "src/work.ts", "work in progress");
823
- createFile(repo, ".gsd/activity/log.jsonl", "activity log");
824
- createFile(repo, ".gsd/runtime/state.json", '{"running":true}');
825
-
826
- svc.switchToMain();
827
- assertEq(svc.getCurrentBranch(), "main", "switchToMain switches to main");
828
-
829
- // Verify the auto-commit on the slice branch used smart staging
830
- const sliceLog = run("git log gsd/M001/S01 --oneline -1", repo);
831
- assertTrue(sliceLog.includes("pre-switch"), "auto-commit message includes pre-switch");
832
-
833
- // Check that the auto-commit on the slice branch excluded runtime files
834
- const showStat = run("git log gsd/M001/S01 -1 --format= --stat", repo);
835
- assertTrue(showStat.includes("src/work.ts"), "switchToMain auto-commit includes real files");
836
- assertTrue(!showStat.includes(".gsd/activity"), "switchToMain auto-commit excludes .gsd/activity/");
837
- assertTrue(!showStat.includes(".gsd/runtime"), "switchToMain auto-commit excludes .gsd/runtime/");
838
-
839
- rmSync(repo, { recursive: true, force: true });
840
- }
841
-
842
- // ─── switchToMain: idempotent when already on main ─────────────────────
843
-
844
- console.log("\n=== switchToMain: idempotent ===");
845
-
846
- {
847
- const repo = initBranchTestRepo();
848
- const svc = new GitServiceImpl(repo);
849
-
850
- assertEq(svc.getCurrentBranch(), "main", "already on main");
851
- svc.switchToMain(); // Should not throw
852
- assertEq(svc.getCurrentBranch(), "main", "still on main after idempotent switchToMain");
853
-
854
- // Verify no extra commits were created
855
- const logCount = run("git rev-list --count HEAD", repo);
856
- assertEq(logCount, "1", "no extra commits from idempotent switchToMain");
857
-
858
- rmSync(repo, { recursive: true, force: true });
859
- }
860
-
861
- // ─── mergeSliceToMain: full lifecycle with feat ─────────────────────────
862
-
863
- console.log("\n=== mergeSliceToMain: full lifecycle ===");
864
-
865
- {
866
- const repo = initBranchTestRepo();
867
- const svc = new GitServiceImpl(repo);
868
-
869
- // Create and switch to slice branch
870
- svc.ensureSliceBranch("M001", "S01");
871
- assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "on slice branch for merge test");
872
-
873
- // Do work on the slice branch
874
- createFile(repo, "src/feature.ts", "export const feature = true;");
875
- svc.commit({ message: "add feature module" });
876
-
877
- // Switch to main and merge
878
- svc.switchToMain();
879
- const result = svc.mergeSliceToMain("M001", "S01", "Implement user authentication");
880
-
881
- assertEq(result.mergedCommitMessage, "feat(M001/S01): Implement user authentication", "merge commit message uses feat type");
882
- assertEq(result.deletedBranch, true, "branch was deleted");
883
- assertEq(result.branch, "gsd/M001/S01", "result includes branch name");
884
-
885
- // Verify commit is on main
886
- const log = run("git log --oneline -1", repo);
887
- assertTrue(log.includes("feat(M001/S01): Implement user authentication"), "merge commit visible in git log");
888
-
889
- // Verify the file is on main
890
- const files = run("git ls-files", repo);
891
- assertTrue(files.includes("src/feature.ts"), "merged file exists on main");
892
-
893
- // Verify slice branch is deleted
894
- const branches = run("git branch", repo);
895
- assertTrue(!branches.includes("gsd/M001/S01"), "slice branch deleted after merge");
896
-
897
- rmSync(repo, { recursive: true, force: true });
898
- }
899
-
900
- // ─── mergeSliceToMain: fix type ───────────────────────────────────────
901
-
902
- console.log("\n=== mergeSliceToMain: fix type ===");
903
-
904
- {
905
- const repo = initBranchTestRepo();
906
- const svc = new GitServiceImpl(repo);
907
-
908
- svc.ensureSliceBranch("M001", "S02");
909
- createFile(repo, "src/bugfix.ts", "// fixed");
910
- svc.commit({ message: "fix the bug" });
911
-
912
- svc.switchToMain();
913
- const result = svc.mergeSliceToMain("M001", "S02", "Fix broken config");
914
-
915
- assertTrue(result.mergedCommitMessage.startsWith("fix("), "merge commit starts with fix(");
916
- assertEq(result.mergedCommitMessage, "fix(M001/S02): Fix broken config", "fix merge commit message correct");
917
-
918
- rmSync(repo, { recursive: true, force: true });
919
- }
920
-
921
- // ─── mergeSliceToMain: docs type ──────────────────────────────────────
922
-
923
- console.log("\n=== mergeSliceToMain: docs type ===");
924
-
925
- {
926
- const repo = initBranchTestRepo();
927
- const svc = new GitServiceImpl(repo);
928
-
929
- svc.ensureSliceBranch("M001", "S03");
930
- createFile(repo, "docs/guide.md", "# Guide");
931
- svc.commit({ message: "write docs" });
932
-
933
- svc.switchToMain();
934
- const result = svc.mergeSliceToMain("M001", "S03", "Docs update");
935
-
936
- assertTrue(result.mergedCommitMessage.startsWith("docs("), "merge commit starts with docs(");
937
- assertEq(result.mergedCommitMessage, "docs(M001/S03): Docs update", "docs merge commit message correct");
938
-
939
- rmSync(repo, { recursive: true, force: true });
940
- }
941
-
942
- // ─── mergeSliceToMain: refactor type ──────────────────────────────────
943
-
944
- console.log("\n=== mergeSliceToMain: refactor type ===");
945
-
946
- {
947
- const repo = initBranchTestRepo();
948
- const svc = new GitServiceImpl(repo);
949
-
950
- svc.ensureSliceBranch("M001", "S04");
951
- createFile(repo, "src/refactored.ts", "// cleaner");
952
- svc.commit({ message: "restructure modules" });
953
-
954
- svc.switchToMain();
955
- const result = svc.mergeSliceToMain("M001", "S04", "Refactor state management");
956
-
957
- assertTrue(result.mergedCommitMessage.startsWith("refactor("), "merge commit starts with refactor(");
958
- assertEq(result.mergedCommitMessage, "refactor(M001/S04): Refactor state management", "refactor merge commit message correct");
959
-
960
- rmSync(repo, { recursive: true, force: true });
961
- }
962
-
963
- // ─── mergeSliceToMain: error — not on main ────────────────────────────
964
-
965
- console.log("\n=== mergeSliceToMain: error cases ===");
966
-
967
- {
968
- const repo = initBranchTestRepo();
969
- const svc = new GitServiceImpl(repo);
970
-
971
- // Create a slice branch with a commit
972
- svc.ensureSliceBranch("M001", "S01");
973
- createFile(repo, "src/work.ts", "work");
974
- svc.commit({ message: "slice work" });
975
-
976
- // Try to merge while still on the slice branch
977
- let threw = false;
978
- try {
979
- svc.mergeSliceToMain("M001", "S01", "Some feature");
980
- } catch (e) {
981
- threw = true;
982
- const msg = (e as Error).message;
983
- assertTrue(msg.includes("must be called from the main branch"), "error mentions main branch requirement");
984
- assertTrue(msg.includes("gsd/M001/S01"), "error includes current branch name");
985
- }
986
- assertTrue(threw, "mergeSliceToMain throws when not on main");
987
-
988
- rmSync(repo, { recursive: true, force: true });
989
- }
990
-
991
- // ─── mergeSliceToMain: error — branch doesn't exist ───────────────────
992
-
993
- {
994
- const repo = initBranchTestRepo();
995
- const svc = new GitServiceImpl(repo);
996
-
997
- let threw = false;
998
- try {
999
- svc.mergeSliceToMain("M001", "S99", "Nonexistent");
1000
- } catch (e) {
1001
- threw = true;
1002
- const msg = (e as Error).message;
1003
- assertTrue(msg.includes("does not exist"), "error mentions branch does not exist");
1004
- assertTrue(msg.includes("gsd/M001/S99"), "error includes missing branch name");
1005
- }
1006
- assertTrue(threw, "mergeSliceToMain throws when branch doesn't exist");
1007
-
1008
- rmSync(repo, { recursive: true, force: true });
1009
- }
1010
-
1011
- // ─── mergeSliceToMain: error — no commits ahead ───────────────────────
1012
-
1013
- {
1014
- const repo = initBranchTestRepo();
1015
- const svc = new GitServiceImpl(repo);
1016
-
1017
- // Create slice branch but don't add any commits
1018
- svc.ensureSliceBranch("M001", "S01");
1019
- // Switch back to main without committing anything on the slice branch
1020
- svc.switchToMain();
1021
-
1022
- let threw = false;
1023
- try {
1024
- svc.mergeSliceToMain("M001", "S01", "Empty slice");
1025
- } catch (e) {
1026
- threw = true;
1027
- const msg = (e as Error).message;
1028
- assertTrue(msg.includes("no commits ahead"), "error mentions no commits ahead");
1029
- assertTrue(msg.includes("gsd/M001/S01"), "error includes branch name");
1030
- }
1031
- assertTrue(threw, "mergeSliceToMain throws when no commits ahead");
1032
-
1033
- rmSync(repo, { recursive: true, force: true });
1034
- }
1035
-
1036
- // ─── mergeSliceToMain: auto-resolve .gsd/ planning artifact conflicts ──
1037
-
1038
- console.log("\n=== mergeSliceToMain: auto-resolve .gsd/ planning conflicts ===");
1039
-
1040
- {
1041
- const repo = initBranchTestRepo();
1042
- const svc = new GitServiceImpl(repo);
1043
-
1044
- // Create a .gsd/ planning artifact on main (simulates reassess-roadmap)
1045
- createFile(repo, ".gsd/DECISIONS.md", "# Decisions\n\n- D001: Original decision\n");
1046
- run("git add -A", repo);
1047
- run('git commit -m "add decisions on main"', repo);
1048
-
1049
- // Create slice branch and modify the same .gsd/ file differently
1050
- svc.ensureSliceBranch("M001", "S01");
1051
- createFile(repo, ".gsd/DECISIONS.md", "# Decisions\n\n- D001: Original decision\n- D002: New decision from slice\n");
1052
- createFile(repo, "src/feature.ts", "export const x = 1;");
1053
- run("git add -A", repo);
1054
- run('git commit -m "slice work with .gsd/ changes"', repo);
1055
-
1056
- // Back on main, modify the same .gsd/ file to create a conflict
1057
- svc.switchToMain();
1058
- createFile(repo, ".gsd/DECISIONS.md", "# Decisions\n\n- D001: Updated decision on main\n");
1059
- run("git add -A", repo);
1060
- run('git commit -m "update decisions on main"', repo);
1061
-
1062
- // Merge should auto-resolve .gsd/ conflicts by taking theirs (slice branch)
1063
- const result = svc.mergeSliceToMain("M001", "S01", "Feature with .gsd/ conflicts");
1064
- assertEq(result.deletedBranch, true, ".gsd/ conflict auto-resolved: branch deleted");
1065
-
1066
- // Verify the merge succeeded and src file is present
1067
- assertTrue(existsSync(join(repo, "src/feature.ts")), ".gsd/ conflict auto-resolved: src file merged");
1068
-
1069
- rmSync(repo, { recursive: true, force: true });
1070
- }
1071
-
1072
585
  // ═══════════════════════════════════════════════════════════════════════
1073
- // S05: Enhanced features — merge guards, snapshots, auto-push, rich commits
586
+ // S05: Enhanced features — snapshots, pre-merge checks
1074
587
  // ═══════════════════════════════════════════════════════════════════════
1075
588
 
1076
589
  // ─── createSnapshot: prefs enabled ─────────────────────────────────────
@@ -1081,12 +594,12 @@ async function main(): Promise<void> {
1081
594
  const repo = initBranchTestRepo();
1082
595
  const svc = new GitServiceImpl(repo, { snapshots: true });
1083
596
 
1084
- // Create a slice branch with a commit
1085
- svc.ensureSliceBranch("M001", "S01");
597
+ // Create a branch with a commit
598
+ run("git checkout -b gsd/M001/S01", repo);
1086
599
  createFile(repo, "src/snap.ts", "snapshot me");
1087
600
  svc.commit({ message: "snapshot test commit" });
1088
601
 
1089
- // Create snapshot ref for this slice branch
602
+ // Create snapshot ref for this branch
1090
603
  svc.createSnapshot("gsd/M001/S01");
1091
604
 
1092
605
  // Verify ref exists under refs/gsd/snapshots/
@@ -1104,7 +617,7 @@ async function main(): Promise<void> {
1104
617
  const repo = initBranchTestRepo();
1105
618
  const svc = new GitServiceImpl(repo, { snapshots: false });
1106
619
 
1107
- svc.ensureSliceBranch("M001", "S01");
620
+ run("git checkout -b gsd/M001/S01", repo);
1108
621
  createFile(repo, "src/no-snap.ts", "no snapshot");
1109
622
  svc.commit({ message: "no snapshot commit" });
1110
623
 
@@ -1201,222 +714,6 @@ async function main(): Promise<void> {
1201
714
  rmSync(repo, { recursive: true, force: true });
1202
715
  }
1203
716
 
1204
- // ─── Rich commit message ──────────────────────────────────────────────
1205
-
1206
- console.log("\n=== mergeSliceToMain: rich commit message ===");
1207
-
1208
- {
1209
- const repo = initBranchTestRepo();
1210
- const svc = new GitServiceImpl(repo, { pre_merge_check: false });
1211
-
1212
- svc.ensureSliceBranch("M001", "S01");
1213
-
1214
- // Make 3 distinct commits on the slice branch
1215
- createFile(repo, "src/auth.ts", "export const auth = true;");
1216
- svc.commit({ message: "add auth module" });
1217
-
1218
- createFile(repo, "src/login.ts", "export const login = true;");
1219
- svc.commit({ message: "add login page" });
1220
-
1221
- createFile(repo, "src/session.ts", "export const session = true;");
1222
- svc.commit({ message: "add session handling" });
1223
-
1224
- svc.switchToMain();
1225
- const result = svc.mergeSliceToMain("M001", "S01", "Implement user authentication");
1226
-
1227
- // Inspect the full commit body on main
1228
- const commitBody = run("git log -1 --format=%B", repo);
1229
-
1230
- // Rich commit should have the subject line
1231
- assertTrue(commitBody.includes("feat(M001/S01): Implement user authentication"),
1232
- "rich commit has conventional subject line");
1233
-
1234
- // Rich commit body should include task list with commit subjects
1235
- assertTrue(commitBody.includes("add auth module"),
1236
- "rich commit body includes first commit subject");
1237
- assertTrue(commitBody.includes("add login page"),
1238
- "rich commit body includes second commit subject");
1239
- assertTrue(commitBody.includes("add session handling"),
1240
- "rich commit body includes third commit subject");
1241
-
1242
- // Rich commit body should include Branch: line for forensics
1243
- assertTrue(commitBody.includes("Branch:"),
1244
- "rich commit body includes Branch: line");
1245
- assertTrue(commitBody.includes("gsd/M001/S01"),
1246
- "rich commit body Branch: line includes slice branch name");
1247
-
1248
- rmSync(repo, { recursive: true, force: true });
1249
- }
1250
-
1251
- // ─── Auto-push: enabled ───────────────────────────────────────────────
1252
-
1253
- console.log("\n=== Auto-push: enabled ===");
1254
-
1255
- {
1256
- // Create a bare remote repo
1257
- const bareDir = mkdtempSync(join(tmpdir(), "gsd-git-bare-"));
1258
- run("git init --bare -b main", bareDir);
1259
-
1260
- // Create local repo and add the bare as remote
1261
- const repo = initBranchTestRepo();
1262
- run(`git remote add origin ${bareDir}`, repo);
1263
- run("git push -u origin main", repo);
1264
-
1265
- const svc = new GitServiceImpl(repo, { auto_push: true, pre_merge_check: false });
1266
-
1267
- svc.ensureSliceBranch("M001", "S01");
1268
- createFile(repo, "src/pushed.ts", "export const pushed = true;");
1269
- svc.commit({ message: "work to push" });
1270
-
1271
- svc.switchToMain();
1272
- svc.mergeSliceToMain("M001", "S01", "Add pushed feature");
1273
-
1274
- // Verify the remote has the merge commit
1275
- const remoteLog = run(`git --git-dir=${bareDir} log --oneline -1`, bareDir);
1276
- assertTrue(remoteLog.includes("Add pushed feature"),
1277
- "auto-push: remote has the merge commit when auto_push is true");
1278
-
1279
- rmSync(repo, { recursive: true, force: true });
1280
- rmSync(bareDir, { recursive: true, force: true });
1281
- }
1282
-
1283
- // ─── Auto-push: disabled ──────────────────────────────────────────────
1284
-
1285
- console.log("\n=== Auto-push: disabled ===");
1286
-
1287
- {
1288
- const bareDir = mkdtempSync(join(tmpdir(), "gsd-git-bare-"));
1289
- run("git init --bare -b main", bareDir);
1290
-
1291
- const repo = initBranchTestRepo();
1292
- run(`git remote add origin ${bareDir}`, repo);
1293
- run("git push -u origin main", repo);
1294
-
1295
- // auto_push explicitly false (or omitted — same behavior)
1296
- const svc = new GitServiceImpl(repo, { auto_push: false, pre_merge_check: false });
1297
-
1298
- svc.ensureSliceBranch("M001", "S01");
1299
- createFile(repo, "src/not-pushed.ts", "export const notPushed = true;");
1300
- svc.commit({ message: "work not pushed" });
1301
-
1302
- svc.switchToMain();
1303
- svc.mergeSliceToMain("M001", "S01", "Add unpushed feature");
1304
-
1305
- // Remote should NOT have the new merge commit — still at the initial push
1306
- const remoteLog = run(`git --git-dir=${bareDir} log --oneline`, bareDir);
1307
- assertTrue(!remoteLog.includes("Add unpushed feature"),
1308
- "auto-push: remote does NOT have merge commit when auto_push is false");
1309
-
1310
- rmSync(repo, { recursive: true, force: true });
1311
- rmSync(bareDir, { recursive: true, force: true });
1312
- }
1313
-
1314
- // ─── Remote fetch before branching: with remote ────────────────────────
1315
-
1316
- console.log("\n=== Remote fetch: with remote ===");
1317
-
1318
- {
1319
- const bareDir = mkdtempSync(join(tmpdir(), "gsd-git-bare-"));
1320
- run("git init --bare -b main", bareDir);
1321
-
1322
- const repo = initBranchTestRepo();
1323
- run(`git remote add origin ${bareDir}`, repo);
1324
- run("git push -u origin main", repo);
1325
-
1326
- // Add a commit to the remote via a temporary clone
1327
- const cloneDir = mkdtempSync(join(tmpdir(), "gsd-git-clone-"));
1328
- run(`git clone ${bareDir} ${cloneDir}`, cloneDir);
1329
- run('git config user.name "Remote Dev"', cloneDir);
1330
- run('git config user.email "remote@example.com"', cloneDir);
1331
- createFile(cloneDir, "remote-file.txt", "from remote");
1332
- run("git add -A", cloneDir);
1333
- run('git commit -m "remote commit"', cloneDir);
1334
- run("git push origin main", cloneDir);
1335
-
1336
- // ensureSliceBranch should fetch before creating the branch — no crash
1337
- const svc = new GitServiceImpl(repo);
1338
- let noError = true;
1339
- try {
1340
- svc.ensureSliceBranch("M001", "S01");
1341
- } catch {
1342
- noError = false;
1343
- }
1344
- assertTrue(noError, "ensureSliceBranch succeeds when remote has new commits (fetch runs)");
1345
-
1346
- rmSync(repo, { recursive: true, force: true });
1347
- rmSync(bareDir, { recursive: true, force: true });
1348
- rmSync(cloneDir, { recursive: true, force: true });
1349
- }
1350
-
1351
- // ─── Remote fetch before branching: without remote ─────────────────────
1352
-
1353
- console.log("\n=== Remote fetch: without remote ===");
1354
-
1355
- {
1356
- const repo = initBranchTestRepo();
1357
- // No remote configured — ensureSliceBranch should not crash
1358
- const svc = new GitServiceImpl(repo);
1359
-
1360
- let noError = true;
1361
- try {
1362
- svc.ensureSliceBranch("M001", "S01");
1363
- } catch {
1364
- noError = false;
1365
- }
1366
- assertTrue(noError, "ensureSliceBranch succeeds when no remote is configured");
1367
- assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "branch created even without remote");
1368
-
1369
- rmSync(repo, { recursive: true, force: true });
1370
- }
1371
-
1372
- // ─── Facade prefs: mergeSliceToMain creates snapshot when prefs set ────
1373
-
1374
- console.log("\n=== Facade prefs: snapshot via merge with prefs ===");
1375
-
1376
- {
1377
- const repo = initBranchTestRepo();
1378
- // Simulate facade behavior: GitServiceImpl with snapshots:true should
1379
- // create a snapshot ref during mergeSliceToMain
1380
- const svc = new GitServiceImpl(repo, { snapshots: true, pre_merge_check: false });
1381
-
1382
- svc.ensureSliceBranch("M001", "S01");
1383
- createFile(repo, "src/facade-test.ts", "facade");
1384
- svc.commit({ message: "facade test commit" });
1385
-
1386
- svc.switchToMain();
1387
- svc.mergeSliceToMain("M001", "S01", "Facade snapshot test");
1388
-
1389
- // After merge, a snapshot ref should exist (created before merge)
1390
- const refs = run("git for-each-ref refs/gsd/snapshots/", repo);
1391
- assertTrue(refs.includes("refs/gsd/snapshots/"), "mergeSliceToMain creates snapshot when prefs.snapshots is true");
1392
- assertTrue(refs.includes("gsd/M001/S01"), "snapshot ref references the slice branch name");
1393
-
1394
- rmSync(repo, { recursive: true, force: true });
1395
- }
1396
-
1397
- // ─── Facade prefs: no snapshot when prefs omit snapshots ───────────────
1398
-
1399
- console.log("\n=== Facade prefs: no snapshot when prefs omit snapshots ===");
1400
-
1401
- {
1402
- const repo = initBranchTestRepo();
1403
- // Default prefs — snapshots not enabled
1404
- const svc = new GitServiceImpl(repo, { pre_merge_check: false });
1405
-
1406
- svc.ensureSliceBranch("M001", "S01");
1407
- createFile(repo, "src/no-facade-snap.ts", "no facade snap");
1408
- svc.commit({ message: "no facade snapshot" });
1409
-
1410
- svc.switchToMain();
1411
- svc.mergeSliceToMain("M001", "S01", "No snapshot test");
1412
-
1413
- // No snapshot ref should exist
1414
- const refs = run("git for-each-ref refs/gsd/snapshots/", repo);
1415
- assertEq(refs, "", "no snapshot ref when snapshots pref is not set");
1416
-
1417
- rmSync(repo, { recursive: true, force: true });
1418
- }
1419
-
1420
717
  // ─── VALID_BRANCH_NAME regex ──────────────────────────────────────────
1421
718
 
1422
719
  console.log("\n=== VALID_BRANCH_NAME regex ===");
@@ -1628,62 +925,6 @@ async function main(): Promise<void> {
1628
925
  rmSync(repo, { recursive: true, force: true });
1629
926
  }
1630
927
 
1631
- // ─── End-to-end: feature branch workflow ──────────────────────────────
1632
-
1633
- console.log("\n=== End-to-end: feature branch workflow ===");
1634
-
1635
- {
1636
- const repo = initBranchTestRepo();
1637
-
1638
- // Simulate: user creates feature branch and starts GSD
1639
- run("git checkout -b f-123-new-thing", repo);
1640
- createFile(repo, "setup.txt", "initial setup");
1641
- run("git add -A", repo);
1642
- run('git commit -m "initial feature setup"', repo);
1643
-
1644
- // Record integration branch (this is what auto.ts does at startup)
1645
- writeIntegrationBranch(repo, "M001", "f-123-new-thing");
1646
-
1647
- // Create GitServiceImpl with milestone set
1648
- const svc = new GitServiceImpl(repo);
1649
- svc.setMilestoneId("M001");
1650
-
1651
- // Verify getMainBranch returns the feature branch, not "main"
1652
- assertEq(svc.getMainBranch(), "f-123-new-thing", "e2e: getMainBranch returns feature branch");
1653
-
1654
- // Create slice branch — should branch from f-123-new-thing (current)
1655
- svc.ensureSliceBranch("M001", "S01");
1656
- assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "e2e: slice branch created");
1657
-
1658
- // The slice branch should have the feature branch's commit
1659
- const log = run("git log --oneline", repo);
1660
- assertTrue(log.includes("initial feature setup"), "e2e: slice branch inherits feature branch content");
1661
-
1662
- // Do work on the slice branch
1663
- createFile(repo, "src/feature.ts", "export const feature = true;");
1664
- svc.commit({ message: "feat: add feature module" });
1665
-
1666
- // switchToMain should go to feature branch
1667
- svc.switchToMain();
1668
- assertEq(svc.getCurrentBranch(), "f-123-new-thing", "e2e: switchToMain goes to feature branch, not main");
1669
-
1670
- // mergeSliceToMain should merge into feature branch
1671
- const result = svc.mergeSliceToMain("M001", "S01", "Add feature module");
1672
- assertEq(result.mergedCommitMessage, "feat(M001/S01): Add feature module", "e2e: merge commit message correct");
1673
- assertEq(svc.getCurrentBranch(), "f-123-new-thing", "e2e: after merge, still on feature branch");
1674
-
1675
- // The feature branch should have the merged work
1676
- const files = run("git ls-files", repo);
1677
- assertTrue(files.includes("src/feature.ts"), "e2e: merged file exists on feature branch");
1678
-
1679
- // Main should NOT have the merged work
1680
- run("git checkout main", repo);
1681
- const mainFiles = run("git ls-files", repo);
1682
- assertTrue(!mainFiles.includes("src/feature.ts"), "e2e: main does NOT have merged work — it stays on the feature branch");
1683
-
1684
- rmSync(repo, { recursive: true, force: true });
1685
- }
1686
-
1687
928
  // ─── Per-milestone isolation: different milestones, different targets ──
1688
929
 
1689
930
  console.log("\n=== Integration branch: per-milestone isolation ===");