gsd-pi 2.12.0 → 2.13.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.
- package/dist/cli.js +18 -1
- package/dist/resource-loader.d.ts +2 -0
- package/dist/resource-loader.js +36 -1
- package/dist/resources/extensions/gsd/auto-worktree.ts +509 -0
- package/dist/resources/extensions/gsd/auto.ts +222 -11
- package/dist/resources/extensions/gsd/doctor.ts +195 -1
- package/dist/resources/extensions/gsd/git-self-heal.ts +198 -0
- package/dist/resources/extensions/gsd/git-service.ts +11 -0
- package/dist/resources/extensions/gsd/preferences.ts +17 -1
- package/dist/resources/extensions/gsd/prompts/system.md +32 -29
- package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
- package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
- package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
- package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
- package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +6 -4
- package/dist/resources/extensions/search-the-web/native-search.ts +15 -10
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +4 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +2 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +3 -3
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -0
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +1 -1
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +3 -3
- package/packages/pi-coding-agent/src/core/system-prompt.ts +9 -0
- package/packages/pi-tui/dist/components/editor.d.ts +11 -0
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +64 -6
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +71 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +509 -0
- package/src/resources/extensions/gsd/auto.ts +222 -11
- package/src/resources/extensions/gsd/doctor.ts +195 -1
- package/src/resources/extensions/gsd/git-self-heal.ts +198 -0
- package/src/resources/extensions/gsd/git-service.ts +11 -0
- package/src/resources/extensions/gsd/preferences.ts +17 -1
- package/src/resources/extensions/gsd/prompts/system.md +32 -29
- package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +6 -4
- package/src/resources/extensions/search-the-web/native-search.ts +15 -10
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-e2e.test.ts -- End-to-end tests for worktree-isolated git flow.
|
|
3
|
+
*
|
|
4
|
+
* Covers 5 cross-cutting groups not tested by individual slice tests:
|
|
5
|
+
* 1. Full lifecycle chain (create -> slice commits -> merge to milestone -> merge to main)
|
|
6
|
+
* 2. Preference gating (shouldUseWorktreeIsolation with overrides)
|
|
7
|
+
* 3. merge_to_main mode resolution (getMergeToMainMode)
|
|
8
|
+
* 4. Self-heal in merge context (abortAndReset, withMergeHeal)
|
|
9
|
+
* 5. Doctor detection of orphaned worktrees
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
mkdtempSync, mkdirSync, writeFileSync, rmSync,
|
|
14
|
+
existsSync, realpathSync, readFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { execSync } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
createAutoWorktree,
|
|
22
|
+
mergeMilestoneToMain,
|
|
23
|
+
mergeSliceToMilestone,
|
|
24
|
+
shouldUseWorktreeIsolation,
|
|
25
|
+
} from "../auto-worktree.ts";
|
|
26
|
+
import { getSliceBranchName } from "../worktree.ts";
|
|
27
|
+
import { abortAndReset, withMergeHeal, MergeConflictError } from "../git-self-heal.ts";
|
|
28
|
+
import { runGSDDoctor } from "../doctor.ts";
|
|
29
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
30
|
+
|
|
31
|
+
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
32
|
+
|
|
33
|
+
// ---- Helpers ----
|
|
34
|
+
|
|
35
|
+
function run(cmd: string, cwd: string): string {
|
|
36
|
+
return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createTempRepo(): string {
|
|
40
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "wt-e2e-test-")));
|
|
41
|
+
run("git init", dir);
|
|
42
|
+
run("git config user.email test@test.com", dir);
|
|
43
|
+
run("git config user.name Test", dir);
|
|
44
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
45
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
46
|
+
writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n");
|
|
47
|
+
run("git add .", dir);
|
|
48
|
+
run("git commit -m init", dir);
|
|
49
|
+
run("git branch -M main", dir);
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeRoadmap(
|
|
54
|
+
milestoneId: string,
|
|
55
|
+
title: string,
|
|
56
|
+
slices: Array<{ id: string; title: string }>,
|
|
57
|
+
): string {
|
|
58
|
+
const sliceLines = slices.map(s => `- [x] **${s.id}: ${s.title}**`).join("\n");
|
|
59
|
+
return `# ${milestoneId}: ${title}\n\n## Slices\n${sliceLines}\n`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function addSliceToMilestone(
|
|
63
|
+
repo: string,
|
|
64
|
+
wtPath: string,
|
|
65
|
+
milestoneId: string,
|
|
66
|
+
sliceId: string,
|
|
67
|
+
sliceTitle: string,
|
|
68
|
+
commits: Array<{ file: string; content: string; message: string }>,
|
|
69
|
+
): void {
|
|
70
|
+
const normalizedPath = wtPath.replaceAll("\\", "/");
|
|
71
|
+
const marker = "/.gsd/worktrees/";
|
|
72
|
+
const idx = normalizedPath.indexOf(marker);
|
|
73
|
+
const worktreeName = idx !== -1 ? normalizedPath.slice(idx + marker.length).split("/")[0] : null;
|
|
74
|
+
|
|
75
|
+
const sliceBranch = getSliceBranchName(milestoneId, sliceId, worktreeName);
|
|
76
|
+
|
|
77
|
+
run(`git checkout -b ${sliceBranch}`, wtPath);
|
|
78
|
+
for (const c of commits) {
|
|
79
|
+
writeFileSync(join(wtPath, c.file), c.content);
|
|
80
|
+
run("git add .", wtPath);
|
|
81
|
+
run(`git commit -m "${c.message}"`, wtPath);
|
|
82
|
+
}
|
|
83
|
+
run(`git checkout milestone/${milestoneId}`, wtPath);
|
|
84
|
+
mergeSliceToMilestone(repo, milestoneId, sliceId, sliceTitle);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function main(): Promise<void> {
|
|
88
|
+
const savedCwd = process.cwd();
|
|
89
|
+
const tempDirs: string[] = [];
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// ================================================================
|
|
93
|
+
// Group 1: Full lifecycle chain
|
|
94
|
+
// ================================================================
|
|
95
|
+
console.log("\n=== Full lifecycle: worktree -> slices -> milestone merge -> main ===");
|
|
96
|
+
{
|
|
97
|
+
const repo = createTempRepo();
|
|
98
|
+
tempDirs.push(repo);
|
|
99
|
+
|
|
100
|
+
// Count commits on main before
|
|
101
|
+
const mainLogBefore = run("git log --oneline main", repo);
|
|
102
|
+
const commitCountBefore = mainLogBefore.split("\n").length;
|
|
103
|
+
|
|
104
|
+
// Create worktree for M001
|
|
105
|
+
const wtPath = createAutoWorktree(repo, "M001");
|
|
106
|
+
tempDirs.push(wtPath);
|
|
107
|
+
assertTrue(existsSync(wtPath), "worktree directory created");
|
|
108
|
+
|
|
109
|
+
// Add two slices with commits
|
|
110
|
+
addSliceToMilestone(repo, wtPath, "M001", "S01", "Add auth", [
|
|
111
|
+
{ file: "auth.ts", content: "export const auth = true;\n", message: "feat: add auth" },
|
|
112
|
+
]);
|
|
113
|
+
addSliceToMilestone(repo, wtPath, "M001", "S02", "Add dashboard", [
|
|
114
|
+
{ file: "dash.ts", content: "export const dash = true;\n", message: "feat: add dashboard" },
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
// Build roadmap content
|
|
118
|
+
const roadmapContent = makeRoadmap("M001", "First milestone", [
|
|
119
|
+
{ id: "S01", title: "Add auth" },
|
|
120
|
+
{ id: "S02", title: "Add dashboard" },
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
// Merge milestone to main
|
|
124
|
+
process.chdir(wtPath);
|
|
125
|
+
const result = mergeMilestoneToMain(repo, "M001", roadmapContent);
|
|
126
|
+
process.chdir(savedCwd);
|
|
127
|
+
|
|
128
|
+
// Assert exactly one new commit on main
|
|
129
|
+
const mainLogAfter = run("git log --oneline main", repo);
|
|
130
|
+
const commitCountAfter = mainLogAfter.split("\n").length;
|
|
131
|
+
assertEq(commitCountAfter, commitCountBefore + 1, "exactly one new commit on main");
|
|
132
|
+
|
|
133
|
+
// Commit message contains both slice titles
|
|
134
|
+
const lastCommitMsg = run("git log -1 --format=%B main", repo);
|
|
135
|
+
assertMatch(lastCommitMsg, /Add auth/, "commit message contains S01 title");
|
|
136
|
+
assertMatch(lastCommitMsg, /Add dashboard/, "commit message contains S02 title");
|
|
137
|
+
|
|
138
|
+
// Worktree directory removed
|
|
139
|
+
assertTrue(!existsSync(wtPath), "worktree directory removed after merge");
|
|
140
|
+
|
|
141
|
+
// Milestone branch deleted
|
|
142
|
+
const branches = run("git branch", repo);
|
|
143
|
+
assertTrue(!branches.includes("milestone/M001"), "milestone branch deleted");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ================================================================
|
|
147
|
+
// Group 2: Preference gating (shouldUseWorktreeIsolation)
|
|
148
|
+
// ================================================================
|
|
149
|
+
console.log("\n=== Preference gating ===");
|
|
150
|
+
{
|
|
151
|
+
const repo = createTempRepo();
|
|
152
|
+
tempDirs.push(repo);
|
|
153
|
+
|
|
154
|
+
// Override to branch mode
|
|
155
|
+
const branchResult = shouldUseWorktreeIsolation(repo, { isolation: "branch" });
|
|
156
|
+
assertEq(branchResult, false, "isolation=branch returns false");
|
|
157
|
+
|
|
158
|
+
// Override to worktree mode
|
|
159
|
+
const wtResult = shouldUseWorktreeIsolation(repo, { isolation: "worktree" });
|
|
160
|
+
assertEq(wtResult, true, "isolation=worktree returns true");
|
|
161
|
+
|
|
162
|
+
// Default (no legacy branches) returns true
|
|
163
|
+
const defaultResult = shouldUseWorktreeIsolation(repo);
|
|
164
|
+
assertEq(defaultResult, true, "new project defaults to worktree (true)");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ================================================================
|
|
168
|
+
// Group 3: merge_to_main mode resolution
|
|
169
|
+
// ================================================================
|
|
170
|
+
console.log("\n=== merge_to_main mode ===");
|
|
171
|
+
{
|
|
172
|
+
// getMergeToMainMode reads from loadEffectiveGSDPreferences — test via legacy branch detection
|
|
173
|
+
// Instead, test that the function returns the default "milestone" when no prefs set
|
|
174
|
+
// (Cannot inject overridePrefs — function signature doesn't accept them)
|
|
175
|
+
// We verify the shouldUseWorktreeIsolation override path handles legacy detection
|
|
176
|
+
const repo = createTempRepo();
|
|
177
|
+
tempDirs.push(repo);
|
|
178
|
+
|
|
179
|
+
// Create a legacy gsd/*/* branch to test legacy detection
|
|
180
|
+
run("git checkout -b gsd/M001/S01", repo);
|
|
181
|
+
writeFileSync(join(repo, "legacy.txt"), "legacy\n");
|
|
182
|
+
run("git add .", repo);
|
|
183
|
+
run("git commit -m legacy", repo);
|
|
184
|
+
run("git checkout main", repo);
|
|
185
|
+
|
|
186
|
+
const legacyResult = shouldUseWorktreeIsolation(repo);
|
|
187
|
+
assertEq(legacyResult, false, "legacy gsd branches detected -> branch mode");
|
|
188
|
+
|
|
189
|
+
// Explicit worktree override wins over legacy detection
|
|
190
|
+
const overrideResult = shouldUseWorktreeIsolation(repo, { isolation: "worktree" });
|
|
191
|
+
assertEq(overrideResult, true, "explicit worktree override wins over legacy");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ================================================================
|
|
195
|
+
// Group 4: Self-heal (abortAndReset, withMergeHeal)
|
|
196
|
+
// ================================================================
|
|
197
|
+
console.log("\n=== Self-heal ===");
|
|
198
|
+
{
|
|
199
|
+
// 4a: abortAndReset cleans up MERGE_HEAD
|
|
200
|
+
const repo = createTempRepo();
|
|
201
|
+
tempDirs.push(repo);
|
|
202
|
+
|
|
203
|
+
// Create conflicting branches
|
|
204
|
+
run("git checkout -b feature", repo);
|
|
205
|
+
writeFileSync(join(repo, "conflict.txt"), "feature content\n");
|
|
206
|
+
run("git add .", repo);
|
|
207
|
+
run("git commit -m feature", repo);
|
|
208
|
+
run("git checkout main", repo);
|
|
209
|
+
writeFileSync(join(repo, "conflict.txt"), "main content\n");
|
|
210
|
+
run("git add .", repo);
|
|
211
|
+
run("git commit -m main-change", repo);
|
|
212
|
+
|
|
213
|
+
// Trigger merge conflict
|
|
214
|
+
try { run("git merge feature", repo); } catch { /* expected */ }
|
|
215
|
+
assertTrue(existsSync(join(repo, ".git", "MERGE_HEAD")), "MERGE_HEAD exists before abort");
|
|
216
|
+
|
|
217
|
+
const abortResult = abortAndReset(repo);
|
|
218
|
+
assertTrue(!existsSync(join(repo, ".git", "MERGE_HEAD")), "MERGE_HEAD removed after abort");
|
|
219
|
+
assertTrue(abortResult.cleaned.length > 0, "abortAndReset reports cleaned items");
|
|
220
|
+
}
|
|
221
|
+
{
|
|
222
|
+
// 4b: withMergeHeal throws MergeConflictError for real conflicts
|
|
223
|
+
const repo = createTempRepo();
|
|
224
|
+
tempDirs.push(repo);
|
|
225
|
+
|
|
226
|
+
run("git checkout -b conflict-branch", repo);
|
|
227
|
+
writeFileSync(join(repo, "file.txt"), "branch version\n");
|
|
228
|
+
run("git add .", repo);
|
|
229
|
+
run("git commit -m branch-ver", repo);
|
|
230
|
+
run("git checkout main", repo);
|
|
231
|
+
writeFileSync(join(repo, "file.txt"), "main version\n");
|
|
232
|
+
run("git add .", repo);
|
|
233
|
+
run("git commit -m main-ver", repo);
|
|
234
|
+
|
|
235
|
+
let caughtError: unknown = null;
|
|
236
|
+
try {
|
|
237
|
+
withMergeHeal(repo, () => {
|
|
238
|
+
execSync("git merge conflict-branch", { cwd: repo, stdio: "pipe" });
|
|
239
|
+
});
|
|
240
|
+
} catch (e) {
|
|
241
|
+
caughtError = e;
|
|
242
|
+
}
|
|
243
|
+
assertTrue(caughtError instanceof MergeConflictError, "withMergeHeal throws MergeConflictError");
|
|
244
|
+
if (caughtError instanceof MergeConflictError) {
|
|
245
|
+
assertTrue(caughtError.conflictedFiles.length > 0, "MergeConflictError has conflictedFiles");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ================================================================
|
|
250
|
+
// Group 5: Doctor detects orphaned worktrees
|
|
251
|
+
// ================================================================
|
|
252
|
+
console.log("\n=== Doctor: orphaned worktree detection ===");
|
|
253
|
+
{
|
|
254
|
+
// Build a repo with a completed milestone
|
|
255
|
+
const repo = createTempRepo();
|
|
256
|
+
tempDirs.push(repo);
|
|
257
|
+
|
|
258
|
+
// Create completed milestone roadmap
|
|
259
|
+
const msDir = join(repo, ".gsd", "milestones", "M001");
|
|
260
|
+
mkdirSync(msDir, { recursive: true });
|
|
261
|
+
writeFileSync(join(msDir, "ROADMAP.md"), `---
|
|
262
|
+
id: M001
|
|
263
|
+
title: "Test Milestone"
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
# M001: Test Milestone
|
|
267
|
+
|
|
268
|
+
## Vision
|
|
269
|
+
Test
|
|
270
|
+
|
|
271
|
+
## Success Criteria
|
|
272
|
+
- Done
|
|
273
|
+
|
|
274
|
+
## Slices
|
|
275
|
+
- [x] **S01: Test slice** \`risk:low\` \`depends:[]\`
|
|
276
|
+
> After this: done
|
|
277
|
+
|
|
278
|
+
## Boundary Map
|
|
279
|
+
_None_
|
|
280
|
+
`);
|
|
281
|
+
run("git add -A", repo);
|
|
282
|
+
run("git commit -m 'add milestone'", repo);
|
|
283
|
+
|
|
284
|
+
// Create orphaned worktree
|
|
285
|
+
mkdirSync(join(repo, ".gsd", "worktrees"), { recursive: true });
|
|
286
|
+
run("git worktree add -b milestone/M001 .gsd/worktrees/M001", repo);
|
|
287
|
+
|
|
288
|
+
// Detect
|
|
289
|
+
const detect = await runGSDDoctor(repo);
|
|
290
|
+
const orphanIssues = detect.issues.filter(i => i.code === "orphaned_auto_worktree");
|
|
291
|
+
assertTrue(orphanIssues.length > 0, "doctor detects orphaned worktree");
|
|
292
|
+
assertEq(orphanIssues[0]?.unitId, "M001", "orphaned worktree unitId is M001");
|
|
293
|
+
|
|
294
|
+
// Fix
|
|
295
|
+
const fixed = await runGSDDoctor(repo, { fix: true });
|
|
296
|
+
assertTrue(
|
|
297
|
+
fixed.fixesApplied.some(f => f.includes("removed orphaned worktree")),
|
|
298
|
+
"doctor fix removes orphaned worktree",
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Verify gone
|
|
302
|
+
const wtList = run("git worktree list", repo);
|
|
303
|
+
assertTrue(!wtList.includes("milestone/M001"), "worktree gone after doctor fix");
|
|
304
|
+
}
|
|
305
|
+
} finally {
|
|
306
|
+
process.chdir(savedCwd);
|
|
307
|
+
for (const d of tempDirs) {
|
|
308
|
+
try { rmSync(d, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
report();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
main();
|
|
@@ -120,15 +120,17 @@ export function worktreeBranchName(name: string): string {
|
|
|
120
120
|
/**
|
|
121
121
|
* Create a new git worktree under .gsd/worktrees/<name>/ with branch worktree/<name>.
|
|
122
122
|
* The branch is created from the current HEAD of the main branch.
|
|
123
|
+
*
|
|
124
|
+
* @param opts.branch — override the default `worktree/<name>` branch name
|
|
123
125
|
*/
|
|
124
|
-
export function createWorktree(basePath: string, name: string): WorktreeInfo {
|
|
126
|
+
export function createWorktree(basePath: string, name: string, opts: { branch?: string } = {}): WorktreeInfo {
|
|
125
127
|
// Validate name: alphanumeric, hyphens, underscores only
|
|
126
128
|
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
127
129
|
throw new Error(`Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
const wtPath = worktreePath(basePath, name);
|
|
131
|
-
const branch = worktreeBranchName(name);
|
|
133
|
+
const branch = opts.branch ?? worktreeBranchName(name);
|
|
132
134
|
|
|
133
135
|
if (existsSync(wtPath)) {
|
|
134
136
|
throw new Error(`Worktree "${name}" already exists at ${wtPath}`);
|
|
@@ -260,11 +262,11 @@ export function listWorktrees(basePath: string): WorktreeInfo[] {
|
|
|
260
262
|
export function removeWorktree(
|
|
261
263
|
basePath: string,
|
|
262
264
|
name: string,
|
|
263
|
-
opts: { deleteBranch?: boolean; force?: boolean } = {},
|
|
265
|
+
opts: { deleteBranch?: boolean; force?: boolean; branch?: string } = {},
|
|
264
266
|
): void {
|
|
265
267
|
const wtPath = worktreePath(basePath, name);
|
|
266
268
|
const resolvedWtPath = existsSync(wtPath) ? realpathSync(wtPath) : wtPath;
|
|
267
|
-
const branch = worktreeBranchName(name);
|
|
269
|
+
const branch = opts.branch ?? worktreeBranchName(name);
|
|
268
270
|
const { deleteBranch = true, force = false } = opts;
|
|
269
271
|
|
|
270
272
|
// If we're inside the worktree, move out first — git can't remove an in-use directory
|
|
@@ -105,16 +105,21 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): { getIsAnthropic:
|
|
|
105
105
|
const payload = event.payload as Record<string, unknown>;
|
|
106
106
|
if (!payload) return;
|
|
107
107
|
|
|
108
|
-
// Detect Anthropic provider.
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
108
|
+
// Detect Anthropic provider. Use the model object from the event (most
|
|
109
|
+
// reliable — comes directly from the resolved Model), then fall back to
|
|
110
|
+
// the model_select flag, then to the model name heuristic (last resort).
|
|
111
|
+
// The model name heuristic is needed for session restores where
|
|
112
|
+
// modelsAreEqual suppresses model_select AND the SDK doesn't pass model.
|
|
113
|
+
const eventModel = event.model as { provider: string } | undefined;
|
|
114
|
+
let isAnthropic: boolean;
|
|
115
|
+
if (eventModel?.provider) {
|
|
116
|
+
isAnthropic = eventModel.provider === "anthropic";
|
|
117
|
+
} else if (modelSelectFired) {
|
|
118
|
+
isAnthropic = isAnthropicProvider;
|
|
119
|
+
} else {
|
|
120
|
+
const modelName = typeof payload.model === "string" ? payload.model : "";
|
|
121
|
+
isAnthropic = modelName.startsWith("claude-");
|
|
122
|
+
}
|
|
118
123
|
if (!isAnthropic) return;
|
|
119
124
|
|
|
120
125
|
// Strip thinking blocks from history to avoid signature validation errors
|