gsd-pi 2.19.0 → 2.20.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 (249) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +3 -3
  3. package/dist/onboarding.d.ts +3 -1
  4. package/dist/onboarding.js +77 -3
  5. package/dist/remote-questions-config.d.ts +1 -1
  6. package/dist/resources/extensions/google-search/index.ts +164 -47
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +103 -24
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
  9. package/dist/resources/extensions/gsd/auto.ts +424 -30
  10. package/dist/resources/extensions/gsd/commands.ts +518 -36
  11. package/dist/resources/extensions/gsd/context-budget.ts +243 -0
  12. package/dist/resources/extensions/gsd/context-store.ts +195 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +41 -3
  14. package/dist/resources/extensions/gsd/db-writer.ts +341 -0
  15. package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
  16. package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
  17. package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  18. package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
  19. package/dist/resources/extensions/gsd/doctor.ts +283 -2
  20. package/dist/resources/extensions/gsd/export.ts +81 -2
  21. package/dist/resources/extensions/gsd/files.ts +39 -9
  22. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  23. package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
  24. package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
  25. package/dist/resources/extensions/gsd/history.ts +0 -1
  26. package/dist/resources/extensions/gsd/index.ts +277 -1
  27. package/dist/resources/extensions/gsd/md-importer.ts +526 -0
  28. package/dist/resources/extensions/gsd/metrics.ts +39 -3
  29. package/dist/resources/extensions/gsd/notifications.ts +0 -1
  30. package/dist/resources/extensions/gsd/post-unit-hooks.ts +70 -1
  31. package/dist/resources/extensions/gsd/preferences.ts +125 -150
  32. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
  33. package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  34. package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  35. package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
  36. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  37. package/dist/resources/extensions/gsd/quick.ts +156 -0
  38. package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
  39. package/dist/resources/extensions/gsd/skill-health.ts +417 -0
  40. package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
  41. package/dist/resources/extensions/gsd/state.ts +30 -0
  42. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  43. package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  44. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  45. package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  46. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  47. package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  48. package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  49. package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  50. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  51. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  52. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  53. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  54. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  55. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  56. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  57. package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  58. package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  59. package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  60. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  61. package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  62. package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  63. package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  64. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  65. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  66. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  67. package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  68. package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  69. package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  70. package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  71. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
  72. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  73. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  74. package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  75. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  76. package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  77. package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  78. package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  79. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
  80. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  81. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
  82. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  83. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  84. package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  85. package/dist/resources/extensions/gsd/types.ts +29 -0
  86. package/dist/resources/extensions/gsd/undo.ts +0 -1
  87. package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
  88. package/dist/resources/extensions/gsd/visualizer-data.ts +352 -1
  89. package/dist/resources/extensions/gsd/visualizer-overlay.ts +166 -22
  90. package/dist/resources/extensions/gsd/visualizer-views.ts +464 -2
  91. package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
  92. package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
  93. package/dist/resources/extensions/remote-questions/config.ts +4 -2
  94. package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -4
  95. package/dist/resources/extensions/remote-questions/format.ts +154 -8
  96. package/dist/resources/extensions/remote-questions/manager.ts +9 -7
  97. package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
  98. package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  99. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  100. package/dist/resources/extensions/remote-questions/types.ts +2 -1
  101. package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  102. package/dist/resources/extensions/voice/index.ts +4 -3
  103. package/package.json +1 -1
  104. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
  106. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  109. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
  111. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
  113. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
  115. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
  117. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
  119. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
  120. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
  122. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
  124. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
  126. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  128. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
  130. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
  133. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
  136. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  138. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  139. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  140. package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
  141. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  142. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
  143. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  144. package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
  145. package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
  146. package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
  147. package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
  148. package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
  149. package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
  150. package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
  151. package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
  152. package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
  153. package/src/resources/extensions/google-search/index.ts +164 -47
  154. package/src/resources/extensions/gsd/auto-prompts.ts +103 -24
  155. package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
  156. package/src/resources/extensions/gsd/auto.ts +424 -30
  157. package/src/resources/extensions/gsd/commands.ts +518 -36
  158. package/src/resources/extensions/gsd/context-budget.ts +243 -0
  159. package/src/resources/extensions/gsd/context-store.ts +195 -0
  160. package/src/resources/extensions/gsd/dashboard-overlay.ts +41 -3
  161. package/src/resources/extensions/gsd/db-writer.ts +341 -0
  162. package/src/resources/extensions/gsd/debug-logger.ts +178 -0
  163. package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
  164. package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  165. package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
  166. package/src/resources/extensions/gsd/doctor.ts +283 -2
  167. package/src/resources/extensions/gsd/export.ts +81 -2
  168. package/src/resources/extensions/gsd/files.ts +39 -9
  169. package/src/resources/extensions/gsd/git-service.ts +6 -0
  170. package/src/resources/extensions/gsd/gsd-db.ts +752 -0
  171. package/src/resources/extensions/gsd/guided-flow.ts +26 -1
  172. package/src/resources/extensions/gsd/history.ts +0 -1
  173. package/src/resources/extensions/gsd/index.ts +277 -1
  174. package/src/resources/extensions/gsd/md-importer.ts +526 -0
  175. package/src/resources/extensions/gsd/metrics.ts +39 -3
  176. package/src/resources/extensions/gsd/notifications.ts +0 -1
  177. package/src/resources/extensions/gsd/post-unit-hooks.ts +70 -1
  178. package/src/resources/extensions/gsd/preferences.ts +125 -150
  179. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
  180. package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  181. package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  182. package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
  183. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  184. package/src/resources/extensions/gsd/quick.ts +156 -0
  185. package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
  186. package/src/resources/extensions/gsd/skill-health.ts +417 -0
  187. package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
  188. package/src/resources/extensions/gsd/state.ts +30 -0
  189. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  190. package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  191. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  192. package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  193. package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  194. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  195. package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  196. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  197. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  198. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  199. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  200. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  201. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  202. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  203. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  204. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  205. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  206. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  207. package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  208. package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  209. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  210. package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  211. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  212. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  213. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  214. package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  215. package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  216. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  217. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  218. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
  219. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  220. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  221. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  222. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  223. package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  224. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  225. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  226. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
  227. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  228. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
  229. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  230. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  231. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  232. package/src/resources/extensions/gsd/types.ts +29 -0
  233. package/src/resources/extensions/gsd/undo.ts +0 -1
  234. package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
  235. package/src/resources/extensions/gsd/visualizer-data.ts +352 -1
  236. package/src/resources/extensions/gsd/visualizer-overlay.ts +166 -22
  237. package/src/resources/extensions/gsd/visualizer-views.ts +464 -2
  238. package/src/resources/extensions/gsd/worktree-command.ts +18 -0
  239. package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
  240. package/src/resources/extensions/remote-questions/config.ts +4 -2
  241. package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -4
  242. package/src/resources/extensions/remote-questions/format.ts +154 -8
  243. package/src/resources/extensions/remote-questions/manager.ts +9 -7
  244. package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
  245. package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  246. package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  247. package/src/resources/extensions/remote-questions/types.ts +2 -1
  248. package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  249. package/src/resources/extensions/voice/index.ts +4 -3
@@ -68,6 +68,101 @@ async function getClient(): Promise<GoogleGenAIClient> {
68
68
  return client;
69
69
  }
70
70
 
71
+ /**
72
+ * Perform a search using OAuth credentials via the Cloud Code Assist API.
73
+ * This is used as a fallback when GEMINI_API_KEY is not set.
74
+ */
75
+ async function searchWithOAuth(
76
+ query: string,
77
+ accessToken: string,
78
+ projectId: string,
79
+ signal?: AbortSignal,
80
+ ): Promise<SearchResult> {
81
+ const model = process.env.GEMINI_SEARCH_MODEL || "gemini-2.5-flash";
82
+ const url = `https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent`;
83
+
84
+ const GEMINI_CLI_HEADERS = {
85
+ ideType: "IDE_UNSPECIFIED",
86
+ platform: "PLATFORM_UNSPECIFIED",
87
+ pluginType: "GEMINI",
88
+ };
89
+
90
+ const executeFetch = async (retries = 3): Promise<Response> => {
91
+ const response = await fetch(url, {
92
+ method: "POST",
93
+ headers: {
94
+ Authorization: `Bearer ${accessToken}`,
95
+ "Content-Type": "application/json",
96
+ "User-Agent": "google-cloud-sdk vscode_cloudshelleditor/0.1",
97
+ "X-Goog-Api-Client": "gl-node/22.17.0",
98
+ "Client-Metadata": JSON.stringify(GEMINI_CLI_HEADERS),
99
+ },
100
+ body: JSON.stringify({
101
+ project: projectId,
102
+ model,
103
+ request: {
104
+ contents: [{ parts: [{ text: query }] }],
105
+ tools: [{ googleSearch: {} }],
106
+ },
107
+ }),
108
+ signal,
109
+ });
110
+
111
+ if (!response.ok && retries > 0 && (response.status === 429 || response.status >= 500)) {
112
+ await new Promise((resolve) => setTimeout(resolve, 1000 * (4 - retries)));
113
+ return executeFetch(retries - 1);
114
+ }
115
+
116
+ return response;
117
+ };
118
+
119
+ const response = await executeFetch();
120
+
121
+ if (!response.ok) {
122
+ const errorText = await response.text();
123
+ throw new Error(`Cloud Code Assist API error (${response.status}): ${errorText}`);
124
+ }
125
+
126
+ // Note: streamGenerateContent returns SSE; for now, we consume all chunks.
127
+ // For simplicity and to match the previous structure, we'll read to end.
128
+ const text = await response.text();
129
+ const jsonLines = text.split("\n")
130
+ .filter(l => l.startsWith("data:"))
131
+ .map(l => l.slice(5).trim())
132
+ .filter(l => l.length > 0);
133
+
134
+ let data;
135
+ if (jsonLines.length > 0) {
136
+ // Aggregate chunks if needed, but for now we take the last chunk or assume it's one
137
+ data = JSON.parse(jsonLines[jsonLines.length - 1]);
138
+ } else {
139
+ data = JSON.parse(text);
140
+ } const candidate = data.response?.candidates?.[0];
141
+ const answer = candidate?.content?.parts?.find((p: any) => p.text)?.text ?? "";
142
+ const grounding = candidate?.groundingMetadata;
143
+
144
+ const sources: SearchSource[] = [];
145
+ const seenTitles = new Set<string>();
146
+ if (grounding?.groundingChunks) {
147
+ for (const chunk of grounding.groundingChunks) {
148
+ if (chunk.web) {
149
+ const title = chunk.web.title ?? "Untitled";
150
+ if (seenTitles.has(title)) continue;
151
+ seenTitles.add(title);
152
+ const domain = chunk.web.domain ?? title;
153
+ sources.push({
154
+ title,
155
+ uri: chunk.web.uri ?? "",
156
+ domain,
157
+ });
158
+ }
159
+ }
160
+ }
161
+
162
+ const searchQueries = grounding?.webSearchQueries ?? [];
163
+ return { answer, sources, searchQueries, cached: false };
164
+ }
165
+
71
166
  // ── In-session cache ─────────────────────────────────────────────────────────
72
167
 
73
168
  const resultCache = new Map<string, SearchResult>();
@@ -87,7 +182,7 @@ export default function (pi: ExtensionAPI) {
87
182
  "Returns an AI-synthesized answer grounded in Google Search results, plus source URLs. " +
88
183
  "Use this when you need current information from the web: recent events, documentation, " +
89
184
  "product details, technical references, news, etc. " +
90
- "Requires GEMINI_API_KEY. Alternative to Brave-based search tools for users with Google Cloud credits.",
185
+ "Requires GEMINI_API_KEY or Google login. Alternative to Brave-based search tools.",
91
186
  promptSnippet: "Search the web via Google Search to get current information with sources",
92
187
  promptGuidelines: [
93
188
  "Use google_search when you need up-to-date web information that isn't in your training data.",
@@ -109,17 +204,33 @@ export default function (pi: ExtensionAPI) {
109
204
  ),
110
205
  }),
111
206
 
112
- async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
207
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
113
208
  const startTime = Date.now();
114
209
  const maxSources = Math.min(Math.max(params.maxSources ?? 5, 1), 10);
115
210
 
116
- // Check for API key
211
+ // Check for credentials
212
+ let oauthToken: string | undefined;
213
+ let projectId: string | undefined;
214
+
117
215
  if (!process.env.GEMINI_API_KEY) {
216
+ const oauthRaw = await ctx.modelRegistry.getApiKeyForProvider("google-gemini-cli");
217
+ if (oauthRaw) {
218
+ try {
219
+ const parsed = JSON.parse(oauthRaw);
220
+ oauthToken = parsed.token;
221
+ projectId = parsed.projectId;
222
+ } catch {
223
+ // Fall through to error
224
+ }
225
+ }
226
+ }
227
+
228
+ if (!process.env.GEMINI_API_KEY && (!oauthToken || !projectId)) {
118
229
  return {
119
230
  content: [
120
231
  {
121
232
  type: "text",
122
- text: "Error: GEMINI_API_KEY is not set. Please set this environment variable to use Google Search.\n\nExample: export GEMINI_API_KEY=your_key",
233
+ text: "Error: No authentication found for Google Search. Please set GEMINI_API_KEY or log in via Google.\n\nExample: export GEMINI_API_KEY=your_key or use /login google",
123
234
  },
124
235
  ],
125
236
  isError: true,
@@ -128,7 +239,7 @@ export default function (pi: ExtensionAPI) {
128
239
  sourceCount: 0,
129
240
  cached: false,
130
241
  durationMs: Date.now() - startTime,
131
- error: "auth_error: GEMINI_API_KEY not set",
242
+ error: "auth_error: No credentials set",
132
243
  } as SearchDetails,
133
244
  };
134
245
  }
@@ -152,49 +263,52 @@ export default function (pi: ExtensionAPI) {
152
263
  // Call Gemini with Google Search grounding
153
264
  let result: SearchResult;
154
265
  try {
155
- const ai = await getClient();
156
- const response = await ai.models.generateContent({
157
- model: process.env.GEMINI_SEARCH_MODEL || "gemini-2.5-flash",
158
- contents: params.query,
159
- config: {
160
- tools: [{ googleSearch: {} }],
161
- abortSignal: signal,
162
- },
163
- });
164
-
165
- // Extract answer text
166
- const answer = response.text ?? "";
167
-
168
- // Extract grounding metadata
169
- const candidate = response.candidates?.[0];
170
- const grounding = candidate?.groundingMetadata;
171
-
172
- // Parse sources from grounding chunks
173
- const sources: SearchSource[] = [];
174
- const seenTitles = new Set<string>();
175
- if (grounding?.groundingChunks) {
176
- for (const chunk of grounding.groundingChunks) {
177
- if (chunk.web) {
178
- const title = chunk.web.title ?? "Untitled";
179
- // Dedupe by title since URIs are redirect URLs that differ per call
180
- if (seenTitles.has(title)) continue;
181
- seenTitles.add(title);
182
- // domain field is not available via Gemini API, use title as fallback
183
- // (title is typically the domain name, e.g. "wikipedia.org")
184
- const domain = chunk.web.domain ?? title;
185
- sources.push({
186
- title,
187
- uri: chunk.web.uri ?? "",
188
- domain,
189
- });
266
+ if (process.env.GEMINI_API_KEY) {
267
+ const ai = await getClient();
268
+ const response = await ai.models.generateContent({
269
+ model: process.env.GEMINI_SEARCH_MODEL || "gemini-2.5-flash",
270
+ contents: params.query,
271
+ config: {
272
+ tools: [{ googleSearch: {} }],
273
+ abortSignal: signal,
274
+ },
275
+ });
276
+
277
+ // Extract answer text
278
+ const answer = response.text ?? "";
279
+
280
+ // Extract grounding metadata
281
+ const candidate = response.candidates?.[0];
282
+ const grounding = candidate?.groundingMetadata;
283
+
284
+ // Parse sources from grounding chunks
285
+ const sources: SearchSource[] = [];
286
+ const seenTitles = new Set<string>();
287
+ if (grounding?.groundingChunks) {
288
+ for (const chunk of grounding.groundingChunks) {
289
+ if (chunk.web) {
290
+ const title = chunk.web.title ?? "Untitled";
291
+ // Dedupe by title since URIs are redirect URLs that differ per call
292
+ if (seenTitles.has(title)) continue;
293
+ seenTitles.add(title);
294
+ // domain field is not available via Gemini API, use title as fallback
295
+ // (title is typically the domain name, e.g. "wikipedia.org")
296
+ const domain = chunk.web.domain ?? title;
297
+ sources.push({
298
+ title,
299
+ uri: chunk.web.uri ?? "",
300
+ domain,
301
+ });
302
+ }
190
303
  }
191
304
  }
192
- }
193
305
 
194
- // Extract search queries Gemini actually performed
195
- const searchQueries = grounding?.webSearchQueries ?? [];
196
-
197
- result = { answer, sources, searchQueries, cached: false };
306
+ // Extract search queries Gemini actually performed
307
+ const searchQueries = grounding?.webSearchQueries ?? [];
308
+ result = { answer, sources, searchQueries, cached: false };
309
+ } else {
310
+ result = await searchWithOAuth(params.query, oauthToken!, projectId!, signal);
311
+ }
198
312
  } catch (err: unknown) {
199
313
  const msg = err instanceof Error ? err.message : String(err);
200
314
 
@@ -287,9 +401,12 @@ export default function (pi: ExtensionAPI) {
287
401
  // ── Startup notification ─────────────────────────────────────────────────
288
402
 
289
403
  pi.on("session_start", async (_event, ctx) => {
290
- if (!process.env.GEMINI_API_KEY) {
404
+ if (process.env.GEMINI_API_KEY) return;
405
+
406
+ const hasOAuth = await ctx.modelRegistry.authStorage.hasAuth("google-gemini-cli");
407
+ if (!hasOAuth) {
291
408
  ctx.ui.notify(
292
- "Google Search: No GEMINI_API_KEY set. The google_search tool will not work until this is configured.",
409
+ "Google Search: No authentication set. Log in via Google or set GEMINI_API_KEY to use google_search.",
293
410
  "warning",
294
411
  );
295
412
  }
@@ -55,7 +55,7 @@ export async function inlineFileOptional(
55
55
  * Load and inline dependency slice summaries (full content, not just paths).
56
56
  */
57
57
  export async function inlineDependencySummaries(
58
- mid: string, sid: string, base: string,
58
+ mid: string, sid: string, base: string, budgetChars?: number,
59
59
  ): Promise<string> {
60
60
  const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
61
61
  const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
@@ -79,7 +79,14 @@ export async function inlineDependencySummaries(
79
79
  sections.push(`- \`${relPath}\` _(not found)_`);
80
80
  }
81
81
  }
82
- return sections.join("\n\n");
82
+
83
+ const result = sections.join("\n\n");
84
+ // When a budget is provided, truncate at section boundaries to fit
85
+ if (budgetChars !== undefined && result.length > budgetChars) {
86
+ const { truncateAtSectionBoundary } = await import("./context-budget.js");
87
+ return truncateAtSectionBoundary(result, budgetChars).content;
88
+ }
89
+ return result;
83
90
  }
84
91
 
85
92
  /**
@@ -95,6 +102,76 @@ export async function inlineGsdRootFile(
95
102
  return inlineFileOptional(absPath, relGsdRootFile(key), label);
96
103
  }
97
104
 
105
+ // ─── DB-Aware Inline Helpers ──────────────────────────────────────────────
106
+
107
+ /**
108
+ * Inline decisions with optional milestone scoping from the DB.
109
+ * Falls back to filesystem via inlineGsdRootFile when DB unavailable or empty.
110
+ */
111
+ export async function inlineDecisionsFromDb(
112
+ base: string, milestoneId?: string, scope?: string,
113
+ ): Promise<string | null> {
114
+ try {
115
+ const { isDbAvailable } = await import("./gsd-db.js");
116
+ if (isDbAvailable()) {
117
+ const { queryDecisions, formatDecisionsForPrompt } = await import("./context-store.js");
118
+ const decisions = queryDecisions({ milestoneId, scope });
119
+ if (decisions.length > 0) {
120
+ const formatted = formatDecisionsForPrompt(decisions);
121
+ return `### Decisions\nSource: \`.gsd/DECISIONS.md\`\n\n${formatted}`;
122
+ }
123
+ }
124
+ } catch {
125
+ // DB not available — fall through to filesystem
126
+ }
127
+ return inlineGsdRootFile(base, "decisions.md", "Decisions");
128
+ }
129
+
130
+ /**
131
+ * Inline requirements with optional slice scoping from the DB.
132
+ * Falls back to filesystem via inlineGsdRootFile when DB unavailable or empty.
133
+ */
134
+ export async function inlineRequirementsFromDb(
135
+ base: string, sliceId?: string,
136
+ ): Promise<string | null> {
137
+ try {
138
+ const { isDbAvailable } = await import("./gsd-db.js");
139
+ if (isDbAvailable()) {
140
+ const { queryRequirements, formatRequirementsForPrompt } = await import("./context-store.js");
141
+ const requirements = queryRequirements({ sliceId });
142
+ if (requirements.length > 0) {
143
+ const formatted = formatRequirementsForPrompt(requirements);
144
+ return `### Requirements\nSource: \`.gsd/REQUIREMENTS.md\`\n\n${formatted}`;
145
+ }
146
+ }
147
+ } catch {
148
+ // DB not available — fall through to filesystem
149
+ }
150
+ return inlineGsdRootFile(base, "requirements.md", "Requirements");
151
+ }
152
+
153
+ /**
154
+ * Inline project context from the DB.
155
+ * Falls back to filesystem via inlineGsdRootFile when DB unavailable or empty.
156
+ */
157
+ export async function inlineProjectFromDb(
158
+ base: string,
159
+ ): Promise<string | null> {
160
+ try {
161
+ const { isDbAvailable } = await import("./gsd-db.js");
162
+ if (isDbAvailable()) {
163
+ const { queryProject } = await import("./context-store.js");
164
+ const content = queryProject();
165
+ if (content) {
166
+ return `### Project\nSource: \`.gsd/PROJECT.md\`\n\n${content}`;
167
+ }
168
+ }
169
+ } catch {
170
+ // DB not available — fall through to filesystem
171
+ }
172
+ return inlineGsdRootFile(base, "project.md", "Project");
173
+ }
174
+
98
175
  // ─── Skill Discovery ──────────────────────────────────────────────────────
99
176
 
100
177
  /**
@@ -371,11 +448,11 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
371
448
 
372
449
  const inlined: string[] = [];
373
450
  inlined.push(await inlineFile(contextPath, contextRel, "Milestone Context"));
374
- const projectInline = await inlineGsdRootFile(base, "project.md", "Project");
451
+ const projectInline = await inlineProjectFromDb(base);
375
452
  if (projectInline) inlined.push(projectInline);
376
- const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
453
+ const requirementsInline = await inlineRequirementsFromDb(base);
377
454
  if (requirementsInline) inlined.push(requirementsInline);
378
- const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
455
+ const decisionsInline = await inlineDecisionsFromDb(base, mid);
379
456
  if (decisionsInline) inlined.push(decisionsInline);
380
457
  const knowledgeInlineRM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
381
458
  if (knowledgeInlineRM) inlined.push(knowledgeInlineRM);
@@ -409,12 +486,14 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
409
486
  const { inlinePriorMilestoneSummary } = await import("./files.js");
410
487
  const priorSummaryInline = await inlinePriorMilestoneSummary(mid, base);
411
488
  if (priorSummaryInline) inlined.push(priorSummaryInline);
412
- const projectInline = inlineLevel !== "minimal" ? await inlineGsdRootFile(base, "project.md", "Project") : null;
413
- if (projectInline) inlined.push(projectInline);
414
- const requirementsInline = inlineLevel !== "minimal" ? await inlineGsdRootFile(base, "requirements.md", "Requirements") : null;
415
- if (requirementsInline) inlined.push(requirementsInline);
416
- const decisionsInline = inlineLevel !== "minimal" ? await inlineGsdRootFile(base, "decisions.md", "Decisions") : null;
417
- if (decisionsInline) inlined.push(decisionsInline);
489
+ if (inlineLevel !== "minimal") {
490
+ const projectInline = await inlineProjectFromDb(base);
491
+ if (projectInline) inlined.push(projectInline);
492
+ const requirementsInline = await inlineRequirementsFromDb(base);
493
+ if (requirementsInline) inlined.push(requirementsInline);
494
+ const decisionsInline = await inlineDecisionsFromDb(base, mid);
495
+ if (decisionsInline) inlined.push(decisionsInline);
496
+ }
418
497
  const knowledgeInlinePM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
419
498
  if (knowledgeInlinePM) inlined.push(knowledgeInlinePM);
420
499
  inlined.push(inlineTemplate("roadmap", "Roadmap"));
@@ -461,9 +540,9 @@ export async function buildResearchSlicePrompt(
461
540
  if (contextInline) inlined.push(contextInline);
462
541
  const researchInline = await inlineFileOptional(milestoneResearchPath, milestoneResearchRel, "Milestone Research");
463
542
  if (researchInline) inlined.push(researchInline);
464
- const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
543
+ const decisionsInline = await inlineDecisionsFromDb(base, mid);
465
544
  if (decisionsInline) inlined.push(decisionsInline);
466
- const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
545
+ const requirementsInline = await inlineRequirementsFromDb(base, sid);
467
546
  if (requirementsInline) inlined.push(requirementsInline);
468
547
  const knowledgeInlineRS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
469
548
  if (knowledgeInlineRS) inlined.push(knowledgeInlineRS);
@@ -505,9 +584,9 @@ export async function buildPlanSlicePrompt(
505
584
  const researchInline = await inlineFileOptional(researchPath, researchRel, "Slice Research");
506
585
  if (researchInline) inlined.push(researchInline);
507
586
  if (inlineLevel !== "minimal") {
508
- const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
587
+ const decisionsInline = await inlineDecisionsFromDb(base, mid);
509
588
  if (decisionsInline) inlined.push(decisionsInline);
510
- const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
589
+ const requirementsInline = await inlineRequirementsFromDb(base, sid);
511
590
  if (requirementsInline) inlined.push(requirementsInline);
512
591
  }
513
592
  const knowledgeInlinePS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
@@ -634,7 +713,7 @@ export async function buildCompleteSlicePrompt(
634
713
  inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
635
714
  inlined.push(await inlineFile(slicePlanPath, slicePlanRel, "Slice Plan"));
636
715
  if (inlineLevel !== "minimal") {
637
- const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
716
+ const requirementsInline = await inlineRequirementsFromDb(base, sid);
638
717
  if (requirementsInline) inlined.push(requirementsInline);
639
718
  }
640
719
  const knowledgeInlineCS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
@@ -705,11 +784,11 @@ export async function buildCompleteMilestonePrompt(
705
784
 
706
785
  // Inline root GSD files (skip for minimal — completion can read these if needed)
707
786
  if (inlineLevel !== "minimal") {
708
- const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
787
+ const requirementsInline = await inlineRequirementsFromDb(base);
709
788
  if (requirementsInline) inlined.push(requirementsInline);
710
- const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
789
+ const decisionsInline = await inlineDecisionsFromDb(base, mid);
711
790
  if (decisionsInline) inlined.push(decisionsInline);
712
- const projectInline = await inlineGsdRootFile(base, "project.md", "Project");
791
+ const projectInline = await inlineProjectFromDb(base);
713
792
  if (projectInline) inlined.push(projectInline);
714
793
  }
715
794
  const knowledgeInlineCM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
@@ -767,7 +846,7 @@ export async function buildReplanSlicePrompt(
767
846
  }
768
847
 
769
848
  // Inline decisions
770
- const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
849
+ const decisionsInline = await inlineDecisionsFromDb(base, mid);
771
850
  if (decisionsInline) inlined.push(decisionsInline);
772
851
  const replanActiveOverrides = await loadActiveOverrides(base);
773
852
  const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
@@ -818,7 +897,7 @@ export async function buildRunUatPrompt(
818
897
  if (summaryInline) inlined.push(summaryInline);
819
898
  }
820
899
 
821
- const projectInline = await inlineGsdRootFile(base, "project.md", "Project");
900
+ const projectInline = await inlineProjectFromDb(base);
822
901
  if (projectInline) inlined.push(projectInline);
823
902
 
824
903
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
@@ -850,11 +929,11 @@ export async function buildReassessRoadmapPrompt(
850
929
  inlined.push(await inlineFile(roadmapPath, roadmapRel, "Current Roadmap"));
851
930
  inlined.push(await inlineFile(summaryPath, summaryRel, `${completedSliceId} Summary`));
852
931
  if (inlineLevel !== "minimal") {
853
- const projectInline = await inlineGsdRootFile(base, "project.md", "Project");
932
+ const projectInline = await inlineProjectFromDb(base);
854
933
  if (projectInline) inlined.push(projectInline);
855
- const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
934
+ const requirementsInline = await inlineRequirementsFromDb(base);
856
935
  if (requirementsInline) inlined.push(requirementsInline);
857
- const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
936
+ const decisionsInline = await inlineDecisionsFromDb(base, mid);
858
937
  if (decisionsInline) inlined.push(decisionsInline);
859
938
  }
860
939
  const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
@@ -7,8 +7,9 @@
7
7
  */
8
8
 
9
9
  import { existsSync, cpSync, readFileSync, realpathSync, utimesSync } from "node:fs";
10
- import { join, resolve } from "node:path";
11
- import { execSync } from "node:child_process";
10
+ import { isAbsolute, join, resolve } from "node:path";
11
+ import { copyWorktreeDb, reconcileWorktreeDb, isDbAvailable } from "./gsd-db.js";
12
+ import { execSync, execFileSync } from "node:child_process";
12
13
  import {
13
14
  createWorktree,
14
15
  removeWorktree,
@@ -33,6 +34,7 @@ import {
33
34
  nativeAddPaths,
34
35
  nativeRmForce,
35
36
  nativeBranchDelete,
37
+ nativeBranchExists,
36
38
  } from "./native-git-bridge.js";
37
39
 
38
40
  // ─── Module State ──────────────────────────────────────────────────────────
@@ -75,6 +77,48 @@ function nudgeGitBranchCache(previousCwd: string): void {
75
77
  }
76
78
  }
77
79
 
80
+ // ─── Worktree Post-Create Hook (#597) ────────────────────────────────────────
81
+
82
+ /**
83
+ * Run the user-configured post-create hook script after worktree creation.
84
+ * The script receives SOURCE_DIR and WORKTREE_DIR as environment variables.
85
+ * Failure is non-fatal — returns the error message or null on success.
86
+ *
87
+ * Reads the hook path from git.worktree_post_create in preferences.
88
+ * Pass hookPath directly to bypass preference loading (useful for testing).
89
+ */
90
+ export function runWorktreePostCreateHook(sourceDir: string, worktreeDir: string, hookPath?: string): string | null {
91
+ if (hookPath === undefined) {
92
+ const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
93
+ hookPath = prefs?.worktree_post_create;
94
+ }
95
+ if (!hookPath) return null;
96
+
97
+ // Resolve relative paths against the source project root
98
+ const resolved = isAbsolute(hookPath) ? hookPath : join(sourceDir, hookPath);
99
+ if (!existsSync(resolved)) {
100
+ return `Worktree post-create hook not found: ${resolved}`;
101
+ }
102
+
103
+ try {
104
+ execSync(resolved, {
105
+ cwd: worktreeDir,
106
+ env: {
107
+ ...process.env,
108
+ SOURCE_DIR: sourceDir,
109
+ WORKTREE_DIR: worktreeDir,
110
+ },
111
+ stdio: ["ignore", "pipe", "pipe"],
112
+ encoding: "utf-8",
113
+ timeout: 30_000, // 30 second timeout
114
+ });
115
+ return null;
116
+ } catch (err) {
117
+ const msg = err instanceof Error ? err.message : String(err);
118
+ return `Worktree post-create hook failed: ${msg}`;
119
+ }
120
+ }
121
+
78
122
  // ─── Auto-Worktree Branch Naming ───────────────────────────────────────────
79
123
 
80
124
  export function autoWorktreeBranch(milestoneId: string): string {
@@ -93,11 +137,21 @@ export function autoWorktreeBranch(milestoneId: string): string {
93
137
  export function createAutoWorktree(basePath: string, milestoneId: string): string {
94
138
  const branch = autoWorktreeBranch(milestoneId);
95
139
 
96
- // Use the integration branch recorded in META.json as the start point.
97
- // This ensures the worktree branch is created from the branch the user
98
- // was on when they started the milestone (e.g. f-setup-gsd-2), not main.
99
- const integrationBranch = readIntegrationBranch(basePath, milestoneId) ?? undefined;
100
- const info = createWorktree(basePath, milestoneId, { branch, startPoint: integrationBranch });
140
+ // Check if the milestone branch already exists it survives auto-mode
141
+ // stop/pause and contains committed work from prior sessions. If it exists,
142
+ // re-attach the worktree to it WITHOUT resetting. Only create a fresh branch
143
+ // from the integration branch when no prior work exists.
144
+ const branchExists = nativeBranchExists(basePath, branch);
145
+
146
+ let info: { name: string; path: string; branch: string; exists: boolean };
147
+ if (branchExists) {
148
+ // Re-attach worktree to the existing milestone branch (preserving commits)
149
+ info = createWorktree(basePath, milestoneId, { branch, reuseExistingBranch: true });
150
+ } else {
151
+ // Fresh start — create branch from integration branch
152
+ const integrationBranch = readIntegrationBranch(basePath, milestoneId) ?? undefined;
153
+ info = createWorktree(basePath, milestoneId, { branch, startPoint: integrationBranch });
154
+ }
101
155
 
102
156
  // Copy .gsd/ planning artifacts from the source repo into the new worktree.
103
157
  // Worktrees are fresh git checkouts — untracked files don't carry over.
@@ -106,6 +160,13 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
106
160
  // on plan-slice because the plan file doesn't exist in the worktree.
107
161
  copyPlanningArtifacts(basePath, info.path);
108
162
 
163
+ // Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
164
+ const hookError = runWorktreePostCreateHook(basePath, info.path);
165
+ if (hookError) {
166
+ // Non-fatal — log but don't prevent worktree usage
167
+ console.error(`[GSD] ${hookError}`);
168
+ }
169
+
109
170
  const previousCwd = process.cwd();
110
171
 
111
172
  try {
@@ -151,14 +212,28 @@ function copyPlanningArtifacts(srcBase: string, wtPath: string): void {
151
212
  } catch { /* non-fatal */ }
152
213
  }
153
214
  }
215
+
216
+ // Copy gsd.db if present in source
217
+ const srcDb = join(srcGsd, "gsd.db");
218
+ const destDb = join(dstGsd, "gsd.db");
219
+ if (existsSync(srcDb)) {
220
+ try {
221
+ copyWorktreeDb(srcDb, destDb);
222
+ } catch { /* non-fatal */ }
223
+ }
154
224
  }
155
225
 
156
226
  /**
157
227
  * Teardown an auto-worktree: chdir back to original base, then remove
158
228
  * the worktree and its branch.
159
229
  */
160
- export function teardownAutoWorktree(originalBasePath: string, milestoneId: string): void {
230
+ export function teardownAutoWorktree(
231
+ originalBasePath: string,
232
+ milestoneId: string,
233
+ opts: { preserveBranch?: boolean } = {},
234
+ ): void {
161
235
  const branch = autoWorktreeBranch(milestoneId);
236
+ const { preserveBranch = false } = opts;
162
237
  const previousCwd = process.cwd();
163
238
 
164
239
  try {
@@ -171,7 +246,7 @@ export function teardownAutoWorktree(originalBasePath: string, milestoneId: stri
171
246
  }
172
247
 
173
248
  nudgeGitBranchCache(previousCwd);
174
- removeWorktree(originalBasePath, milestoneId, { branch });
249
+ removeWorktree(originalBasePath, milestoneId, { branch, deleteBranch: !preserveBranch });
175
250
  }
176
251
 
177
252
  /**
@@ -299,6 +374,15 @@ export function mergeMilestoneToMain(
299
374
  // 1. Auto-commit dirty state in worktree before leaving
300
375
  autoCommitDirtyState(worktreeCwd);
301
376
 
377
+ // Reconcile worktree DB into main DB before leaving worktree context
378
+ if (isDbAvailable()) {
379
+ try {
380
+ const worktreeDbPath = join(worktreeCwd, ".gsd", "gsd.db");
381
+ const mainDbPath = join(originalBasePath_, ".gsd", "gsd.db");
382
+ reconcileWorktreeDb(mainDbPath, worktreeDbPath);
383
+ } catch { /* non-fatal */ }
384
+ }
385
+
302
386
  // 2. Parse roadmap for slice listing
303
387
  const roadmap = parseRoadmap(roadmapContent);
304
388
  const completedSlices = roadmap.slices.filter(s => s.done);