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
@@ -4,8 +4,6 @@
4
4
  * Tests the full lifecycle of GSD operations inside a worktree:
5
5
  * - Branch namespacing (gsd/<wt>/<M>/<S> instead of gsd/<M>/<S>)
6
6
  * - getMainBranch returns worktree/<name> inside a worktree
7
- * - switchToMain goes to worktree/<name>, not main
8
- * - mergeSliceToMain merges into worktree/<name>
9
7
  * - Parallel worktrees don't conflict on branch names
10
8
  * - State derivation works correctly inside worktrees
11
9
  */
@@ -19,21 +17,15 @@ import {
19
17
  createWorktree,
20
18
  listWorktrees,
21
19
  removeWorktree,
22
- worktreePath,
23
- worktreeBranchName,
24
20
  } from "../worktree-manager.ts";
25
21
 
26
22
  import {
27
23
  detectWorktreeName,
28
- ensureSliceBranch,
29
- getActiveSliceBranch,
30
24
  getCurrentBranch,
31
25
  getMainBranch,
32
26
  getSliceBranchName,
33
- isOnSliceBranch,
34
- mergeSliceToMain,
35
- switchToMain,
36
27
  autoCommitCurrentBranch,
28
+ SLICE_BRANCH_RE,
37
29
  } from "../worktree.ts";
38
30
 
39
31
  import { deriveState } from "../state.ts";
@@ -104,21 +96,20 @@ async function main(): Promise<void> {
104
96
  console.log("\n=== Worktree initial branch ===");
105
97
  assertEq(getCurrentBranch(wt.path), "worktree/alpha", "worktree starts on its own branch");
106
98
 
107
- // ── ensureSliceBranch inside worktree ──────────────────────────────────────
108
-
109
- console.log("\n=== ensureSliceBranch in worktree ===");
110
- const created = ensureSliceBranch(wt.path, "M001", "S01");
111
- assertTrue(created, "slice branch created");
112
- assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S01", "worktree-namespaced slice branch");
113
- assertTrue(isOnSliceBranch(wt.path), "isOnSliceBranch returns true");
114
- assertEq(getActiveSliceBranch(wt.path), "gsd/alpha/M001/S01", "getActiveSliceBranch returns namespaced branch");
115
-
116
99
  // ── Verify branch name helper ──────────────────────────────────────────────
117
100
 
118
101
  console.log("\n=== getSliceBranchName with worktree ===");
119
102
  assertEq(getSliceBranchName("M001", "S01", "alpha"), "gsd/alpha/M001/S01", "explicit worktree param");
120
103
  assertEq(getSliceBranchName("M001", "S01"), "gsd/M001/S01", "no worktree param = plain branch");
121
104
 
105
+ // ── Slice branch creation and detection inside worktree ────────────────────
106
+
107
+ console.log("\n=== Slice branch in worktree ===");
108
+ const sliceBranch = getSliceBranchName("M001", "S01", "alpha");
109
+ run(`git checkout -b ${sliceBranch}`, wt.path);
110
+ assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S01", "worktree-namespaced slice branch");
111
+ assertTrue(SLICE_BRANCH_RE.test(getCurrentBranch(wt.path)), "slice branch regex matches namespaced branch");
112
+
122
113
  // ── Do work on slice branch, then merge to worktree branch ─────────────────
123
114
 
124
115
  console.log("\n=== Work and merge slice in worktree ===");
@@ -126,14 +117,12 @@ async function main(): Promise<void> {
126
117
  run("git add .", wt.path);
127
118
  run('git commit -m "feat: add feature"', wt.path);
128
119
 
129
- // switchToMain should go to worktree/alpha, NOT main
130
- switchToMain(wt.path);
131
- assertEq(getCurrentBranch(wt.path), "worktree/alpha", "switchToMain goes to worktree branch, not main");
120
+ // Checkout worktree base branch and merge slice branch
121
+ run("git checkout worktree/alpha", wt.path);
122
+ assertEq(getCurrentBranch(wt.path), "worktree/alpha", "back on worktree branch");
132
123
 
133
- // mergeSliceToMain should merge into worktree/alpha
134
- const merge = mergeSliceToMain(wt.path, "M001", "S01", "First");
135
- assertEq(merge.branch, "gsd/alpha/M001/S01", "merged the namespaced branch");
136
- assertTrue(merge.deletedBranch, "slice branch deleted after merge");
124
+ run(`git merge --no-ff ${sliceBranch} -m "feat(M001/S01): First"`, wt.path);
125
+ run(`git branch -d ${sliceBranch}`, wt.path);
137
126
  assertEq(getCurrentBranch(wt.path), "worktree/alpha", "still on worktree branch after merge");
138
127
  assertTrue(readFileSync(join(wt.path, "feature.txt"), "utf-8").includes("new feature"), "merge brought feature to worktree branch");
139
128
 
@@ -144,36 +133,19 @@ async function main(): Promise<void> {
144
133
  // ── Second slice in same worktree ──────────────────────────────────────────
145
134
 
146
135
  console.log("\n=== Second slice in worktree ===");
147
- const created2 = ensureSliceBranch(wt.path, "M001", "S02");
148
- assertTrue(created2, "S02 branch created");
136
+ const sliceBranch2 = getSliceBranchName("M001", "S02", "alpha");
137
+ run(`git checkout -b ${sliceBranch2}`, wt.path);
149
138
  assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S02", "on S02 namespaced branch");
150
139
 
151
140
  writeFileSync(join(wt.path, "feature2.txt"), "second feature\n", "utf-8");
152
141
  run("git add .", wt.path);
153
142
  run('git commit -m "feat: add feature 2"', wt.path);
154
143
 
155
- switchToMain(wt.path);
156
- const merge2 = mergeSliceToMain(wt.path, "M001", "S02", "Second");
157
- assertEq(merge2.branch, "gsd/alpha/M001/S02", "S02 merge correct");
144
+ run("git checkout worktree/alpha", wt.path);
145
+ run(`git merge --no-ff ${sliceBranch2} -m "feat(M001/S02): Second"`, wt.path);
146
+ run(`git branch -d ${sliceBranch2}`, wt.path);
158
147
  assertEq(getCurrentBranch(wt.path), "worktree/alpha", "back on worktree branch");
159
148
 
160
- // ── Main tree can still do its own slice work independently ────────────────
161
-
162
- console.log("\n=== Main tree independent slice work ===");
163
- assertEq(getCurrentBranch(base), "main", "main tree still on main");
164
- const mainCreated = ensureSliceBranch(base, "M001", "S01");
165
- assertTrue(mainCreated, "main tree can create S01 branch (no conflict with worktree)");
166
- assertEq(getCurrentBranch(base), "gsd/M001/S01", "main tree on plain branch name");
167
-
168
- writeFileSync(join(base, "main-feature.txt"), "main work\n", "utf-8");
169
- run("git add .", base);
170
- run('git commit -m "feat: main work"', base);
171
-
172
- switchToMain(base);
173
- assertEq(getCurrentBranch(base), "main", "main tree switchToMain goes to main");
174
- const mainMerge = mergeSliceToMain(base, "M001", "S01", "First");
175
- assertEq(mainMerge.branch, "gsd/M001/S01", "main tree merge uses plain branch");
176
-
177
149
  // ── Parallel worktrees don't conflict ──────────────────────────────────────
178
150
 
179
151
  console.log("\n=== Parallel worktrees ===");
@@ -181,13 +153,13 @@ async function main(): Promise<void> {
181
153
  assertEq(getMainBranch(wt2.path), "worktree/beta", "second worktree has its own base branch");
182
154
 
183
155
  // Both worktrees can create S01 branches without conflict
184
- const betaCreated = ensureSliceBranch(wt2.path, "M001", "S01");
185
- assertTrue(betaCreated, "beta worktree can create S01");
156
+ const betaBranch = getSliceBranchName("M001", "S01", "beta");
157
+ run(`git checkout -b ${betaBranch}`, wt2.path);
186
158
  assertEq(getCurrentBranch(wt2.path), "gsd/beta/M001/S01", "beta has its own namespaced branch");
187
159
 
188
160
  // Alpha worktree can re-create S01 too (it was already merged+deleted earlier)
189
- const alphaReCreated = ensureSliceBranch(wt.path, "M001", "S01");
190
- assertTrue(alphaReCreated, "alpha worktree can re-create S01");
161
+ const alphaReBranch = getSliceBranchName("M001", "S01", "alpha");
162
+ run(`git checkout -b ${alphaReBranch}`, wt.path);
191
163
  assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S01", "alpha re-created S01");
192
164
 
193
165
  // Both exist simultaneously
@@ -199,7 +171,7 @@ async function main(): Promise<void> {
199
171
 
200
172
  console.log("\n=== State derivation in worktree ===");
201
173
  // Switch alpha back to its base so deriveState sees milestone files
202
- switchToMain(wt.path);
174
+ run("git checkout worktree/alpha", wt.path);
203
175
  const state = await deriveState(wt.path);
204
176
  assertTrue(state.activeMilestone !== null, "worktree has active milestone");
205
177
  assertEq(state.activeMilestone?.id, "M001", "correct milestone");
@@ -207,7 +179,8 @@ async function main(): Promise<void> {
207
179
  // ── autoCommitCurrentBranch in worktree ────────────────────────────────────
208
180
 
209
181
  console.log("\n=== autoCommitCurrentBranch in worktree ===");
210
- ensureSliceBranch(wt2.path, "M001", "S01"); // re-checkout if needed
182
+ // Re-checkout the beta slice branch
183
+ run(`git checkout ${betaBranch}`, wt2.path);
211
184
  writeFileSync(join(wt2.path, "dirty.txt"), "uncommitted\n", "utf-8");
212
185
  const commitMsg = autoCommitCurrentBranch(wt2.path, "execute-task", "M001/S01/T01");
213
186
  assertTrue(commitMsg !== null, "auto-commit works in worktree");
@@ -217,8 +190,8 @@ async function main(): Promise<void> {
217
190
 
218
191
  console.log("\n=== Cleanup ===");
219
192
  // Switch worktrees back to their base branches before removal
220
- switchToMain(wt.path);
221
- switchToMain(wt2.path);
193
+ run("git checkout worktree/alpha", wt.path);
194
+ run("git checkout worktree/beta", wt2.path);
222
195
  removeWorktree(base, "alpha", { deleteBranch: true });
223
196
  removeWorktree(base, "beta", { deleteBranch: true });
224
197
  assertEq(listWorktrees(base).length, 0, "all worktrees removed");
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { execSync } from "node:child_process";
@@ -7,21 +7,14 @@ import {
7
7
  autoCommitCurrentBranch,
8
8
  captureIntegrationBranch,
9
9
  detectWorktreeName,
10
- ensureSliceBranch,
11
- getActiveSliceBranch,
12
10
  getCurrentBranch,
13
11
  getMainBranch,
14
12
  getSliceBranchName,
15
- isOnSliceBranch,
16
- mergeSliceToMain,
17
13
  parseSliceBranch,
18
14
  setActiveMilestoneId,
19
15
  SLICE_BRANCH_RE,
20
- switchToMain,
21
16
  } from "../worktree.ts";
22
17
  import { readIntegrationBranch } from "../git-service.ts";
23
- import { deriveState } from "../state.ts";
24
- import { indexWorkspace } from "../workspace-index.ts";
25
18
  import { createTestContext } from './test-helpers.ts';
26
19
 
27
20
  const { assertEq, assertTrue, report } = createTestContext();
@@ -41,27 +34,6 @@ run("git add .", base);
41
34
  run('git commit -m "chore: init"', base);
42
35
 
43
36
  async function main(): Promise<void> {
44
- console.log("\n=== ensureSliceBranch ===");
45
- const created = ensureSliceBranch(base, "M001", "S01");
46
- assertTrue(created, "branch created on first ensure");
47
- assertEq(getCurrentBranch(base), "gsd/M001/S01", "switched to slice branch");
48
-
49
- console.log("\n=== idempotent ensure ===");
50
- const secondCreate = ensureSliceBranch(base, "M001", "S01");
51
- assertEq(secondCreate, false, "branch not recreated on second ensure");
52
- assertEq(getCurrentBranch(base), "gsd/M001/S01", "still on slice branch");
53
-
54
- console.log("\n=== getActiveSliceBranch ===");
55
- assertEq(getActiveSliceBranch(base), "gsd/M001/S01", "getActiveSliceBranch returns current slice branch");
56
-
57
- console.log("\n=== state surfaces active branch ===");
58
- const state = await deriveState(base);
59
- assertEq(state.activeBranch, "gsd/M001/S01", "state exposes active branch");
60
-
61
- console.log("\n=== workspace index surfaces branch ===");
62
- const index = await indexWorkspace(base);
63
- const slice = index.milestones[0]?.slices[0];
64
- assertEq(slice?.branch, "gsd/M001/S01", "workspace index exposes branch");
65
37
 
66
38
  console.log("\n=== autoCommitCurrentBranch ===");
67
39
  // Clean — should return null
@@ -75,56 +47,6 @@ async function main(): Promise<void> {
75
47
  assertTrue(dirtyResult!.includes("M001/S01/T01"), "commit message includes unit id");
76
48
  assertEq(run("git status --short", base), "", "repo is clean after auto-commit");
77
49
 
78
- console.log("\n=== switchToMain ===");
79
- switchToMain(base);
80
- assertEq(getCurrentBranch(base), "main", "switched back to main");
81
- assertEq(getActiveSliceBranch(base), null, "getActiveSliceBranch returns null on main");
82
-
83
- console.log("\n=== mergeSliceToMain ===");
84
- // Switch back to slice, make a change, switch to main, merge
85
- ensureSliceBranch(base, "M001", "S01");
86
- writeFileSync(join(base, "README.md"), "hello from slice\n", "utf-8");
87
- run("git add README.md", base);
88
- run('git commit -m "feat: slice change"', base);
89
- switchToMain(base);
90
-
91
- const merge = mergeSliceToMain(base, "M001", "S01", "Slice One");
92
- assertEq(merge.branch, "gsd/M001/S01", "merge reports branch");
93
- assertEq(getCurrentBranch(base), "main", "still on main after merge");
94
- assertTrue(readFileSync(join(base, "README.md"), "utf-8").includes("slice"), "main got squashed content");
95
- assertTrue(merge.deletedBranch, "branch was deleted");
96
-
97
- // Verify branch is actually gone
98
- const branches = run("git branch", base);
99
- assertTrue(!branches.includes("gsd/M001/S01"), "slice branch no longer exists");
100
-
101
- console.log("\n=== switchToMain auto-commits dirty files ===");
102
- // Set up S02
103
- mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S02", "tasks"), { recursive: true });
104
- writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), [
105
- "# M001: Demo", "", "## Slices",
106
- "- [x] **S01: Slice One** `risk:low` `depends:[]`", " > Done",
107
- "- [ ] **S02: Slice Two** `risk:low` `depends:[]`", " > Demo 2",
108
- ].join("\n") + "\n", "utf-8");
109
- run("git add .", base);
110
- run('git commit -m "chore: add S02"', base);
111
-
112
- ensureSliceBranch(base, "M001", "S02");
113
- writeFileSync(join(base, "feature.txt"), "new feature\n", "utf-8");
114
- // Don't commit — switchToMain should auto-commit
115
- switchToMain(base);
116
- assertEq(getCurrentBranch(base), "main", "switched to main despite dirty files");
117
-
118
- // Verify the commit happened on the slice branch
119
- ensureSliceBranch(base, "M001", "S02");
120
- assertTrue(readFileSync(join(base, "feature.txt"), "utf-8").includes("new feature"), "dirty file was committed on slice branch");
121
- switchToMain(base);
122
-
123
- // Now merge S02
124
- const mergeS02 = mergeSliceToMain(base, "M001", "S02", "Slice Two");
125
- assertTrue(readFileSync(join(base, "feature.txt"), "utf-8").includes("new feature"), "main got feature from auto-committed branch");
126
- assertEq(mergeS02.deletedBranch, true, "S02 branch deleted");
127
-
128
50
  console.log("\n=== getSliceBranchName ===");
129
51
  assertEq(getSliceBranchName("M001", "S01"), "gsd/M001/S01", "branch name format correct");
130
52
  assertEq(getSliceBranchName("M001", "S01", null), "gsd/M001/S01", "null worktree = plain branch");
@@ -161,90 +83,8 @@ async function main(): Promise<void> {
161
83
  assertEq(detectWorktreeName("/projects/myapp/.gsd/worktrees/feature-auth"), "feature-auth", "detects worktree name");
162
84
  assertEq(detectWorktreeName("/projects/myapp/.gsd/worktrees/my-wt/subdir"), "my-wt", "detects worktree with subdir");
163
85
 
164
- // ── Regression: slice branch from non-main working branch ───────────
165
- // Reproduces the bug where planning artifacts committed to a working
166
- // branch (e.g. "developer") are lost when the slice branch is created
167
- // from "main" which doesn't have them.
168
- console.log("\n=== ensureSliceBranch from non-main working branch ===");
169
- const base2 = mkdtempSync(join(tmpdir(), "gsd-branch-base-test-"));
170
- run("git init -b main", base2);
171
- run('git config user.name "Pi Test"', base2);
172
- run('git config user.email "pi@example.com"', base2);
173
- writeFileSync(join(base2, "README.md"), "hello\n", "utf-8");
174
- run("git add .", base2);
175
- run('git commit -m "chore: init"', base2);
176
-
177
- // Create a "developer" branch with planning artifacts (like the real scenario)
178
- run("git checkout -b developer", base2);
179
- mkdirSync(join(base2, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
180
- writeFileSync(join(base2, ".gsd", "milestones", "M001", "M001-CONTEXT.md"), "# M001 Context\nGoal: fix eslint\n", "utf-8");
181
- writeFileSync(join(base2, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), [
182
- "# M001: ESLint Cleanup", "", "## Slices",
183
- "- [ ] **S01: Config Fix** `risk:low` `depends:[]`", " > Fix config",
184
- ].join("\n") + "\n", "utf-8");
185
- run("git add .", base2);
186
- run('git commit -m "docs(M001): context and roadmap"', base2);
187
-
188
- // Verify main does NOT have the artifacts
189
- const mainRoadmap = run("git show main:.gsd/milestones/M001/M001-ROADMAP.md 2>&1 || echo MISSING", base2);
190
- assertTrue(mainRoadmap.includes("MISSING") || mainRoadmap.includes("does not exist"), "main branch lacks roadmap");
191
-
192
- // Now create slice branch from developer — should inherit artifacts
193
- assertEq(getCurrentBranch(base2), "developer", "on developer branch before ensure");
194
- const created3 = ensureSliceBranch(base2, "M001", "S01");
195
- assertTrue(created3, "slice branch created from developer");
196
- assertEq(getCurrentBranch(base2), "gsd/M001/S01", "switched to slice branch");
197
-
198
- // The critical assertion: planning artifacts must exist on the slice branch
199
- assertTrue(existsSync(join(base2, ".gsd", "milestones", "M001", "M001-ROADMAP.md")), "roadmap exists on slice branch");
200
- assertTrue(existsSync(join(base2, ".gsd", "milestones", "M001", "M001-CONTEXT.md")), "context exists on slice branch");
201
-
202
- // Verify deriveState sees the correct phase (not pre-planning)
203
- const state2 = await deriveState(base2);
204
- assertEq(state2.phase, "planning", "deriveState sees planning phase on slice branch");
205
- assertTrue(state2.activeSlice !== null, "active slice found");
206
- assertEq(state2.activeSlice!.id, "S01", "active slice is S01");
207
-
208
- rmSync(base2, { recursive: true, force: true });
209
-
210
- // ── Slice branch from another slice branch falls back to main ───────
211
- console.log("\n=== ensureSliceBranch from slice branch falls back to main ===");
212
- const base3 = mkdtempSync(join(tmpdir(), "gsd-branch-chain-test-"));
213
- run("git init -b main", base3);
214
- run('git config user.name "Pi Test"', base3);
215
- run('git config user.email "pi@example.com"', base3);
216
- mkdirSync(join(base3, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
217
- mkdirSync(join(base3, ".gsd", "milestones", "M001", "slices", "S02", "tasks"), { recursive: true });
218
- writeFileSync(join(base3, "README.md"), "hello\n", "utf-8");
219
- writeFileSync(join(base3, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), [
220
- "# M001: Demo", "", "## Slices",
221
- "- [ ] **S01: First** `risk:low` `depends:[]`", " > first",
222
- "- [ ] **S02: Second** `risk:low` `depends:[]`", " > second",
223
- ].join("\n") + "\n", "utf-8");
224
- run("git add .", base3);
225
- run('git commit -m "chore: init"', base3);
226
-
227
- ensureSliceBranch(base3, "M001", "S01");
228
- assertEq(getCurrentBranch(base3), "gsd/M001/S01", "on S01 slice branch");
229
-
230
- // Creating S02 while on S01 should NOT chain from S01 — should use main
231
- const created4 = ensureSliceBranch(base3, "M001", "S02");
232
- assertTrue(created4, "S02 branch created");
233
- assertEq(getCurrentBranch(base3), "gsd/M001/S02", "switched to S02");
234
-
235
- // S02 should be based on main, not on gsd/M001/S01
236
- const s02Base = run("git merge-base main gsd/M001/S02", base3);
237
- const mainHead = run("git rev-parse main", base3);
238
- assertEq(s02Base, mainHead, "S02 is based on main, not on S01 slice branch");
239
-
240
- rmSync(base3, { recursive: true, force: true });
241
-
242
86
  // ═══════════════════════════════════════════════════════════════════════
243
87
  // Integration branch — facade-level tests
244
- //
245
- // These exercise the same codepath auto.ts uses:
246
- // captureIntegrationBranch() → setActiveMilestoneId() → getMainBranch()
247
- // → switchToMain() → mergeSliceToMain()
248
88
  // ═══════════════════════════════════════════════════════════════════════
249
89
 
250
90
  // ── captureIntegrationBranch on a feature branch ──────────────────────
@@ -273,43 +113,6 @@ async function main(): Promise<void> {
273
113
  rmSync(repo, { recursive: true, force: true });
274
114
  }
275
115
 
276
- // ── captureIntegrationBranch is idempotent on same lineage ──────────
277
-
278
- console.log("\n=== captureIntegrationBranch: idempotent ===");
279
-
280
- {
281
- const repo = mkdtempSync(join(tmpdir(), "gsd-integ-idem-"));
282
- run("git init -b main", repo);
283
- run("git config user.name 'Pi Test'", repo);
284
- run("git config user.email 'pi@example.com'", repo);
285
- writeFileSync(join(repo, "README.md"), "init\n");
286
- run("git add -A && git commit -m init", repo);
287
- run("git checkout -b f-first", repo);
288
-
289
- captureIntegrationBranch(repo, "M001");
290
- setActiveMilestoneId(repo, "M001");
291
- assertEq(readIntegrationBranch(repo, "M001"), "f-first",
292
- "first capture records f-first");
293
-
294
- // Capture again on the same branch (simulates restart/resume) — should NOT overwrite
295
- captureIntegrationBranch(repo, "M001");
296
- assertEq(readIntegrationBranch(repo, "M001"), "f-first",
297
- "second capture on same branch does not overwrite");
298
-
299
- // After creating a slice branch (which inherits the metadata commit),
300
- // capture should still be idempotent
301
- ensureSliceBranch(repo, "M001", "S01");
302
- // Now on gsd/M001/S01 — capture should be no-op (slice branch rejected)
303
- captureIntegrationBranch(repo, "M001");
304
- switchToMain(repo);
305
- assertEq(readIntegrationBranch(repo, "M001"), "f-first",
306
- "capture from slice branch is no-op, original preserved");
307
- assertEq(getCurrentBranch(repo), "f-first",
308
- "switchToMain returns to feature branch, confirming integration branch works");
309
-
310
- rmSync(repo, { recursive: true, force: true });
311
- }
312
-
313
116
  // ── captureIntegrationBranch skips slice branches ─────────────────────
314
117
 
315
118
  console.log("\n=== captureIntegrationBranch: skips slice branches ===");
@@ -359,234 +162,6 @@ async function main(): Promise<void> {
359
162
  rmSync(repo, { recursive: true, force: true });
360
163
  }
361
164
 
362
- // ── Full multi-slice lifecycle on a feature branch ────────────────────
363
- //
364
- // Simulates what auto.ts does: start on feature branch, capture it,
365
- // create S01, work, merge S01 back to feature branch, then S02 branches
366
- // from feature branch (not main), works, merges to feature branch.
367
- // Main stays untouched throughout.
368
-
369
- console.log("\n=== Multi-slice lifecycle on feature branch ===");
370
-
371
- {
372
- const repo = mkdtempSync(join(tmpdir(), "gsd-integ-multi-"));
373
- run("git init -b main", repo);
374
- run("git config user.name 'Pi Test'", repo);
375
- run("git config user.email 'pi@example.com'", repo);
376
- writeFileSync(join(repo, "README.md"), "base\n");
377
- run("git add -A && git commit -m init", repo);
378
-
379
- // User creates feature branch
380
- run("git checkout -b feature/big-change", repo);
381
- writeFileSync(join(repo, "setup.txt"), "feature setup\n");
382
- run('git add -A && git commit -m "feat: initial setup"', repo);
383
-
384
- // auto.ts startup: capture + set milestone
385
- captureIntegrationBranch(repo, "M001");
386
- setActiveMilestoneId(repo, "M001");
387
-
388
- assertEq(getMainBranch(repo), "feature/big-change",
389
- "multi: getMainBranch returns feature branch");
390
-
391
- // ── S01 lifecycle ──────────────────────────────────────────────────
392
- ensureSliceBranch(repo, "M001", "S01");
393
- assertEq(getCurrentBranch(repo), "gsd/M001/S01", "multi: on S01");
394
-
395
- // Verify S01 has feature branch content
396
- assertTrue(existsSync(join(repo, "setup.txt")),
397
- "multi: S01 inherited feature branch content");
398
-
399
- writeFileSync(join(repo, "s01-work.txt"), "s01 output\n");
400
- run('git add -A && git commit -m "feat(S01): work"', repo);
401
-
402
- switchToMain(repo);
403
- assertEq(getCurrentBranch(repo), "feature/big-change",
404
- "multi: switchToMain goes to feature branch");
405
-
406
- const s01merge = mergeSliceToMain(repo, "M001", "S01", "First slice");
407
- assertEq(getCurrentBranch(repo), "feature/big-change",
408
- "multi: after S01 merge, on feature branch");
409
- assertTrue(existsSync(join(repo, "s01-work.txt")),
410
- "multi: S01 work merged to feature branch");
411
- assertTrue(s01merge.deletedBranch, "multi: S01 branch deleted");
412
-
413
- // Main should NOT have S01 work
414
- run("git stash", repo); // stash any .gsd changes
415
- run("git checkout main", repo);
416
- assertTrue(!existsSync(join(repo, "s01-work.txt")),
417
- "multi: main does NOT have S01 work");
418
- run("git checkout feature/big-change", repo);
419
- run("git stash pop || true", repo);
420
-
421
- // ── S02 lifecycle ──────────────────────────────────────────────────
422
- // S02 should branch from feature/big-change which now has S01's work
423
- ensureSliceBranch(repo, "M001", "S02");
424
- assertEq(getCurrentBranch(repo), "gsd/M001/S02", "multi: on S02");
425
-
426
- // S02 should have S01's merged output (branched from feature branch)
427
- assertTrue(existsSync(join(repo, "s01-work.txt")),
428
- "multi: S02 has S01 output (inherited via feature branch)");
429
-
430
- writeFileSync(join(repo, "s02-work.txt"), "s02 output\n");
431
- run('git add -A && git commit -m "feat(S02): work"', repo);
432
-
433
- switchToMain(repo);
434
- assertEq(getCurrentBranch(repo), "feature/big-change",
435
- "multi: switchToMain goes to feature branch after S02");
436
-
437
- const s02merge = mergeSliceToMain(repo, "M001", "S02", "Second slice");
438
- assertEq(getCurrentBranch(repo), "feature/big-change",
439
- "multi: after S02 merge, on feature branch");
440
- assertTrue(existsSync(join(repo, "s02-work.txt")),
441
- "multi: S02 work merged to feature branch");
442
- assertTrue(existsSync(join(repo, "s01-work.txt")),
443
- "multi: S01 work still on feature branch after S02 merge");
444
- assertTrue(s02merge.deletedBranch, "multi: S02 branch deleted");
445
-
446
- // Final check: main still untouched
447
- run("git stash", repo);
448
- run("git checkout main", repo);
449
- assertTrue(!existsSync(join(repo, "s01-work.txt")),
450
- "multi: main still lacks S01 work at end");
451
- assertTrue(!existsSync(join(repo, "s02-work.txt")),
452
- "multi: main still lacks S02 work at end");
453
- assertEq(readFileSync(join(repo, "README.md"), "utf-8").trim(), "base",
454
- "multi: main README unchanged");
455
-
456
- rmSync(repo, { recursive: true, force: true });
457
- }
458
-
459
- // ── Resume scenario: milestone ID re-set after restart ────────────────
460
- //
461
- // Simulates crash + restart: the cached GitServiceImpl is lost, but the
462
- // metadata file persists on disk. Re-calling setActiveMilestoneId should
463
- // restore integration branch resolution.
464
-
465
- console.log("\n=== Resume: milestone ID re-set restores integration branch ===");
466
-
467
- {
468
- const repo = mkdtempSync(join(tmpdir(), "gsd-integ-resume-"));
469
- run("git init -b main", repo);
470
- run("git config user.name 'Pi Test'", repo);
471
- run("git config user.email 'pi@example.com'", repo);
472
- writeFileSync(join(repo, "README.md"), "init\n");
473
- run("git add -A && git commit -m init", repo);
474
-
475
- run("git checkout -b my-feature", repo);
476
- captureIntegrationBranch(repo, "M001");
477
- setActiveMilestoneId(repo, "M001");
478
-
479
- // Create a slice and do some work
480
- ensureSliceBranch(repo, "M001", "S01");
481
- writeFileSync(join(repo, "work.txt"), "wip\n");
482
- run('git add -A && git commit -m "wip"', repo);
483
-
484
- // Simulate "restart" — clear milestone ID (fresh service instance)
485
- setActiveMilestoneId(repo, null);
486
- assertEq(getMainBranch(repo), "main",
487
- "resume: getMainBranch returns main when milestone cleared");
488
-
489
- // Re-set milestone ID (what auto.ts does on resume)
490
- setActiveMilestoneId(repo, "M001");
491
- assertEq(getMainBranch(repo), "my-feature",
492
- "resume: getMainBranch returns feature branch after re-set");
493
-
494
- // Full lifecycle still works after resume
495
- switchToMain(repo);
496
- assertEq(getCurrentBranch(repo), "my-feature",
497
- "resume: switchToMain goes to feature branch after re-set");
498
-
499
- const result = mergeSliceToMain(repo, "M001", "S01", "Resume slice");
500
- assertEq(getCurrentBranch(repo), "my-feature",
501
- "resume: merge lands on feature branch after re-set");
502
- assertTrue(existsSync(join(repo, "work.txt")),
503
- "resume: merged work exists on feature branch");
504
-
505
- rmSync(repo, { recursive: true, force: true });
506
- }
507
-
508
- // ── Backward compat: no metadata file, plain main workflow ────────────
509
- //
510
- // Simulates existing projects that were created before this feature.
511
- // No metadata file exists, milestone ID is set — getMainBranch should
512
- // still return "main" and the entire slice lifecycle works unchanged.
513
-
514
- console.log("\n=== Backward compat: no metadata, main workflow ===");
515
-
516
- {
517
- const repo = mkdtempSync(join(tmpdir(), "gsd-integ-compat-"));
518
- run("git init -b main", repo);
519
- run("git config user.name 'Pi Test'", repo);
520
- run("git config user.email 'pi@example.com'", repo);
521
- writeFileSync(join(repo, "README.md"), "init\n");
522
- run("git add -A && git commit -m init", repo);
523
-
524
- // Set milestone but DON'T capture integration branch (simulates old project)
525
- setActiveMilestoneId(repo, "M001");
526
-
527
- assertEq(getMainBranch(repo), "main",
528
- "compat: getMainBranch returns main without metadata");
529
-
530
- // Full lifecycle on main still works
531
- ensureSliceBranch(repo, "M001", "S01");
532
- writeFileSync(join(repo, "feature.txt"), "new\n");
533
- run('git add -A && git commit -m "feat: work"', repo);
534
-
535
- switchToMain(repo);
536
- assertEq(getCurrentBranch(repo), "main",
537
- "compat: switchToMain goes to main");
538
-
539
- const result = mergeSliceToMain(repo, "M001", "S01", "Compat slice");
540
- assertEq(getCurrentBranch(repo), "main",
541
- "compat: merge lands on main");
542
- assertTrue(existsSync(join(repo, "feature.txt")),
543
- "compat: merged work exists on main");
544
- assertTrue(result.deletedBranch, "compat: branch deleted");
545
-
546
- rmSync(repo, { recursive: true, force: true });
547
- }
548
-
549
- // ── ensureSliceBranch from another slice with integration branch ──────
550
- //
551
- // When on gsd/M001/S01 and creating S02, the code falls back to
552
- // getMainBranch() (not the current slice). With integration branch set,
553
- // S02 should branch from the feature branch.
554
-
555
- console.log("\n=== ensureSliceBranch: S02 from S01 uses integration branch as base ===");
556
-
557
- {
558
- const repo = mkdtempSync(join(tmpdir(), "gsd-integ-chain-"));
559
- run("git init -b main", repo);
560
- run("git config user.name 'Pi Test'", repo);
561
- run("git config user.email 'pi@example.com'", repo);
562
- writeFileSync(join(repo, "README.md"), "init\n");
563
- run("git add -A && git commit -m init", repo);
564
-
565
- run("git checkout -b dev-branch", repo);
566
- writeFileSync(join(repo, "dev-only.txt"), "from dev\n");
567
- run('git add -A && git commit -m "dev setup"', repo);
568
-
569
- captureIntegrationBranch(repo, "M001");
570
- setActiveMilestoneId(repo, "M001");
571
-
572
- // Create S01 (from dev-branch)
573
- ensureSliceBranch(repo, "M001", "S01");
574
- writeFileSync(join(repo, "s01.txt"), "s01\n");
575
- run('git add -A && git commit -m "s01 work"', repo);
576
-
577
- // While on S01, create S02 — should fall back to integration branch
578
- ensureSliceBranch(repo, "M001", "S02");
579
- assertEq(getCurrentBranch(repo), "gsd/M001/S02", "chain: on S02");
580
-
581
- // S02 should be based on dev-branch (the integration branch)
582
- assertTrue(existsSync(join(repo, "dev-only.txt")),
583
- "chain: S02 has dev-branch content");
584
- assertTrue(!existsSync(join(repo, "s01.txt")),
585
- "chain: S02 does NOT have S01 content (not chained from S01)");
586
-
587
- rmSync(repo, { recursive: true, force: true });
588
- }
589
-
590
165
  rmSync(base, { recursive: true, force: true });
591
166
  report();
592
167
  }
@@ -175,7 +175,6 @@ export interface GSDState {
175
175
  recentDecisions: string[];
176
176
  blockers: string[];
177
177
  nextAction: string;
178
- activeBranch?: string;
179
178
  activeWorkspace?: string;
180
179
  registry: MilestoneRegistryEntry[];
181
180
  requirements?: RequirementCounts;