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
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { existsSync, mkdirSync, realpathSync } from "node:fs";
19
19
  import { execSync } from "node:child_process";
20
- import { join, resolve } from "node:path";
20
+ import { join, resolve, sep } from "node:path";
21
21
 
22
22
  // ─── Types ─────────────────────────────────────────────────────────────────
23
23
 
@@ -213,7 +213,11 @@ export function listWorktrees(basePath: string): WorktreeInfo[] {
213
213
 
214
214
  const entryPath = wtLine.replace("worktree ", "");
215
215
  const branch = branchLine.replace("branch refs/heads/", "");
216
- const branchWorktreeName = branch.startsWith("worktree/") ? branch.slice("worktree/".length) : null;
216
+ const branchWorktreeName = branch.startsWith("worktree/")
217
+ ? branch.slice("worktree/".length)
218
+ : branch.startsWith("milestone/")
219
+ ? branch.slice("milestone/".length)
220
+ : null;
217
221
  const entryVariants = [resolve(entryPath)];
218
222
  if (existsSync(entryPath)) {
219
223
  entryVariants.push(realpathSync(entryPath));
@@ -272,7 +276,7 @@ export function removeWorktree(
272
276
  // If we're inside the worktree, move out first — git can't remove an in-use directory
273
277
  const cwd = process.cwd();
274
278
  const resolvedCwd = existsSync(cwd) ? realpathSync(cwd) : cwd;
275
- if (resolvedCwd === resolvedWtPath || resolvedCwd.startsWith(resolvedWtPath + "/")) {
279
+ if (resolvedCwd === resolvedWtPath || resolvedCwd.startsWith(resolvedWtPath + sep)) {
276
280
  process.chdir(basePath);
277
281
  }
278
282
 
@@ -1,18 +1,15 @@
1
1
  /**
2
- * GSD Slice Branch Management — Thin Facade
2
+ * GSD Worktree Utilities
3
3
  *
4
- * Simple branch-per-slice workflow. No worktrees, no registry.
5
- * Runtime state (metrics, activity, lock, STATE.md) is gitignored
6
- * so branch switches are clean.
4
+ * Pure utility functions for worktree name detection, legacy branch name
5
+ * parsing, and integration branch capture.
7
6
  *
8
- * All git-mutation functions delegate to GitServiceImpl from git-service.ts.
9
7
  * Pure utility functions (detectWorktreeName, getSliceBranchName, parseSliceBranch,
10
- * SLICE_BRANCH_RE) remain standalone.
8
+ * SLICE_BRANCH_RE) remain standalone for backwards compatibility.
11
9
  *
12
- * Flow:
13
- * 1. ensureSliceBranch() create + checkout slice branch
14
- * 2. agent does work, commits
15
- * 3. mergeSliceToMain() — checkout integration branch, squash-merge, delete slice branch
10
+ * Branchless architecture: all work commits sequentially on the milestone branch.
11
+ * Pure utility functions (detectWorktreeName, getSliceBranchName, parseSliceBranch,
12
+ * SLICE_BRANCH_RE) remain for backwards compatibility with legacy branches.
16
13
  */
17
14
 
18
15
  import { sep } from "node:path";
@@ -20,8 +17,6 @@ import { sep } from "node:path";
20
17
  import { GitServiceImpl, writeIntegrationBranch } from "./git-service.js";
21
18
  import { loadEffectiveGSDPreferences } from "./preferences.js";
22
19
 
23
- // Re-export MergeSliceResult from the canonical source (D014 — type-only re-export)
24
- export type { MergeSliceResult } from "./git-service.js";
25
20
  export { MergeConflictError } from "./git-service.js";
26
21
 
27
22
  // ─── Lazy GitServiceImpl Cache ─────────────────────────────────────────────
@@ -140,19 +135,6 @@ export function getCurrentBranch(basePath: string): string {
140
135
  return getService(basePath).getCurrentBranch();
141
136
  }
142
137
 
143
- /**
144
- * Ensure the slice branch exists and is checked out.
145
- * Creates the branch from the current branch if it's not a slice branch,
146
- * otherwise from main. This preserves planning artifacts (CONTEXT, ROADMAP,
147
- * etc.) that were committed on the working branch — which may differ from
148
- * the repo's default branch (e.g. `developer` vs `main`).
149
- * When inside a worktree, the branch is namespaced to avoid conflicts.
150
- * Returns true if the branch was newly created.
151
- */
152
- export function ensureSliceBranch(basePath: string, milestoneId: string, sliceId: string): boolean {
153
- return getService(basePath).ensureSliceBranch(milestoneId, sliceId);
154
- }
155
-
156
138
  /**
157
139
  * Auto-commit any dirty files in the current working tree.
158
140
  * Returns the commit message used, or null if already clean.
@@ -163,44 +145,4 @@ export function autoCommitCurrentBranch(
163
145
  return getService(basePath).autoCommit(unitType, unitId);
164
146
  }
165
147
 
166
- /**
167
- * Switch to the integration branch, auto-committing any dirty files on the current branch first.
168
- */
169
- export function switchToMain(basePath: string): void {
170
- getService(basePath).switchToMain();
171
- }
172
148
 
173
- /**
174
- * Squash-merge a completed slice branch into the integration branch.
175
- * Expects to already be on the integration branch (call switchToMain first).
176
- * Deletes the slice branch after merge.
177
- */
178
- export function mergeSliceToMain(
179
- basePath: string, milestoneId: string, sliceId: string, sliceTitle: string,
180
- ): import("./git-service.ts").MergeSliceResult {
181
- return getService(basePath).mergeSliceToMain(milestoneId, sliceId, sliceTitle);
182
- }
183
-
184
- // ─── Query Functions (delegate to GitServiceImpl) ──────────────────────────
185
-
186
- /**
187
- * Check if we're currently on a slice branch (not main).
188
- * Handles both plain (gsd/M001/S01) and worktree-namespaced (gsd/wt/M001/S01) branches.
189
- */
190
- export function isOnSliceBranch(basePath: string): boolean {
191
- const current = getCurrentBranch(basePath);
192
- return SLICE_BRANCH_RE.test(current);
193
- }
194
-
195
- /**
196
- * Get the active slice branch name, or null if on main.
197
- * Handles both plain and worktree-namespaced branch patterns.
198
- */
199
- export function getActiveSliceBranch(basePath: string): string | null {
200
- try {
201
- const current = getCurrentBranch(basePath);
202
- return SLICE_BRANCH_RE.test(current) ? current : null;
203
- } catch {
204
- return null;
205
- }
206
- }
@@ -90,8 +90,10 @@ export function registerSearchProviderCommand(pi: ExtensionAPI): void {
90
90
 
91
91
  setSearchProviderPreference(chosen)
92
92
  const effective = resolveSearchProvider()
93
+ const isAnthropic = ctx.model?.provider === 'anthropic'
94
+ const nativeNote = isAnthropic ? '\nNote: Native Anthropic web search is also active (automatic, no API key needed).' : ''
93
95
  ctx.ui.notify(
94
- `Search provider set to ${chosen}. Effective provider: ${effective ?? 'none (no API keys)'}`,
96
+ `Search provider set to ${chosen}. Effective provider: ${effective ?? 'none (no API keys)'}${nativeNote}`,
95
97
  'info',
96
98
  )
97
99
  },
@@ -1,282 +0,0 @@
1
- /**
2
- * auto-worktree-merge.test.ts — Integration tests for mergeSliceToMilestone.
3
- *
4
- * Covers: --no-ff merge topology, rich commit messages, slice branch deletion,
5
- * zero-commit error, real code conflicts, .gsd/ non-conflict in worktree mode.
6
- * All tests use real git operations in temp repos.
7
- */
8
-
9
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync } from "node:fs";
10
- import { join } from "node:path";
11
- import { tmpdir } from "node:os";
12
- import { execSync } from "node:child_process";
13
-
14
- import {
15
- createAutoWorktree,
16
- teardownAutoWorktree,
17
- mergeSliceToMilestone,
18
- } from "../auto-worktree.ts";
19
- import { MergeConflictError } from "../git-service.ts";
20
- import { getSliceBranchName } from "../worktree.ts";
21
-
22
- import { createTestContext } from "./test-helpers.ts";
23
-
24
- const { assertEq, assertTrue, assertMatch, report } = createTestContext();
25
-
26
- function run(cmd: string, cwd: string): string {
27
- return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
28
- }
29
-
30
- function createTempRepo(): string {
31
- const dir = realpathSync(mkdtempSync(join(tmpdir(), "wt-merge-test-")));
32
- run("git init", dir);
33
- run("git config user.email test@test.com", dir);
34
- run("git config user.name Test", dir);
35
- writeFileSync(join(dir, "README.md"), "# test\n");
36
- mkdirSync(join(dir, ".gsd"), { recursive: true });
37
- writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n");
38
- run("git add .", dir);
39
- run("git commit -m init", dir);
40
- run("git branch -M main", dir);
41
- return dir;
42
- }
43
-
44
- /** Create a slice branch in the worktree, add commits, return branch name. */
45
- function setupSliceBranch(
46
- wtPath: string,
47
- milestoneId: string,
48
- sliceId: string,
49
- commits: Array<{ file: string; content: string; message: string }>,
50
- ): string {
51
- // Detect worktree name for branch naming
52
- const normalizedPath = wtPath.replaceAll("\\", "/");
53
- const marker = "/.gsd/worktrees/";
54
- const idx = normalizedPath.indexOf(marker);
55
- const worktreeName = idx !== -1 ? normalizedPath.slice(idx + marker.length).split("/")[0] : null;
56
- const sliceBranch = getSliceBranchName(milestoneId, sliceId, worktreeName);
57
-
58
- run(`git checkout -b ${sliceBranch}`, wtPath);
59
- for (const c of commits) {
60
- writeFileSync(join(wtPath, c.file), c.content);
61
- run("git add .", wtPath);
62
- run(`git commit -m "${c.message}"`, wtPath);
63
- }
64
- return sliceBranch;
65
- }
66
-
67
- async function main(): Promise<void> {
68
- const savedCwd = process.cwd();
69
- const tempDirs: string[] = [];
70
-
71
- function freshRepo(): string {
72
- const d = createTempRepo();
73
- tempDirs.push(d);
74
- return d;
75
- }
76
-
77
- try {
78
- // ─── Test 1: Single slice --no-ff merge ────────────────────────────
79
- console.log("\n=== single slice --no-ff merge ===");
80
- {
81
- const repo = freshRepo();
82
- const wtPath = createAutoWorktree(repo, "M003");
83
-
84
- const sliceBranch = setupSliceBranch(wtPath, "M003", "S01", [
85
- { file: "a.ts", content: "const a = 1;\n", message: "add a.ts" },
86
- { file: "b.ts", content: "const b = 2;\n", message: "add b.ts" },
87
- { file: "c.ts", content: "const c = 3;\n", message: "add c.ts" },
88
- ]);
89
- run("git checkout milestone/M003", wtPath);
90
-
91
- const result = mergeSliceToMilestone(repo, "M003", "S01", "Add core files");
92
-
93
- // Verify we're back on milestone branch
94
- const branch = run("git branch --show-current", wtPath);
95
- assertEq(branch, "milestone/M003", "back on milestone branch after merge");
96
-
97
- // Verify merge topology via git log --graph
98
- const log = run("git log --oneline --graph", wtPath);
99
- assertTrue(log.includes("* "), "merge commit visible in graph (asterisk with two parents)");
100
- assertTrue(log.includes("add a.ts"), "slice commit 'add a.ts' visible");
101
- assertTrue(log.includes("add b.ts"), "slice commit 'add b.ts' visible");
102
- assertTrue(log.includes("add c.ts"), "slice commit 'add c.ts' visible");
103
-
104
- // Verify commit message format
105
- assertMatch(result.mergedCommitMessage, /feat\(M003\/S01\)/, "commit message has conventional format");
106
- assertTrue(result.mergedCommitMessage.includes("Add core files"), "commit message includes slice title");
107
-
108
- // Verify slice branch deleted
109
- assertTrue(result.deletedBranch, "slice branch deleted");
110
- const branches = run("git branch", wtPath);
111
- assertTrue(!branches.includes(sliceBranch), "slice branch no longer in git branch list");
112
-
113
- teardownAutoWorktree(repo, "M003");
114
- }
115
-
116
- // ─── Test 2: Two sequential slices ─────────────────────────────────
117
- console.log("\n=== two sequential slices ===");
118
- {
119
- const repo = freshRepo();
120
- const wtPath = createAutoWorktree(repo, "M003");
121
-
122
- // Slice S01
123
- setupSliceBranch(wtPath, "M003", "S01", [
124
- { file: "s1.ts", content: "export const s1 = 1;\n", message: "s1 work" },
125
- ]);
126
- run("git checkout milestone/M003", wtPath);
127
- mergeSliceToMilestone(repo, "M003", "S01", "First slice");
128
-
129
- // Slice S02
130
- setupSliceBranch(wtPath, "M003", "S02", [
131
- { file: "s2.ts", content: "export const s2 = 2;\n", message: "s2 work" },
132
- ]);
133
- run("git checkout milestone/M003", wtPath);
134
- mergeSliceToMilestone(repo, "M003", "S02", "Second slice");
135
-
136
- // Verify two merge boundaries
137
- const log = run("git log --oneline --graph", wtPath);
138
- const mergeLines = log.split("\n").filter(l => l.includes("* "));
139
- assertTrue(mergeLines.length >= 2, "two distinct merge commits in graph");
140
- assertTrue(log.includes("s1 work"), "S01 commit visible");
141
- assertTrue(log.includes("s2 work"), "S02 commit visible");
142
-
143
- teardownAutoWorktree(repo, "M003");
144
- }
145
-
146
- // ─── Test 3: Zero commits throws ───────────────────────────────────
147
- console.log("\n=== zero commits throws ===");
148
- {
149
- const repo = freshRepo();
150
- const wtPath = createAutoWorktree(repo, "M003");
151
-
152
- // Create slice branch with no commits ahead
153
- const normalizedPath = wtPath.replaceAll("\\", "/");
154
- const marker = "/.gsd/worktrees/";
155
- const idx = normalizedPath.indexOf(marker);
156
- const worktreeName = idx !== -1 ? normalizedPath.slice(idx + marker.length).split("/")[0] : null;
157
- const sliceBranch = getSliceBranchName("M003", "S01", worktreeName);
158
- run(`git checkout -b ${sliceBranch}`, wtPath);
159
- // No commits — immediately try to merge
160
- run(`git checkout milestone/M003`, wtPath);
161
-
162
- let threw = false;
163
- try {
164
- mergeSliceToMilestone(repo, "M003", "S01", "Empty slice");
165
- } catch (err) {
166
- threw = true;
167
- assertTrue(
168
- err instanceof Error && err.message.includes("no commits ahead"),
169
- "error message mentions no commits ahead",
170
- );
171
- }
172
- assertTrue(threw, "mergeSliceToMilestone throws on zero commits");
173
-
174
- teardownAutoWorktree(repo, "M003");
175
- }
176
-
177
- // ─── Test 4: Real code conflict throws MergeConflictError ──────────
178
- console.log("\n=== real code conflict throws MergeConflictError ===");
179
- {
180
- const repo = freshRepo();
181
- const wtPath = createAutoWorktree(repo, "M003");
182
-
183
- // Add a file on milestone branch
184
- writeFileSync(join(wtPath, "shared.ts"), "// version 1\n");
185
- run("git add .", wtPath);
186
- run('git commit -m "add shared.ts"', wtPath);
187
-
188
- // Create slice branch, modify same file differently
189
- const normalizedPath = wtPath.replaceAll("\\", "/");
190
- const marker = "/.gsd/worktrees/";
191
- const idx = normalizedPath.indexOf(marker);
192
- const worktreeName = idx !== -1 ? normalizedPath.slice(idx + marker.length).split("/")[0] : null;
193
- const sliceBranch = getSliceBranchName("M003", "S01", worktreeName);
194
- run(`git checkout -b ${sliceBranch}`, wtPath);
195
- writeFileSync(join(wtPath, "shared.ts"), "// slice version\nexport const x = 1;\n");
196
- run("git add .", wtPath);
197
- run('git commit -m "slice edit shared.ts"', wtPath);
198
-
199
- // Modify same file on milestone branch
200
- run("git checkout milestone/M003", wtPath);
201
- writeFileSync(join(wtPath, "shared.ts"), "// milestone version\nexport const y = 2;\n");
202
- run("git add .", wtPath);
203
- run('git commit -m "milestone edit shared.ts"', wtPath);
204
-
205
- // Go back to milestone branch for merge call
206
- run("git checkout milestone/M003", wtPath);
207
-
208
- let caught: MergeConflictError | null = null;
209
- try {
210
- mergeSliceToMilestone(repo, "M003", "S01", "Conflicting slice");
211
- } catch (err) {
212
- if (err instanceof MergeConflictError) {
213
- caught = err;
214
- } else {
215
- throw err;
216
- }
217
- }
218
-
219
- assertTrue(caught !== null, "MergeConflictError thrown on conflict");
220
- if (caught) {
221
- assertTrue(caught.conflictedFiles.includes("shared.ts"), "conflictedFiles includes shared.ts");
222
- assertEq(caught.strategy, "merge", "strategy is merge");
223
- assertTrue(caught.branch.includes("S01"), "branch includes S01");
224
- }
225
-
226
- // Clean up conflict state before teardown
227
- run("git merge --abort || true", wtPath);
228
- run("git checkout milestone/M003", wtPath);
229
- teardownAutoWorktree(repo, "M003");
230
- }
231
-
232
- // ─── Test 5: .gsd/ changes don't conflict ─────────────────────────
233
- console.log("\n=== .gsd/ changes don't conflict ===");
234
- {
235
- const repo = freshRepo();
236
- const wtPath = createAutoWorktree(repo, "M003");
237
-
238
- // The .gsd/ directory in worktrees is local — it's not shared via git
239
- // between the main repo and the worktree. So modifications to .gsd/
240
- // files in both branches shouldn't cause conflicts because .gsd/ is
241
- // in the main repo's tree but the worktree has its own working copy.
242
- //
243
- // In the worktree, .gsd/ IS tracked (inherited from main). But since
244
- // slice branches diverge from milestone branch, .gsd/ changes on both
245
- // can conflict. The key insight: in real auto-mode, .gsd/ changes only
246
- // happen on the milestone branch (planning artifacts), not on slice
247
- // branches (which only have code changes). So we test that code-only
248
- // slice commits merge cleanly even when milestone has .gsd/ changes.
249
-
250
- // Add a .gsd/ change on milestone branch
251
- writeFileSync(join(wtPath, ".gsd", "STATE.md"), "# Updated State\nactive: M003\n");
252
- run("git add .", wtPath);
253
- run('git commit -m "update .gsd/STATE.md on milestone"', wtPath);
254
-
255
- // Create slice branch with code-only changes
256
- setupSliceBranch(wtPath, "M003", "S01", [
257
- { file: "feature.ts", content: "export const feature = true;\n", message: "add feature" },
258
- ]);
259
- run("git checkout milestone/M003", wtPath);
260
-
261
- // Merge should succeed — no .gsd/ conflict since slice didn't touch .gsd/
262
- const result = mergeSliceToMilestone(repo, "M003", "S01", "Feature slice");
263
- assertTrue(result.branch.includes("S01"), ".gsd/ no-conflict merge succeeded");
264
- assertTrue(result.deletedBranch, "slice branch deleted after .gsd/-safe merge");
265
-
266
- // Verify feature file exists after merge
267
- assertTrue(existsSync(join(wtPath, "feature.ts")), "feature.ts present after merge");
268
-
269
- teardownAutoWorktree(repo, "M003");
270
- }
271
-
272
- } finally {
273
- process.chdir(savedCwd);
274
- for (const d of tempDirs) {
275
- if (existsSync(d)) rmSync(d, { recursive: true, force: true });
276
- }
277
- }
278
-
279
- report();
280
- }
281
-
282
- main();
@@ -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();