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
@@ -1,107 +0,0 @@
1
- /**
2
- * isolation-resolver.test.ts -- Tests for shouldUseWorktreeIsolation resolver.
3
- *
4
- * Tests three resolution paths:
5
- * 1. Explicit git.isolation preference overrides everything
6
- * 2. Legacy detection: existing gsd/*\/* branches = branch mode
7
- * 3. Default: new project = worktree mode
8
- */
9
-
10
- import { mkdtempSync, writeFileSync, rmSync, realpathSync } from "node:fs";
11
- import { join } from "node:path";
12
- import { tmpdir } from "node:os";
13
- import { execSync } from "node:child_process";
14
-
15
- import { shouldUseWorktreeIsolation } from "../auto-worktree.ts";
16
- import { createTestContext } from "./test-helpers.ts";
17
-
18
- const { assertEq, report } = createTestContext();
19
-
20
- function run(command: string, cwd: string): string {
21
- return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
22
- }
23
-
24
- function createTempRepo(): string {
25
- const dir = realpathSync(mkdtempSync(join(tmpdir(), "iso-resolver-test-")));
26
- run("git init", dir);
27
- run("git config user.email test@test.com", dir);
28
- run("git config user.name Test", dir);
29
- writeFileSync(join(dir, "README.md"), "# test\n");
30
- run("git add .", dir);
31
- run("git commit -m init", dir);
32
- run("git branch -M main", dir);
33
- return dir;
34
- }
35
-
36
- async function main(): Promise<void> {
37
- const savedCwd = process.cwd();
38
-
39
- console.log("\n=== shouldUseWorktreeIsolation ===");
40
-
41
- // Test 1: New project with no gsd branches → defaults to worktree (true)
42
- {
43
- const dir = createTempRepo();
44
- try {
45
- const result = shouldUseWorktreeIsolation(dir);
46
- assertEq(result, true, "new project defaults to worktree isolation");
47
- } finally {
48
- process.chdir(savedCwd);
49
- rmSync(dir, { recursive: true, force: true });
50
- }
51
- }
52
-
53
- // Test 2: Legacy project with gsd/*/* branches → returns false (branch mode)
54
- {
55
- const dir = createTempRepo();
56
- try {
57
- // Create a legacy gsd/*/* branch
58
- run("git checkout -b gsd/M001/S01", dir);
59
- writeFileSync(join(dir, "slice.md"), "# S01\n");
60
- run("git add .", dir);
61
- run("git commit -m 'slice work'", dir);
62
- run("git checkout main", dir);
63
-
64
- const result = shouldUseWorktreeIsolation(dir);
65
- assertEq(result, false, "legacy project with gsd branches → branch mode");
66
- } finally {
67
- process.chdir(savedCwd);
68
- rmSync(dir, { recursive: true, force: true });
69
- }
70
- }
71
-
72
- // Test 3: Explicit preference override -- isolation: "worktree"
73
- {
74
- const dir = createTempRepo();
75
- try {
76
- // Create legacy branches that would normally trigger branch mode
77
- run("git checkout -b gsd/M001/S01", dir);
78
- writeFileSync(join(dir, "slice.md"), "# S01\n");
79
- run("git add .", dir);
80
- run("git commit -m 'slice work'", dir);
81
- run("git checkout main", dir);
82
-
83
- const result = shouldUseWorktreeIsolation(dir, { isolation: "worktree" });
84
- assertEq(result, true, "explicit isolation: worktree overrides legacy detection");
85
- } finally {
86
- process.chdir(savedCwd);
87
- rmSync(dir, { recursive: true, force: true });
88
- }
89
- }
90
-
91
- // Test 4: Explicit preference override -- isolation: "branch"
92
- {
93
- const dir = createTempRepo();
94
- try {
95
- // No legacy branches -- would normally default to worktree
96
- const result = shouldUseWorktreeIsolation(dir, { isolation: "branch" });
97
- assertEq(result, false, "explicit isolation: branch overrides default");
98
- } finally {
99
- process.chdir(savedCwd);
100
- rmSync(dir, { recursive: true, force: true });
101
- }
102
- }
103
-
104
- report();
105
- }
106
-
107
- main();
@@ -1,353 +0,0 @@
1
- /**
2
- * Tests for orphaned completed slice branch detection.
3
- *
4
- * Verifies the git operations and detection logic that mergeOrphanedSliceBranches
5
- * in auto.ts relies on — without importing auto.ts (which requires @gsd/pi-coding-agent).
6
- * Uses execSync directly and roadmap-slices.ts (no pi-coding-agent dep) to replicate
7
- * the detection logic.
8
- */
9
-
10
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
11
- import { execSync, execFileSync } from "node:child_process";
12
- import { join } from "node:path";
13
- import { tmpdir } from "node:os";
14
-
15
- import { relMilestoneFile } from "../paths.ts";
16
- import { parseRoadmapSlices } from "../roadmap-slices.ts";
17
-
18
- // Inline SLICE_BRANCH_RE and parseSliceBranch to avoid importing worktree.ts,
19
- // which transitively imports preferences.ts → @gsd/pi-coding-agent (not available in tests).
20
- const SLICE_BRANCH_RE = /^gsd\/(?:([a-zA-Z0-9_-]+)\/)?(M\d+)\/(S\d+)$/;
21
-
22
- function parseSliceBranch(
23
- branchName: string,
24
- ): { worktreeName: string | null; milestoneId: string; sliceId: string } | null {
25
- const match = branchName.match(SLICE_BRANCH_RE);
26
- if (!match) return null;
27
- return { worktreeName: match[1] ?? null, milestoneId: match[2]!, sliceId: match[3]! };
28
- }
29
-
30
- let passed = 0;
31
- let failed = 0;
32
-
33
- function assert(condition: boolean, message: string): void {
34
- if (condition) {
35
- passed++;
36
- } else {
37
- failed++;
38
- console.error(` FAIL: ${message}`);
39
- }
40
- }
41
-
42
- function assertEq<T>(actual: T, expected: T, message: string): void {
43
- if (JSON.stringify(actual) === JSON.stringify(expected)) {
44
- passed++;
45
- } else {
46
- failed++;
47
- console.error(
48
- ` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
49
- );
50
- }
51
- }
52
-
53
- function run(command: string, cwd: string): string {
54
- return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
55
- }
56
-
57
- function git(base: string, args: string[]): string {
58
- try {
59
- return execFileSync("git", args, {
60
- cwd: base,
61
- stdio: ["ignore", "pipe", "pipe"],
62
- encoding: "utf-8",
63
- }).trim();
64
- } catch {
65
- return "";
66
- }
67
- }
68
-
69
- /**
70
- * Replicate the core orphan-detection logic from mergeOrphanedSliceBranches
71
- * in auto.ts — using only paths.ts + roadmap-slices.ts + execSync (no pi-coding-agent deps).
72
- * Returns a list of orphaned branch descriptors.
73
- */
74
- function detectOrphanedSliceBranches(base: string): Array<{
75
- branch: string;
76
- milestoneId: string;
77
- sliceId: string;
78
- sliceTitle: string;
79
- }> {
80
- const orphans: Array<{
81
- branch: string;
82
- milestoneId: string;
83
- sliceId: string;
84
- sliceTitle: string;
85
- }> = [];
86
-
87
- const branchListRaw = git(base, ["branch", "--list", "gsd/*/*", "--format=%(refname:short)"]);
88
- if (!branchListRaw) return orphans;
89
-
90
- const branches = branchListRaw.split("\n").map(b => b.trim()).filter(Boolean);
91
- for (const branch of branches) {
92
- const parsed = parseSliceBranch(branch);
93
- // Skip worktree-namespaced branches
94
- if (!parsed || parsed.worktreeName) continue;
95
-
96
- const { milestoneId, sliceId } = parsed;
97
-
98
- // Skip if already merged (no commits ahead of main)
99
- const aheadCount = git(base, ["rev-list", "--count", `main..${branch}`]);
100
- if (!aheadCount || aheadCount === "0") continue;
101
-
102
- // Read roadmap from the slice branch
103
- const roadmapRelPath = relMilestoneFile(base, milestoneId, "ROADMAP");
104
- const roadmapContent = git(base, ["show", `${branch}:${roadmapRelPath}`]);
105
- if (!roadmapContent) continue;
106
-
107
- const slices = parseRoadmapSlices(roadmapContent);
108
- const sliceEntry = slices.find(s => s.id === sliceId);
109
- if (!sliceEntry?.done) continue;
110
-
111
- orphans.push({
112
- branch,
113
- milestoneId,
114
- sliceId,
115
- sliceTitle: sliceEntry.title || sliceId,
116
- });
117
- }
118
-
119
- return orphans;
120
- }
121
-
122
- // ─── Setup helpers ─────────────────────────────────────────────────────────
123
-
124
- function initRepo(): string {
125
- const repo = mkdtempSync(join(tmpdir(), "gsd-orphan-test-"));
126
- run("git init -b main", repo);
127
- run("git config user.email test@example.com", repo);
128
- run("git config user.name Test", repo);
129
- return repo;
130
- }
131
-
132
- function writeBaseArtifacts(repo: string): void {
133
- mkdirSync(join(repo, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), {
134
- recursive: true,
135
- });
136
-
137
- writeFileSync(
138
- join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
139
- [
140
- "# M001: Demo",
141
- "",
142
- "## Slices",
143
- "- [ ] **S01: First Slice** `risk:low` `depends:[]`",
144
- " > After this: feature works",
145
- "",
146
- ].join("\n"),
147
- );
148
-
149
- writeFileSync(
150
- join(repo, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"),
151
- "# S01: First Slice\n\n**Goal:** Demo\n**Demo:** Demo\n\n## Must-Haves\n- done\n\n## Tasks\n- [x] **T01: Task** `est:5m`\n do it\n",
152
- );
153
- run("git add .", repo);
154
- run('git commit -m "chore: milestone base"', repo);
155
- }
156
-
157
- function writeCompletedArtifactsOnBranch(repo: string): void {
158
- writeFileSync(
159
- join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
160
- [
161
- "# M001: Demo",
162
- "",
163
- "## Slices",
164
- "- [x] **S01: First Slice** `risk:low` `depends:[]`",
165
- " > After this: feature works",
166
- "",
167
- ].join("\n"),
168
- );
169
- writeFileSync(
170
- join(repo, ".gsd", "milestones", "M001", "slices", "S01", "S01-SUMMARY.md"),
171
- "# S01: First Slice\n\nDone.\n",
172
- );
173
- writeFileSync(
174
- join(repo, ".gsd", "milestones", "M001", "slices", "S01", "S01-UAT.md"),
175
- "# UAT\n\nPassed.\n",
176
- );
177
- run("git add .", repo);
178
- run('git commit -m "feat(M001/S01): complete-slice"', repo);
179
- }
180
-
181
- // ─── Tests ────────────────────────────────────────────────────────────────────
182
-
183
- console.log("\n=== parseSliceBranch: plain branch ===");
184
- {
185
- const parsed = parseSliceBranch("gsd/M001/S01");
186
- assert(parsed !== null, "plain branch parsed");
187
- assertEq(parsed?.milestoneId, "M001", "milestone ID extracted");
188
- assertEq(parsed?.sliceId, "S01", "slice ID extracted");
189
- assertEq(parsed?.worktreeName, null, "no worktree name for plain branch");
190
- }
191
-
192
- console.log("\n=== parseSliceBranch: worktree-namespaced branch ===");
193
- {
194
- const parsed = parseSliceBranch("gsd/wt1/M001/S01");
195
- assert(parsed !== null, "worktree branch parsed");
196
- assertEq(parsed?.milestoneId, "M001", "milestone ID extracted from worktree branch");
197
- assertEq(parsed?.sliceId, "S01", "slice ID extracted from worktree branch");
198
- assertEq(parsed?.worktreeName, "wt1", "worktree name extracted");
199
- }
200
-
201
- console.log("\n=== parseSliceBranch: non-slice branch not matched ===");
202
- {
203
- assert(parseSliceBranch("main") === null, "main branch not matched");
204
- assert(parseSliceBranch("gsd/M001") === null, "bare milestone branch not matched");
205
- assert(!SLICE_BRANCH_RE.test("gsd/M001"), "bare milestone branch not matched by regex");
206
- assert(SLICE_BRANCH_RE.test("gsd/M001/S01"), "standard slice branch matched by regex");
207
- }
208
-
209
- console.log("\n=== orphan detection: no slice branches ===");
210
- {
211
- const repo = initRepo();
212
- writeBaseArtifacts(repo);
213
-
214
- const orphans = detectOrphanedSliceBranches(repo);
215
- assertEq(orphans.length, 0, "no orphans when no slice branches exist");
216
-
217
- rmSync(repo, { recursive: true, force: true });
218
- }
219
-
220
- console.log("\n=== orphan detection: slice branch not done ===");
221
- {
222
- const repo = initRepo();
223
- writeBaseArtifacts(repo);
224
-
225
- run("git checkout -b gsd/M001/S01", repo);
226
- writeFileSync(
227
- join(repo, ".gsd", "milestones", "M001", "slices", "S01", "S01-RESEARCH.md"),
228
- "# Research\n",
229
- );
230
- run("git add .", repo);
231
- run('git commit -m "feat: research"', repo);
232
- run("git checkout main", repo);
233
-
234
- const orphans = detectOrphanedSliceBranches(repo);
235
- assertEq(orphans.length, 0, "incomplete slice branch is not reported as orphan");
236
-
237
- rmSync(repo, { recursive: true, force: true });
238
- }
239
-
240
- console.log("\n=== orphan detection: completed slice branch (orphaned) ===");
241
- {
242
- const repo = initRepo();
243
- writeBaseArtifacts(repo);
244
-
245
- run("git checkout -b gsd/M001/S01", repo);
246
- writeCompletedArtifactsOnBranch(repo);
247
- // Return to main without merging — this is the orphaned branch scenario
248
- run("git checkout main", repo);
249
-
250
- const orphans = detectOrphanedSliceBranches(repo);
251
- assertEq(orphans.length, 1, "completed but unmerged branch detected as orphan");
252
- assertEq(orphans[0]?.branch, "gsd/M001/S01", "correct branch name reported");
253
- assertEq(orphans[0]?.milestoneId, "M001", "correct milestone ID");
254
- assertEq(orphans[0]?.sliceId, "S01", "correct slice ID");
255
- assertEq(orphans[0]?.sliceTitle, "First Slice", "correct slice title");
256
-
257
- rmSync(repo, { recursive: true, force: true });
258
- }
259
-
260
- console.log("\n=== orphan detection: already merged branch is not orphan ===");
261
- {
262
- const repo = initRepo();
263
- writeBaseArtifacts(repo);
264
-
265
- run("git checkout -b gsd/M001/S01", repo);
266
- writeCompletedArtifactsOnBranch(repo);
267
- run("git checkout main", repo);
268
- run("git merge --squash gsd/M001/S01", repo);
269
- run('git commit -m "feat(M001/S01): merge"', repo);
270
- run("git branch -D gsd/M001/S01", repo);
271
-
272
- const orphans = detectOrphanedSliceBranches(repo);
273
- assertEq(orphans.length, 0, "already-merged branch is not detected as orphan");
274
-
275
- rmSync(repo, { recursive: true, force: true });
276
- }
277
-
278
- console.log("\n=== orphan detection: worktree-namespaced branch is skipped ===");
279
- {
280
- const repo = initRepo();
281
- writeBaseArtifacts(repo);
282
-
283
- // gsd/wt1/M001/S01 — worktree-namespaced branches are managed by the worktree
284
- // manager and must not be merged by the main-tree orphan check.
285
- run("git checkout -b gsd/wt1/M001/S01", repo);
286
- writeCompletedArtifactsOnBranch(repo);
287
- run("git checkout main", repo);
288
-
289
- const orphans = detectOrphanedSliceBranches(repo);
290
- assertEq(orphans.length, 0, "worktree-namespaced branch not detected by main-tree orphan check");
291
-
292
- rmSync(repo, { recursive: true, force: true });
293
- }
294
-
295
- console.log("\n=== orphan detection: relMilestoneFile resolves roadmap path for git show ===");
296
- {
297
- const repo = initRepo();
298
- writeBaseArtifacts(repo);
299
-
300
- run("git checkout -b gsd/M001/S01", repo);
301
- writeCompletedArtifactsOnBranch(repo);
302
- run("git checkout main", repo);
303
-
304
- // Simulate what mergeOrphanedSliceBranches does: read roadmap from branch
305
- const roadmapRelPath = relMilestoneFile(repo, "M001", "ROADMAP");
306
- const roadmapOnBranch = git(repo, ["show", `gsd/M001/S01:${roadmapRelPath}`]);
307
- assert(roadmapOnBranch.length > 0, "roadmap readable from orphaned branch via git show");
308
-
309
- const slices = parseRoadmapSlices(roadmapOnBranch);
310
- const s01 = slices.find(s => s.id === "S01");
311
- assert(s01?.done === true, "slice marked done on orphaned branch");
312
-
313
- rmSync(repo, { recursive: true, force: true });
314
- }
315
-
316
- console.log("\n=== orphan merge: squash-merge resolves orphan, artifacts appear on main ===");
317
- {
318
- const repo = initRepo();
319
- writeBaseArtifacts(repo);
320
-
321
- run("git checkout -b gsd/M001/S01", repo);
322
- writeCompletedArtifactsOnBranch(repo);
323
- run("git checkout main", repo);
324
-
325
- const orphansBefore = detectOrphanedSliceBranches(repo);
326
- assertEq(orphansBefore.length, 1, "orphan detected before merge");
327
-
328
- // Perform squash-merge (as mergeOrphanedSliceBranches does via mergeSliceToMain)
329
- run("git merge --squash gsd/M001/S01", repo);
330
- run('git commit -m "feat(M001/S01): recover orphaned branch"', repo);
331
- run("git branch -D gsd/M001/S01", repo);
332
-
333
- // Verify artifacts are now on main
334
- assert(
335
- existsSync(
336
- join(repo, ".gsd", "milestones", "M001", "slices", "S01", "S01-SUMMARY.md"),
337
- ),
338
- "SUMMARY merged to main after orphan recovery",
339
- );
340
- assert(
341
- existsSync(join(repo, ".gsd", "milestones", "M001", "slices", "S01", "S01-UAT.md")),
342
- "UAT merged to main after orphan recovery",
343
- );
344
-
345
- // Orphan no longer detected after merge + branch delete
346
- const orphansAfter = detectOrphanedSliceBranches(repo);
347
- assertEq(orphansAfter.length, 0, "no orphans after merge and branch deletion");
348
-
349
- rmSync(repo, { recursive: true, force: true });
350
- }
351
-
352
- console.log(`\nResults: ${passed} passed, ${failed} failed`);
353
- if (failed > 0) process.exit(1);