@tuan_son.dinh/gsd 2.6.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 (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +453 -0
  3. package/dist/app-paths.d.ts +4 -0
  4. package/dist/app-paths.js +6 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +269 -0
  7. package/dist/loader.d.ts +2 -0
  8. package/dist/loader.js +70 -0
  9. package/dist/logo.d.ts +16 -0
  10. package/dist/logo.js +25 -0
  11. package/dist/onboarding.d.ts +43 -0
  12. package/dist/onboarding.js +418 -0
  13. package/dist/pi-migration.d.ts +14 -0
  14. package/dist/pi-migration.js +57 -0
  15. package/dist/resource-loader.d.ts +22 -0
  16. package/dist/resource-loader.js +60 -0
  17. package/dist/tool-bootstrap.d.ts +4 -0
  18. package/dist/tool-bootstrap.js +74 -0
  19. package/dist/wizard.d.ts +7 -0
  20. package/dist/wizard.js +25 -0
  21. package/package.json +60 -0
  22. package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +108 -0
  23. package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -0
  24. package/pkg/dist/modes/interactive/theme/dark.json +85 -0
  25. package/pkg/dist/modes/interactive/theme/light.json +84 -0
  26. package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
  27. package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
  28. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  29. package/pkg/dist/modes/interactive/theme/theme.js +949 -0
  30. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
  31. package/pkg/package.json +8 -0
  32. package/scripts/postinstall.js +127 -0
  33. package/src/resources/GSD-WORKFLOW.md +661 -0
  34. package/src/resources/agents/researcher.md +29 -0
  35. package/src/resources/agents/scout.md +56 -0
  36. package/src/resources/agents/worker.md +31 -0
  37. package/src/resources/extensions/ask-user-questions.ts +249 -0
  38. package/src/resources/extensions/bg-shell/index.ts +2808 -0
  39. package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
  40. package/src/resources/extensions/browser-tools/core.js +1057 -0
  41. package/src/resources/extensions/browser-tools/index.ts +4989 -0
  42. package/src/resources/extensions/browser-tools/package.json +20 -0
  43. package/src/resources/extensions/context7/index.ts +428 -0
  44. package/src/resources/extensions/context7/package.json +11 -0
  45. package/src/resources/extensions/get-secrets-from-user.ts +352 -0
  46. package/src/resources/extensions/google-search/index.ts +323 -0
  47. package/src/resources/extensions/google-search/package.json +9 -0
  48. package/src/resources/extensions/gsd/activity-log.ts +69 -0
  49. package/src/resources/extensions/gsd/auto.ts +2744 -0
  50. package/src/resources/extensions/gsd/commands.ts +313 -0
  51. package/src/resources/extensions/gsd/crash-recovery.ts +85 -0
  52. package/src/resources/extensions/gsd/dashboard-overlay.ts +521 -0
  53. package/src/resources/extensions/gsd/docs/preferences-reference.md +176 -0
  54. package/src/resources/extensions/gsd/doctor.ts +690 -0
  55. package/src/resources/extensions/gsd/files.ts +732 -0
  56. package/src/resources/extensions/gsd/git-service.ts +597 -0
  57. package/src/resources/extensions/gsd/gitignore.ts +168 -0
  58. package/src/resources/extensions/gsd/guided-flow.ts +817 -0
  59. package/src/resources/extensions/gsd/index.ts +558 -0
  60. package/src/resources/extensions/gsd/metrics.ts +374 -0
  61. package/src/resources/extensions/gsd/migrate/command.ts +218 -0
  62. package/src/resources/extensions/gsd/migrate/index.ts +42 -0
  63. package/src/resources/extensions/gsd/migrate/parser.ts +323 -0
  64. package/src/resources/extensions/gsd/migrate/parsers.ts +624 -0
  65. package/src/resources/extensions/gsd/migrate/preview.ts +48 -0
  66. package/src/resources/extensions/gsd/migrate/transformer.ts +346 -0
  67. package/src/resources/extensions/gsd/migrate/types.ts +370 -0
  68. package/src/resources/extensions/gsd/migrate/validator.ts +55 -0
  69. package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
  70. package/src/resources/extensions/gsd/observability-validator.ts +408 -0
  71. package/src/resources/extensions/gsd/package.json +11 -0
  72. package/src/resources/extensions/gsd/paths.ts +308 -0
  73. package/src/resources/extensions/gsd/preferences.ts +757 -0
  74. package/src/resources/extensions/gsd/prompt-loader.ts +50 -0
  75. package/src/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
  76. package/src/resources/extensions/gsd/prompts/complete-slice.md +29 -0
  77. package/src/resources/extensions/gsd/prompts/discuss.md +189 -0
  78. package/src/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
  79. package/src/resources/extensions/gsd/prompts/execute-task.md +61 -0
  80. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -0
  81. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -0
  82. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +59 -0
  83. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -0
  84. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +23 -0
  85. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -0
  86. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +11 -0
  87. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
  88. package/src/resources/extensions/gsd/prompts/plan-milestone.md +65 -0
  89. package/src/resources/extensions/gsd/prompts/plan-slice.md +51 -0
  90. package/src/resources/extensions/gsd/prompts/queue.md +85 -0
  91. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
  92. package/src/resources/extensions/gsd/prompts/replan-slice.md +39 -0
  93. package/src/resources/extensions/gsd/prompts/research-milestone.md +37 -0
  94. package/src/resources/extensions/gsd/prompts/research-slice.md +28 -0
  95. package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
  96. package/src/resources/extensions/gsd/prompts/run-uat.md +109 -0
  97. package/src/resources/extensions/gsd/prompts/system.md +187 -0
  98. package/src/resources/extensions/gsd/prompts/worktree-merge.md +123 -0
  99. package/src/resources/extensions/gsd/session-forensics.ts +487 -0
  100. package/src/resources/extensions/gsd/skill-discovery.ts +137 -0
  101. package/src/resources/extensions/gsd/state.ts +460 -0
  102. package/src/resources/extensions/gsd/templates/context.md +76 -0
  103. package/src/resources/extensions/gsd/templates/decisions.md +8 -0
  104. package/src/resources/extensions/gsd/templates/milestone-summary.md +73 -0
  105. package/src/resources/extensions/gsd/templates/plan.md +131 -0
  106. package/src/resources/extensions/gsd/templates/preferences.md +24 -0
  107. package/src/resources/extensions/gsd/templates/project.md +31 -0
  108. package/src/resources/extensions/gsd/templates/reassessment.md +28 -0
  109. package/src/resources/extensions/gsd/templates/requirements.md +81 -0
  110. package/src/resources/extensions/gsd/templates/research.md +46 -0
  111. package/src/resources/extensions/gsd/templates/roadmap.md +118 -0
  112. package/src/resources/extensions/gsd/templates/slice-context.md +58 -0
  113. package/src/resources/extensions/gsd/templates/slice-summary.md +99 -0
  114. package/src/resources/extensions/gsd/templates/state.md +19 -0
  115. package/src/resources/extensions/gsd/templates/task-plan.md +52 -0
  116. package/src/resources/extensions/gsd/templates/task-summary.md +57 -0
  117. package/src/resources/extensions/gsd/templates/uat.md +54 -0
  118. package/src/resources/extensions/gsd/tests/activity-log-prune.test.ts +327 -0
  119. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +56 -0
  120. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +53 -0
  121. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +225 -0
  122. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +160 -0
  123. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +341 -0
  124. package/src/resources/extensions/gsd/tests/derive-state.test.ts +689 -0
  125. package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
  126. package/src/resources/extensions/gsd/tests/doctor.test.ts +505 -0
  127. package/src/resources/extensions/gsd/tests/git-service.test.ts +1313 -0
  128. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +308 -0
  129. package/src/resources/extensions/gsd/tests/metrics-io.test.ts +201 -0
  130. package/src/resources/extensions/gsd/tests/metrics.test.ts +217 -0
  131. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +390 -0
  132. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +786 -0
  133. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +657 -0
  134. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +443 -0
  135. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +318 -0
  136. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +420 -0
  137. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +309 -0
  138. package/src/resources/extensions/gsd/tests/parsers.test.ts +1351 -0
  139. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +163 -0
  140. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +386 -0
  141. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +171 -0
  142. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
  143. package/src/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
  144. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +521 -0
  145. package/src/resources/extensions/gsd/tests/requirements.test.ts +125 -0
  146. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +34 -0
  147. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +11 -0
  148. package/src/resources/extensions/gsd/tests/run-uat.test.ts +348 -0
  149. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +247 -0
  150. package/src/resources/extensions/gsd/tests/workflow-config.test.mjs +53 -0
  151. package/src/resources/extensions/gsd/tests/workspace-index.test.ts +94 -0
  152. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +253 -0
  153. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
  154. package/src/resources/extensions/gsd/tests/worktree.test.ts +264 -0
  155. package/src/resources/extensions/gsd/types.ts +159 -0
  156. package/src/resources/extensions/gsd/unit-runtime.ts +184 -0
  157. package/src/resources/extensions/gsd/workspace-index.ts +203 -0
  158. package/src/resources/extensions/gsd/worktree-command.ts +845 -0
  159. package/src/resources/extensions/gsd/worktree-manager.ts +392 -0
  160. package/src/resources/extensions/gsd/worktree.ts +183 -0
  161. package/src/resources/extensions/mac-tools/index.ts +852 -0
  162. package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
  163. package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
  164. package/src/resources/extensions/mcporter/index.ts +429 -0
  165. package/src/resources/extensions/remote-questions/config.ts +81 -0
  166. package/src/resources/extensions/remote-questions/discord-adapter.ts +128 -0
  167. package/src/resources/extensions/remote-questions/format.ts +163 -0
  168. package/src/resources/extensions/remote-questions/manager.ts +192 -0
  169. package/src/resources/extensions/remote-questions/remote-command.ts +307 -0
  170. package/src/resources/extensions/remote-questions/slack-adapter.ts +92 -0
  171. package/src/resources/extensions/remote-questions/status.ts +31 -0
  172. package/src/resources/extensions/remote-questions/store.ts +77 -0
  173. package/src/resources/extensions/remote-questions/types.ts +75 -0
  174. package/src/resources/extensions/search-the-web/cache.ts +78 -0
  175. package/src/resources/extensions/search-the-web/command-search-provider.ts +95 -0
  176. package/src/resources/extensions/search-the-web/format.ts +258 -0
  177. package/src/resources/extensions/search-the-web/http.ts +238 -0
  178. package/src/resources/extensions/search-the-web/index.ts +65 -0
  179. package/src/resources/extensions/search-the-web/native-search.ts +157 -0
  180. package/src/resources/extensions/search-the-web/provider.ts +118 -0
  181. package/src/resources/extensions/search-the-web/tavily.ts +116 -0
  182. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
  183. package/src/resources/extensions/search-the-web/tool-llm-context.ts +561 -0
  184. package/src/resources/extensions/search-the-web/tool-search.ts +576 -0
  185. package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
  186. package/src/resources/extensions/shared/confirm-ui.ts +126 -0
  187. package/src/resources/extensions/shared/interview-ui.ts +613 -0
  188. package/src/resources/extensions/shared/next-action-ui.ts +197 -0
  189. package/src/resources/extensions/shared/progress-widget.ts +282 -0
  190. package/src/resources/extensions/shared/terminal.ts +23 -0
  191. package/src/resources/extensions/shared/thinking-widget.ts +107 -0
  192. package/src/resources/extensions/shared/ui.ts +400 -0
  193. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  194. package/src/resources/extensions/slash-commands/audit.ts +88 -0
  195. package/src/resources/extensions/slash-commands/clear.ts +10 -0
  196. package/src/resources/extensions/slash-commands/create-extension.ts +297 -0
  197. package/src/resources/extensions/slash-commands/create-slash-command.ts +234 -0
  198. package/src/resources/extensions/slash-commands/index.ts +12 -0
  199. package/src/resources/extensions/subagent/agents.ts +126 -0
  200. package/src/resources/extensions/subagent/index.ts +1020 -0
  201. package/src/resources/extensions/voice/index.ts +195 -0
  202. package/src/resources/extensions/voice/speech-recognizer.swift +154 -0
  203. package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
  204. package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
  205. package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
  206. package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
  207. package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
  208. package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
  209. package/src/resources/skills/frontend-design/SKILL.md +45 -0
  210. package/src/resources/skills/swiftui/SKILL.md +208 -0
  211. package/src/resources/skills/swiftui/references/animations.md +921 -0
  212. package/src/resources/skills/swiftui/references/architecture.md +1561 -0
  213. package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
  214. package/src/resources/skills/swiftui/references/navigation.md +1492 -0
  215. package/src/resources/skills/swiftui/references/networking-async.md +214 -0
  216. package/src/resources/skills/swiftui/references/performance.md +1706 -0
  217. package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
  218. package/src/resources/skills/swiftui/references/state-management.md +1443 -0
  219. package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
  220. package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
  221. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
  222. package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
  223. package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
  224. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
  225. package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
  226. package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
  227. package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
@@ -0,0 +1,845 @@
1
+ /**
2
+ * GSD Worktree Command — /worktree
3
+ *
4
+ * Create, list, merge, and remove git worktrees under .gsd/worktrees/.
5
+ *
6
+ * Usage:
7
+ * /worktree <name> — create a new worktree
8
+ * /worktree list — list existing worktrees
9
+ * /worktree merge [name] [target] — start LLM-guided merge (auto-detects when inside a worktree)
10
+ * /worktree remove <name> — remove a worktree and its branch
11
+ */
12
+
13
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
14
+ import { loadPrompt } from "./prompt-loader.js";
15
+ import { autoCommitCurrentBranch } from "./worktree.js";
16
+ import { showConfirm } from "../shared/confirm-ui.js";
17
+ import { gsdRoot, milestonesDir } from "./paths.js";
18
+ import {
19
+ createWorktree,
20
+ listWorktrees,
21
+ removeWorktree,
22
+ mergeWorktreeToMain,
23
+ diffWorktreeAll,
24
+ diffWorktreeNumstat,
25
+ getMainBranch,
26
+ getWorktreeGSDDiff,
27
+ getWorktreeCodeDiff,
28
+ getWorktreeLog,
29
+ worktreeBranchName,
30
+ worktreePath,
31
+ } from "./worktree-manager.js";
32
+ import { inferCommitType } from "./git-service.js";
33
+ import type { FileLineStat } from "./worktree-manager.js";
34
+ import { execSync } from "node:child_process";
35
+ import { existsSync, realpathSync, readFileSync, readdirSync, rmSync, unlinkSync, utimesSync } from "node:fs";
36
+ import { join, resolve, sep } from "node:path";
37
+
38
+ /**
39
+ * Tracks the original project root so we can switch back.
40
+ * Set when we first chdir into a worktree, cleared on return.
41
+ */
42
+ let originalCwd: string | null = null;
43
+
44
+ /** Get the original project root if currently in a worktree, or null. */
45
+ export function getWorktreeOriginalCwd(): string | null {
46
+ return originalCwd;
47
+ }
48
+
49
+ /**
50
+ * Resolve the git HEAD file path for a given directory.
51
+ * Handles both normal repos (.git is a directory) and worktrees (.git is a file).
52
+ */
53
+ function resolveGitHeadPath(dir: string): string | null {
54
+ const gitPath = join(dir, ".git");
55
+ if (!existsSync(gitPath)) return null;
56
+
57
+ try {
58
+ const content = readFileSync(gitPath, "utf8").trim();
59
+ if (content.startsWith("gitdir: ")) {
60
+ // Worktree — .git is a file pointing to the real gitdir
61
+ const gitDir = resolve(dir, content.slice(8));
62
+ const headPath = join(gitDir, "HEAD");
63
+ return existsSync(headPath) ? headPath : null;
64
+ }
65
+ // Normal repo — .git is a directory
66
+ const headPath = join(dir, ".git", "HEAD");
67
+ return existsSync(headPath) ? headPath : null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Nudge pi's FooterDataProvider to re-read the git branch.
75
+ *
76
+ * The footer caches the branch and watches a single .git dir for changes.
77
+ * After process.chdir() into a worktree (or back), the watcher is stale —
78
+ * it's still watching the old git dir. We touch HEAD in both the old and
79
+ * new git dirs to ensure the watcher fires regardless of which one it's
80
+ * monitoring. This clears cachedBranch; the next getGitBranch() call uses
81
+ * the new process.cwd() and picks up the correct branch.
82
+ */
83
+ function nudgeGitBranchCache(previousCwd: string): void {
84
+ const now = new Date();
85
+ for (const dir of [previousCwd, process.cwd()]) {
86
+ try {
87
+ const headPath = resolveGitHeadPath(dir);
88
+ if (headPath) utimesSync(headPath, now, now);
89
+ } catch {
90
+ // Best-effort — branch display may be stale
91
+ }
92
+ }
93
+ }
94
+
95
+ /** Get the name of the active worktree, or null if not in one. */
96
+ export function getActiveWorktreeName(): string | null {
97
+ if (!originalCwd) return null;
98
+ const cwd = process.cwd();
99
+ const wtDir = join(originalCwd, ".gsd", "worktrees");
100
+ if (!cwd.startsWith(wtDir)) return null;
101
+ const rel = cwd.slice(wtDir.length + 1);
102
+ const name = rel.split("/")[0] ?? rel.split("\\")[0];
103
+ return name || null;
104
+ }
105
+
106
+ // ─── Shared completions and handler (used by both /worktree and /wt) ────────
107
+
108
+ function worktreeCompletions(prefix: string) {
109
+ const parts = prefix.trim().split(/\s+/);
110
+ const subcommands = ["list", "merge", "remove", "switch", "create", "return"];
111
+
112
+ if (parts.length <= 1) {
113
+ const partial = parts[0] ?? "";
114
+ const cmdCompletions = subcommands
115
+ .filter(cmd => cmd.startsWith(partial))
116
+ .map(cmd => ({ value: cmd, label: cmd }));
117
+ try {
118
+ const mainBase = getWorktreeOriginalCwd() ?? process.cwd();
119
+ const existing = listWorktrees(mainBase);
120
+ const nameCompletions = existing
121
+ .filter(wt => wt.name.startsWith(partial))
122
+ .map(wt => ({ value: wt.name, label: wt.name }));
123
+ return [...cmdCompletions, ...nameCompletions];
124
+ } catch {
125
+ return cmdCompletions;
126
+ }
127
+ }
128
+
129
+ if ((parts[0] === "merge" || parts[0] === "remove" || parts[0] === "switch" || parts[0] === "create") && parts.length <= 2) {
130
+ const namePrefix = parts[1] ?? "";
131
+ try {
132
+ const mainBase = getWorktreeOriginalCwd() ?? process.cwd();
133
+ const existing = listWorktrees(mainBase);
134
+ const nameCompletions = existing
135
+ .filter(wt => wt.name.startsWith(namePrefix))
136
+ .map(wt => ({ value: `${parts[0]} ${wt.name}`, label: wt.name }));
137
+
138
+ // Add "all" option for remove
139
+ if (parts[0] === "remove" && "all".startsWith(namePrefix)) {
140
+ nameCompletions.push({ value: "remove all", label: "all" });
141
+ }
142
+
143
+ return nameCompletions;
144
+ } catch {
145
+ return [];
146
+ }
147
+ }
148
+
149
+ return [];
150
+ }
151
+
152
+ async function worktreeHandler(
153
+ args: string,
154
+ ctx: ExtensionCommandContext,
155
+ pi: ExtensionAPI,
156
+ alias: string,
157
+ ): Promise<void> {
158
+ const trimmed = (typeof args === "string" ? args : "").trim();
159
+ const basePath = process.cwd();
160
+
161
+ if (trimmed === "") {
162
+ ctx.ui.notify(
163
+ [
164
+ "Usage:",
165
+ ` /${alias} <name> — create and switch into a new worktree`,
166
+ ` /${alias} switch <name> — switch into an existing worktree`,
167
+ ` /${alias} return — switch back to the main project tree`,
168
+ ` /${alias} list — list all worktrees`,
169
+ ` /${alias} merge [name] [target] — merge worktree into target branch (auto-detects when inside a worktree)`,
170
+ ` /${alias} remove <name|all> — remove a worktree (or all) and its branch`,
171
+ ].join("\n"),
172
+ "info",
173
+ );
174
+ return;
175
+ }
176
+
177
+ if (trimmed === "list") {
178
+ await handleList(basePath, ctx);
179
+ return;
180
+ }
181
+
182
+ if (trimmed === "return") {
183
+ await handleReturn(ctx);
184
+ return;
185
+ }
186
+
187
+ if (trimmed.startsWith("switch ") || trimmed.startsWith("create ")) {
188
+ const name = trimmed.replace(/^(?:switch|create)\s+/, "").trim();
189
+ if (!name) {
190
+ ctx.ui.notify(`Usage: /${alias} ${trimmed.split(" ")[0]} <name>`, "warning");
191
+ return;
192
+ }
193
+ // create and switch both do the same thing: switch if exists, create if not
194
+ const mainBase = originalCwd ?? basePath;
195
+ const existing = listWorktrees(mainBase);
196
+ if (existing.some(wt => wt.name === name)) {
197
+ await handleSwitch(basePath, name, ctx);
198
+ } else {
199
+ await handleCreate(basePath, name, ctx);
200
+ }
201
+ return;
202
+ }
203
+
204
+ if (trimmed === "merge" || trimmed.startsWith("merge ")) {
205
+ const mergeArgs = trimmed.replace(/^merge\s*/, "").trim().split(/\s+/).filter(Boolean);
206
+ const mainBase = originalCwd ?? basePath;
207
+ const activeWt = getActiveWorktreeName();
208
+
209
+ if (mergeArgs.length === 0) {
210
+ // Bare "/worktree merge" — only valid when inside a worktree
211
+ if (!activeWt) {
212
+ ctx.ui.notify(`Usage: /${alias} merge <name> [target]`, "warning");
213
+ return;
214
+ }
215
+ await handleMerge(mainBase, activeWt, ctx, pi, undefined);
216
+ return;
217
+ }
218
+
219
+ const name = mergeArgs[0]!;
220
+ const targetBranch = mergeArgs[1];
221
+
222
+ // Check if 'name' is an actual worktree
223
+ const worktrees = listWorktrees(mainBase);
224
+ const isWorktree = worktrees.some(w => w.name === name);
225
+
226
+ if (isWorktree) {
227
+ await handleMerge(mainBase, name, ctx, pi, targetBranch);
228
+ } else if (activeWt) {
229
+ // Not a worktree name — user is in a worktree and gave the target branch
230
+ // e.g. "/worktree merge main" while inside worktree "new"
231
+ await handleMerge(mainBase, activeWt, ctx, pi, name);
232
+ } else {
233
+ ctx.ui.notify(`Worktree "${name}" not found. Run /${alias} list to see available worktrees.`, "warning");
234
+ }
235
+ return;
236
+ }
237
+
238
+ if (trimmed === "remove" || trimmed.startsWith("remove ")) {
239
+ const name = trimmed.replace(/^remove\s*/, "").trim();
240
+ const mainBase = originalCwd ?? basePath;
241
+
242
+ if (name === "all") {
243
+ await handleRemoveAll(mainBase, ctx);
244
+ return;
245
+ }
246
+
247
+ if (!name) {
248
+ ctx.ui.notify(`Usage: /${alias} remove <name|all>`, "warning");
249
+ return;
250
+ }
251
+
252
+ await handleRemove(mainBase, name, ctx);
253
+ return;
254
+ }
255
+
256
+ const RESERVED = ["list", "return", "switch", "create", "merge", "remove"];
257
+ if (RESERVED.includes(trimmed)) {
258
+ ctx.ui.notify(`Usage: /${alias} ${trimmed}${trimmed === "list" || trimmed === "return" ? "" : " <name>"}`, "warning");
259
+ return;
260
+ }
261
+
262
+ const mainBase = originalCwd ?? basePath;
263
+ const nameOnly = trimmed.split(/\s+/)[0]!;
264
+ if (trimmed !== nameOnly) {
265
+ ctx.ui.notify(`Unknown command. Did you mean /${alias} switch ${nameOnly}?`, "warning");
266
+ return;
267
+ }
268
+
269
+ const existing = listWorktrees(mainBase);
270
+ if (existing.some(wt => wt.name === nameOnly)) {
271
+ await handleSwitch(basePath, nameOnly, ctx);
272
+ } else {
273
+ await handleCreate(basePath, nameOnly, ctx);
274
+ }
275
+ }
276
+
277
+ export function registerWorktreeCommand(pi: ExtensionAPI): void {
278
+ // Restore worktree state after /reload.
279
+ // The module-level originalCwd resets to null when extensions are re-loaded,
280
+ // but process.cwd() is still inside the worktree. Detect this and recover.
281
+ if (!originalCwd) {
282
+ const cwd = process.cwd();
283
+ const marker = `${sep}.gsd${sep}worktrees${sep}`;
284
+ const markerIdx = cwd.indexOf(marker);
285
+ if (markerIdx !== -1) {
286
+ originalCwd = cwd.slice(0, markerIdx);
287
+ }
288
+ }
289
+
290
+ pi.registerCommand("worktree", {
291
+ description: "Git worktrees (also /wt): /worktree <name> | list | merge | remove",
292
+ getArgumentCompletions: worktreeCompletions,
293
+
294
+ async handler(args: string, ctx: ExtensionCommandContext) {
295
+ await worktreeHandler(args, ctx, pi, "worktree");
296
+ },
297
+ });
298
+
299
+ // /wt alias — same handler, same completions
300
+ pi.registerCommand("wt", {
301
+ description: "Alias for /worktree",
302
+ getArgumentCompletions: worktreeCompletions,
303
+ async handler(args: string, ctx: ExtensionCommandContext) {
304
+ await worktreeHandler(args, ctx, pi, "wt");
305
+ },
306
+ });
307
+ }
308
+
309
+ // ─── Handlers ──────────────────────────────────────────────────────────────
310
+
311
+ /**
312
+ * Check if the worktree has existing GSD milestones that would
313
+ * cause auto-mode to continue previous work instead of starting fresh.
314
+ */
315
+ function hasExistingMilestones(wtPath: string): boolean {
316
+ const mDir = milestonesDir(wtPath);
317
+ if (!existsSync(mDir)) return false;
318
+ try {
319
+ const entries = readdirSync(mDir, { withFileTypes: true })
320
+ .filter(d => d.isDirectory() && /^M\d+/.test(d.name));
321
+ return entries.length > 0;
322
+ } catch {
323
+ return false;
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Clear GSD planning artifacts so auto-mode starts fresh with the discuss flow.
329
+ * Keeps the .gsd/ directory structure intact but removes milestones and root planning files.
330
+ */
331
+ function clearGSDPlans(wtPath: string): void {
332
+ const mDir = milestonesDir(wtPath);
333
+ if (existsSync(mDir)) {
334
+ rmSync(mDir, { recursive: true, force: true });
335
+ }
336
+
337
+ // Remove root planning files — PROJECT.md, DECISIONS.md, QUEUE.md, REQUIREMENTS.md
338
+ // Keep STATE.md (gitignored, will be rebuilt) and other runtime files
339
+ const root = gsdRoot(wtPath);
340
+ const planningFiles = ["PROJECT.md", "DECISIONS.md", "QUEUE.md", "REQUIREMENTS.md"];
341
+ for (const file of planningFiles) {
342
+ const filePath = join(root, file);
343
+ if (existsSync(filePath)) {
344
+ unlinkSync(filePath);
345
+ }
346
+ }
347
+ }
348
+
349
+ async function handleCreate(
350
+ basePath: string,
351
+ name: string,
352
+ ctx: ExtensionCommandContext,
353
+ ): Promise<void> {
354
+ try {
355
+ // Auto-commit dirty files before leaving current workspace (must happen
356
+ // before createWorktree so the new worktree forks from committed HEAD)
357
+ const commitMsg = autoCommitCurrentBranch(basePath, "worktree-switch", name);
358
+
359
+ // Create from the main tree, not from inside another worktree
360
+ const mainBase = originalCwd ?? basePath;
361
+ const info = createWorktree(mainBase, name);
362
+
363
+ // Track original cwd before switching
364
+ if (!originalCwd) originalCwd = basePath;
365
+
366
+ const prevCwd = process.cwd();
367
+ process.chdir(info.path);
368
+ nudgeGitBranchCache(prevCwd);
369
+
370
+ // If the worktree inherited existing milestones, ask whether to keep or clear them
371
+ let clearedPlans = false;
372
+ if (hasExistingMilestones(info.path)) {
373
+ // confirmLabel = Continue (safe default, on the left / first)
374
+ // declineLabel = Start fresh (destructive, on the right)
375
+ const keepExisting = await showConfirm(ctx, {
376
+ title: "Worktree Setup",
377
+ message: [
378
+ `This worktree inherited existing GSD milestones from the main branch.`,
379
+ ``,
380
+ ` Continue — keep milestones and pick up where main left off`,
381
+ ` Start fresh — clear milestones so /gsd auto starts a new project`,
382
+ ].join("\n"),
383
+ confirmLabel: "Continue",
384
+ declineLabel: "Start fresh",
385
+ });
386
+ if (!keepExisting) {
387
+ clearGSDPlans(info.path);
388
+ clearedPlans = true;
389
+ }
390
+ }
391
+
392
+ const commitNote = commitMsg
393
+ ? ` ${CLR.muted("Auto-committed on previous branch before switching.")}`
394
+ : "";
395
+ const freshNote = clearedPlans
396
+ ? ` ${CLR.ok("✓")} Cleared milestones — ${CLR.hint("/gsd auto")} will start fresh.`
397
+ : "";
398
+ ctx.ui.notify(
399
+ [
400
+ `${CLR.ok("✓")} Worktree ${CLR.name(name)} created and activated.`,
401
+ "",
402
+ ` ${CLR.label("path")} ${CLR.path(info.path)}`,
403
+ ` ${CLR.label("branch")} ${CLR.branch(info.branch)}`,
404
+ commitNote,
405
+ freshNote,
406
+ "",
407
+ ` ${CLR.hint(`/worktree merge ${name}`)} ${CLR.muted("merge back when done")}`,
408
+ ` ${CLR.hint("/worktree return")}${" ".repeat(Math.max(1, name.length - 2))} ${CLR.muted("switch back to main tree")}`,
409
+ ].filter(Boolean).join("\n"),
410
+ "info",
411
+ );
412
+ } catch (error) {
413
+ const msg = error instanceof Error ? error.message : String(error);
414
+ ctx.ui.notify(`Failed to create worktree: ${msg}`, "error");
415
+ }
416
+ }
417
+
418
+ async function handleSwitch(
419
+ basePath: string,
420
+ name: string,
421
+ ctx: ExtensionCommandContext,
422
+ ): Promise<void> {
423
+ try {
424
+ const mainBase = originalCwd ?? basePath;
425
+ const wtPath = worktreePath(mainBase, name);
426
+
427
+ if (!existsSync(wtPath)) {
428
+ ctx.ui.notify(
429
+ `Worktree "${name}" not found. Run /worktree list to see available worktrees.`,
430
+ "warning",
431
+ );
432
+ return;
433
+ }
434
+
435
+ // Auto-commit dirty files before leaving current workspace
436
+ const commitMsg = autoCommitCurrentBranch(basePath, "worktree-switch", name);
437
+
438
+ // Track original cwd before switching
439
+ if (!originalCwd) originalCwd = basePath;
440
+
441
+ const prevCwd = process.cwd();
442
+ process.chdir(wtPath);
443
+ nudgeGitBranchCache(prevCwd);
444
+
445
+ const commitNote = commitMsg
446
+ ? ` ${CLR.muted("Auto-committed on previous branch before switching.")}`
447
+ : "";
448
+ ctx.ui.notify(
449
+ [
450
+ `${CLR.ok("✓")} Switched to worktree ${CLR.name(name)}.`,
451
+ "",
452
+ ` ${CLR.label("path")} ${CLR.path(wtPath)}`,
453
+ ` ${CLR.label("branch")} ${CLR.branch(worktreeBranchName(name))}`,
454
+ commitNote,
455
+ "",
456
+ ` ${CLR.hint("/worktree return")} ${CLR.muted("switch back to main tree")}`,
457
+ ].filter(Boolean).join("\n"),
458
+ "info",
459
+ );
460
+ } catch (error) {
461
+ const msg = error instanceof Error ? error.message : String(error);
462
+ ctx.ui.notify(`Failed to switch to worktree: ${msg}`, "error");
463
+ }
464
+ }
465
+
466
+ async function handleReturn(ctx: ExtensionCommandContext): Promise<void> {
467
+ if (!originalCwd) {
468
+ ctx.ui.notify("Already in the main project tree.", "info");
469
+ return;
470
+ }
471
+
472
+ // Auto-commit dirty files before leaving worktree
473
+ const commitMsg = autoCommitCurrentBranch(process.cwd(), "worktree-return", "worktree");
474
+
475
+ const returnTo = originalCwd;
476
+ originalCwd = null;
477
+
478
+ const prevCwd = process.cwd();
479
+ process.chdir(returnTo);
480
+ nudgeGitBranchCache(prevCwd);
481
+
482
+ const commitNote = commitMsg
483
+ ? ` ${CLR.muted("Auto-committed on worktree branch before returning.")}`
484
+ : "";
485
+ ctx.ui.notify(
486
+ [
487
+ `${CLR.ok("✓")} Returned to main project tree.`,
488
+ "",
489
+ ` ${CLR.label("path")} ${CLR.path(returnTo)}`,
490
+ commitNote,
491
+ ].filter(Boolean).join("\n"),
492
+ "info",
493
+ );
494
+ }
495
+
496
+ // ─── ANSI styling ─────────────────────────────────────────────────────────
497
+ // Consistent palette for all worktree command output.
498
+
499
+ const BOLD = "\x1b[1m";
500
+ const DIM = "\x1b[2m";
501
+ const RESET = "\x1b[0m";
502
+ const CYAN = "\x1b[36m";
503
+ const GREEN = "\x1b[32m";
504
+ const RED = "\x1b[31m";
505
+ const YELLOW = "\x1b[33m";
506
+ const WHITE = "\x1b[37m";
507
+ const MAGENTA = "\x1b[35m";
508
+
509
+ // Semantic aliases for consistent use across all handlers
510
+ const CLR = {
511
+ /** Worktree names and primary emphasis */
512
+ name: (s: string) => `${BOLD}${CYAN}${s}${RESET}`,
513
+ /** Active worktree name */
514
+ nameActive: (s: string) => `${BOLD}${GREEN}${s}${RESET}`,
515
+ /** Branch names */
516
+ branch: (s: string) => `${MAGENTA}${s}${RESET}`,
517
+ /** File paths */
518
+ path: (s: string) => `${DIM}${s}${RESET}`,
519
+ /** Labels (key in key:value pairs) */
520
+ label: (s: string) => `${WHITE}${s}${RESET}`,
521
+ /** Hints and commands the user can run */
522
+ hint: (s: string) => `${DIM}${CYAN}${s}${RESET}`,
523
+ /** Success messages and checks */
524
+ ok: (s: string) => `${GREEN}${s}${RESET}`,
525
+ /** Warning badges */
526
+ warn: (s: string) => `${YELLOW}${s}${RESET}`,
527
+ /** Section headers */
528
+ header: (s: string) => `${BOLD}${WHITE}${s}${RESET}`,
529
+ /** Muted secondary info */
530
+ muted: (s: string) => `${DIM}${s}${RESET}`,
531
+ } as const;
532
+
533
+ async function handleList(
534
+ basePath: string,
535
+ ctx: ExtensionCommandContext,
536
+ ): Promise<void> {
537
+ try {
538
+ const mainBase = originalCwd ?? basePath;
539
+ const worktrees = listWorktrees(mainBase);
540
+
541
+ if (worktrees.length === 0) {
542
+ ctx.ui.notify("No GSD worktrees found. Create one with /worktree <name>.", "info");
543
+ return;
544
+ }
545
+
546
+ const cwd = process.cwd();
547
+ const lines = [CLR.header("GSD Worktrees"), ""];
548
+ for (const wt of worktrees) {
549
+ const isCurrent = cwd === wt.path
550
+ || (existsSync(cwd) && existsSync(wt.path)
551
+ && realpathSync(cwd) === realpathSync(wt.path));
552
+
553
+ const styledName = isCurrent ? CLR.nameActive(wt.name) : CLR.name(wt.name);
554
+ const badge = isCurrent
555
+ ? ` ${CLR.ok("● active")}`
556
+ : !wt.exists
557
+ ? ` ${CLR.warn("✗ missing")}`
558
+ : "";
559
+ lines.push(` ${styledName}${badge}`);
560
+ lines.push(` ${CLR.label("branch")} ${CLR.branch(wt.branch)}`);
561
+ lines.push(` ${CLR.label("path")} ${CLR.path(wt.path)}`);
562
+ lines.push("");
563
+ }
564
+
565
+ if (originalCwd) {
566
+ lines.push(` ${CLR.label("main tree")} ${CLR.path(originalCwd)}`);
567
+ }
568
+
569
+ ctx.ui.notify(lines.join("\n"), "info");
570
+ } catch (error) {
571
+ const msg = error instanceof Error ? error.message : String(error);
572
+ ctx.ui.notify(`Failed to list worktrees: ${msg}`, "error");
573
+ }
574
+ }
575
+
576
+ async function handleMerge(
577
+ basePath: string,
578
+ name: string,
579
+ ctx: ExtensionCommandContext,
580
+ pi: ExtensionAPI,
581
+ targetBranch?: string,
582
+ ): Promise<void> {
583
+ try {
584
+ const branch = worktreeBranchName(name);
585
+ const mainBranch = targetBranch ?? getMainBranch(basePath);
586
+
587
+ // Validate the worktree/branch exists
588
+ const worktrees = listWorktrees(basePath);
589
+ const wt = worktrees.find(w => w.name === name);
590
+ if (!wt) {
591
+ ctx.ui.notify(`Worktree "${name}" not found. Run /worktree list to see available worktrees.`, "warning");
592
+ return;
593
+ }
594
+
595
+ // Gather merge context — full repo diff, not just .gsd/
596
+ const diffSummary = diffWorktreeAll(basePath, name);
597
+ const numstat = diffWorktreeNumstat(basePath, name);
598
+ const gsdDiff = getWorktreeGSDDiff(basePath, name);
599
+ const codeDiff = getWorktreeCodeDiff(basePath, name);
600
+ const commitLog = getWorktreeLog(basePath, name);
601
+
602
+ const totalChanges = diffSummary.added.length + diffSummary.modified.length + diffSummary.removed.length;
603
+ if (totalChanges === 0 && !commitLog.trim()) {
604
+ ctx.ui.notify(`Worktree ${CLR.name(name)} has no changes to merge.`, "info");
605
+ return;
606
+ }
607
+
608
+ // Build a map of file → line stats for the preview
609
+ const statMap = new Map<string, FileLineStat>();
610
+ for (const s of numstat) statMap.set(s.file, s);
611
+
612
+ // Compute totals
613
+ let totalAdded = 0;
614
+ let totalRemoved = 0;
615
+ for (const s of numstat) { totalAdded += s.added; totalRemoved += s.removed; }
616
+
617
+ // Split files into code vs GSD for the preview
618
+ const isGSD = (f: string) => f.startsWith(".gsd/");
619
+ const codeChanges = diffSummary.added.filter(f => !isGSD(f)).length
620
+ + diffSummary.modified.filter(f => !isGSD(f)).length
621
+ + diffSummary.removed.filter(f => !isGSD(f)).length;
622
+ const gsdChanges = diffSummary.added.filter(isGSD).length
623
+ + diffSummary.modified.filter(isGSD).length
624
+ + diffSummary.removed.filter(isGSD).length;
625
+
626
+ // Format a file line with +/- stats
627
+ const formatFileLine = (prefix: string, file: string): string => {
628
+ const s = statMap.get(file);
629
+ const stat = s ? ` ${CLR.ok(`+${s.added}`)} ${RED}-${s.removed}${RESET}` : "";
630
+ return ` ${prefix} ${file}${stat}`;
631
+ };
632
+
633
+ // Preview confirmation before merge dispatch
634
+ const previewLines = [
635
+ `Merge ${CLR.name(name)} → ${CLR.branch(mainBranch)}`,
636
+ "",
637
+ ` ${totalChanges} file${totalChanges === 1 ? "" : "s"} changed, ${CLR.ok(`+${totalAdded}`)} ${RED}-${totalRemoved}${RESET} lines ${CLR.muted(`(${codeChanges} code, ${gsdChanges} GSD)`)}`,
638
+ ];
639
+
640
+ const appendFileList = (label: string, files: string[], prefix: string, limit = 10) => {
641
+ if (files.length === 0) return;
642
+ previewLines.push("", ` ${label}:`);
643
+ for (const f of files.slice(0, limit)) previewLines.push(formatFileLine(prefix, f));
644
+ if (files.length > limit) previewLines.push(` … and ${files.length - limit} more`);
645
+ };
646
+
647
+ appendFileList("Added", diffSummary.added, "+");
648
+ appendFileList("Modified", diffSummary.modified, "~");
649
+ appendFileList("Removed", diffSummary.removed, "-");
650
+
651
+ const confirmed = await showConfirm(ctx, {
652
+ title: "Worktree Merge",
653
+ message: previewLines.join("\n"),
654
+ confirmLabel: "Merge",
655
+ declineLabel: "Cancel",
656
+ });
657
+ if (!confirmed) {
658
+ ctx.ui.notify("Merge cancelled.", "info");
659
+ return;
660
+ }
661
+
662
+ // Switch to the main tree before merging.
663
+ // Must be on the main branch to run git merge --squash.
664
+ if (originalCwd) {
665
+ const prevCwd = process.cwd();
666
+ process.chdir(basePath);
667
+ nudgeGitBranchCache(prevCwd);
668
+ originalCwd = null;
669
+ }
670
+
671
+ // --- Deterministic merge path (preferred) ---
672
+ // Try a direct squash-merge first. Only fall back to LLM on conflict.
673
+ const commitType = inferCommitType(name);
674
+ const commitMessage = `${commitType}(${name}): merge worktree ${name}`;
675
+ try {
676
+ mergeWorktreeToMain(basePath, name, commitMessage);
677
+ ctx.ui.notify(
678
+ [
679
+ `${CLR.ok("✓")} Merged ${CLR.name(name)} → ${CLR.branch(mainBranch)} ${CLR.muted("(deterministic squash)")}`,
680
+ "",
681
+ ` ${totalChanges} file${totalChanges === 1 ? "" : "s"} changed, ${CLR.ok(`+${totalAdded}`)} ${RED}-${totalRemoved}${RESET} lines`,
682
+ ` ${CLR.muted("commit:")} ${commitMessage}`,
683
+ ].join("\n"),
684
+ "info",
685
+ );
686
+ return;
687
+ } catch (mergeErr) {
688
+ const mergeMsg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
689
+ const isConflict = /conflict/i.test(mergeMsg);
690
+
691
+ if (isConflict) {
692
+ // Abort the failed merge so the working tree is clean for LLM retry
693
+ try {
694
+ execSync("git merge --abort", { cwd: basePath, stdio: "pipe" });
695
+ } catch { /* already clean */ }
696
+
697
+ ctx.ui.notify(
698
+ `${CLR.muted("Deterministic merge hit conflicts — falling back to LLM-guided merge.")}`,
699
+ "warning",
700
+ );
701
+ // Fall through to LLM dispatch below
702
+ } else {
703
+ // Non-conflict error — surface it directly, don't fall back
704
+ ctx.ui.notify(`Failed to merge: ${mergeMsg}`, "error");
705
+ return;
706
+ }
707
+ }
708
+
709
+ // --- LLM fallback path (conflict resolution) ---
710
+ // Format file lists for the prompt
711
+ const formatFiles = (files: string[]) =>
712
+ files.length > 0 ? files.map(f => `- \`${f}\``).join("\n") : "_(none)_";
713
+
714
+ // Load and populate the merge prompt
715
+ const wtPath = worktreePath(basePath, name);
716
+ const prompt = loadPrompt("worktree-merge", {
717
+ worktreeName: name,
718
+ worktreeBranch: branch,
719
+ mainBranch,
720
+ mainTreePath: basePath,
721
+ worktreePath: wtPath,
722
+ commitLog: commitLog || "(no commits)",
723
+ addedFiles: formatFiles(diffSummary.added),
724
+ modifiedFiles: formatFiles(diffSummary.modified),
725
+ removedFiles: formatFiles(diffSummary.removed),
726
+ gsdDiff: gsdDiff || "(no GSD artifact changes)",
727
+ codeDiff: codeDiff || "(no code changes)",
728
+ });
729
+
730
+ // Dispatch to the LLM
731
+ pi.sendMessage(
732
+ {
733
+ customType: "gsd-worktree-merge",
734
+ content: prompt,
735
+ display: false,
736
+ },
737
+ { triggerTurn: true },
738
+ );
739
+
740
+ ctx.ui.notify(
741
+ `${CLR.ok("✓")} Merge helper started for ${CLR.name(name)} ${CLR.muted(`(${codeChanges} code + ${gsdChanges} GSD artifact change${totalChanges === 1 ? "" : "s"})`)}`,
742
+ "info",
743
+ );
744
+ } catch (error) {
745
+ const msg = error instanceof Error ? error.message : String(error);
746
+ ctx.ui.notify(`Failed to start merge: ${msg}`, "error");
747
+ }
748
+ }
749
+
750
+ async function handleRemove(
751
+ basePath: string,
752
+ name: string,
753
+ ctx: ExtensionCommandContext,
754
+ ): Promise<void> {
755
+ try {
756
+ const mainBase = originalCwd ?? basePath;
757
+
758
+ // Validate the worktree exists before attempting removal
759
+ const worktrees = listWorktrees(mainBase);
760
+ const wt = worktrees.find(w => w.name === name);
761
+ if (!wt) {
762
+ ctx.ui.notify(`Worktree "${name}" not found. Run /worktree list to see available worktrees.`, "warning");
763
+ return;
764
+ }
765
+
766
+ const confirmed = await showConfirm(ctx, {
767
+ title: "Remove Worktree",
768
+ message: `Remove worktree ${CLR.name(name)} and delete branch ${CLR.branch(wt.branch)}?`,
769
+ confirmLabel: "Remove",
770
+ declineLabel: "Cancel",
771
+ });
772
+ if (!confirmed) {
773
+ ctx.ui.notify("Cancelled.", "info");
774
+ return;
775
+ }
776
+
777
+ const prevCwd = process.cwd();
778
+ removeWorktree(mainBase, name, { deleteBranch: true });
779
+
780
+ // If we were in that worktree, removeWorktree chdir'd us out — clear tracking
781
+ if (originalCwd && process.cwd() !== prevCwd) {
782
+ nudgeGitBranchCache(prevCwd);
783
+ originalCwd = null;
784
+ }
785
+
786
+ ctx.ui.notify(`${CLR.ok("✓")} Worktree ${CLR.name(name)} removed ${CLR.muted("(branch deleted)")}.`, "info");
787
+ } catch (error) {
788
+ const msg = error instanceof Error ? error.message : String(error);
789
+ ctx.ui.notify(`Failed to remove worktree: ${msg}`, "error");
790
+ }
791
+ }
792
+
793
+ async function handleRemoveAll(
794
+ basePath: string,
795
+ ctx: ExtensionCommandContext,
796
+ ): Promise<void> {
797
+ try {
798
+ const mainBase = originalCwd ?? basePath;
799
+ const worktrees = listWorktrees(mainBase);
800
+
801
+ if (worktrees.length === 0) {
802
+ ctx.ui.notify("No worktrees to remove.", "info");
803
+ return;
804
+ }
805
+
806
+ const names = worktrees.map(w => w.name);
807
+ const confirmed = await showConfirm(ctx, {
808
+ title: "Remove All Worktrees",
809
+ message: `Remove ${worktrees.length} worktree${worktrees.length === 1 ? "" : "s"} and delete their branches?\n\n${names.map(n => ` • ${CLR.name(n)}`).join("\n")}`,
810
+ confirmLabel: "Remove all",
811
+ declineLabel: "Cancel",
812
+ });
813
+ if (!confirmed) {
814
+ ctx.ui.notify("Cancelled.", "info");
815
+ return;
816
+ }
817
+
818
+ const prevCwd = process.cwd();
819
+ const removed: string[] = [];
820
+ const failed: string[] = [];
821
+
822
+ for (const wt of worktrees) {
823
+ try {
824
+ removeWorktree(mainBase, wt.name, { deleteBranch: true });
825
+ removed.push(wt.name);
826
+ } catch {
827
+ failed.push(wt.name);
828
+ }
829
+ }
830
+
831
+ // If we were in a worktree that got removed, clear tracking
832
+ if (originalCwd && process.cwd() !== prevCwd) {
833
+ nudgeGitBranchCache(prevCwd);
834
+ originalCwd = null;
835
+ }
836
+
837
+ const lines: string[] = [];
838
+ if (removed.length > 0) lines.push(`${CLR.ok("✓")} Removed: ${removed.map(n => CLR.name(n)).join(", ")}`);
839
+ if (failed.length > 0) lines.push(`${CLR.warn("✗")} Failed: ${failed.map(n => CLR.name(n)).join(", ")}`);
840
+ ctx.ui.notify(lines.join("\n"), failed.length > 0 ? "warning" : "info");
841
+ } catch (error) {
842
+ const msg = error instanceof Error ? error.message : String(error);
843
+ ctx.ui.notify(`Failed to remove worktrees: ${msg}`, "error");
844
+ }
845
+ }