gsd-pi 2.42.0 → 2.43.0-dev.5717b75

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 (291) hide show
  1. package/README.md +23 -0
  2. package/dist/cli.js +18 -3
  3. package/dist/loader.js +3 -1
  4. package/dist/resource-loader.js +39 -6
  5. package/dist/resources/extensions/async-jobs/async-bash-tool.js +52 -4
  6. package/dist/resources/extensions/async-jobs/await-tool.js +5 -0
  7. package/dist/resources/extensions/async-jobs/index.js +2 -0
  8. package/dist/resources/extensions/gsd/auto/phases.js +1 -3
  9. package/dist/resources/extensions/gsd/auto-prompts.js +3 -16
  10. package/dist/resources/extensions/gsd/auto-start.js +8 -11
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +11 -5
  12. package/dist/resources/extensions/gsd/detection.js +19 -0
  13. package/dist/resources/extensions/gsd/doctor-checks.js +31 -1
  14. package/dist/resources/extensions/gsd/doctor-providers.js +10 -0
  15. package/dist/resources/extensions/gsd/forensics.js +84 -0
  16. package/dist/resources/extensions/gsd/git-constants.js +1 -0
  17. package/dist/resources/extensions/gsd/git-service.js +1 -1
  18. package/dist/resources/extensions/gsd/native-git-bridge.js +1 -0
  19. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  20. package/dist/resources/extensions/gsd/preferences.js +59 -8
  21. package/dist/resources/extensions/gsd/prompts/forensics.md +12 -5
  22. package/dist/resources/extensions/gsd/repo-identity.js +46 -5
  23. package/dist/resources/extensions/gsd/service-tier.js +13 -4
  24. package/dist/resources/extensions/gsd/session-lock.js +2 -2
  25. package/dist/resources/extensions/gsd/worktree-resolver.js +2 -2
  26. package/dist/resources/extensions/gsd/worktree.js +2 -2
  27. package/dist/resources/extensions/mcp-client/index.js +2 -1
  28. package/dist/resources/extensions/search-the-web/tool-search.js +3 -3
  29. package/dist/web/standalone/.next/BUILD_ID +1 -1
  30. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  31. package/dist/web/standalone/.next/build-manifest.json +3 -3
  32. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  33. package/dist/web/standalone/.next/required-server-files.json +3 -3
  34. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  35. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  37. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  45. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  61. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +5 -5
  99. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  105. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  119. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  121. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  123. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  132. package/dist/web/standalone/.next/server/app/index.html +1 -1
  133. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  134. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  135. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  136. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  137. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  138. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  139. package/dist/web/standalone/.next/server/app/page.js +2 -2
  140. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  142. package/dist/web/standalone/.next/server/chunks/229.js +2 -2
  143. package/dist/web/standalone/.next/server/chunks/471.js +3 -3
  144. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  145. package/dist/web/standalone/.next/server/middleware.js +2 -2
  146. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  147. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  148. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  149. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  150. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  151. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-f2a7482d42a5614b.js +1 -0
  152. package/dist/web/standalone/.next/static/chunks/app/layout-a16c7a7ecdf0c2cf.js +1 -0
  153. package/dist/web/standalone/.next/static/chunks/app/page-b9367c5ae13b99c6.js +1 -0
  154. package/dist/web/standalone/.next/static/chunks/{main-app-2f2ee7b85712c2bd.js → main-app-fdab67f7802d7832.js} +1 -1
  155. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  156. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  157. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  158. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  159. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  160. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  161. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  162. package/dist/web/standalone/server.js +1 -1
  163. package/dist/web-mode.d.ts +2 -0
  164. package/dist/web-mode.js +40 -4
  165. package/package.json +1 -1
  166. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  167. package/packages/pi-agent-core/dist/agent.js +2 -0
  168. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  169. package/packages/pi-agent-core/dist/types.d.ts +6 -0
  170. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  171. package/packages/pi-agent-core/dist/types.js.map +1 -1
  172. package/packages/pi-agent-core/src/agent.test.ts +53 -0
  173. package/packages/pi-agent-core/src/agent.ts +3 -0
  174. package/packages/pi-agent-core/src/types.ts +6 -0
  175. package/packages/pi-agent-core/tsconfig.json +1 -1
  176. package/packages/pi-ai/dist/models.d.ts +5 -3
  177. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  178. package/packages/pi-ai/dist/models.generated.d.ts +801 -1468
  179. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  180. package/packages/pi-ai/dist/models.generated.js +1135 -1588
  181. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  182. package/packages/pi-ai/dist/models.js.map +1 -1
  183. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  184. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +60 -2
  185. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  186. package/packages/pi-ai/scripts/generate-models.ts +1543 -0
  187. package/packages/pi-ai/src/models.generated.ts +1140 -1593
  188. package/packages/pi-ai/src/models.ts +7 -4
  189. package/packages/pi-ai/src/utils/oauth/github-copilot.ts +74 -2
  190. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  191. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -1
  192. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  193. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +7 -0
  194. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  195. package/packages/pi-coding-agent/dist/core/auth-storage.js +29 -2
  196. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  197. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +60 -0
  198. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  199. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  200. package/packages/pi-coding-agent/dist/core/extensions/loader.js +18 -0
  201. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  202. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  203. package/packages/pi-coding-agent/dist/core/lsp/client.js +23 -0
  204. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  205. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  206. package/packages/pi-coding-agent/dist/core/model-registry.js +2 -0
  207. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  208. package/packages/pi-coding-agent/dist/core/package-manager.d.ts +6 -0
  209. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  210. package/packages/pi-coding-agent/dist/core/package-manager.js +63 -11
  211. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  212. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +9 -0
  213. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  214. package/packages/pi-coding-agent/dist/core/resource-loader.js +20 -6
  215. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  216. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  217. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -5
  218. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  219. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  220. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.js +3 -0
  221. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.js.map +1 -1
  222. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  223. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +9 -6
  224. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  225. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  226. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +30 -10
  227. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  228. package/packages/pi-coding-agent/package.json +1 -1
  229. package/packages/pi-coding-agent/src/core/agent-session.ts +7 -1
  230. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +68 -0
  231. package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -2
  232. package/packages/pi-coding-agent/src/core/extensions/loader.ts +18 -0
  233. package/packages/pi-coding-agent/src/core/lsp/client.ts +29 -0
  234. package/packages/pi-coding-agent/src/core/model-registry.ts +3 -0
  235. package/packages/pi-coding-agent/src/core/package-manager.ts +99 -58
  236. package/packages/pi-coding-agent/src/core/resource-loader.ts +24 -6
  237. package/packages/pi-coding-agent/src/core/system-prompt.ts +6 -5
  238. package/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts +3 -0
  239. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +10 -6
  240. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +31 -11
  241. package/pkg/package.json +1 -1
  242. package/src/resources/extensions/async-jobs/async-bash-timeout.test.ts +122 -0
  243. package/src/resources/extensions/async-jobs/async-bash-tool.ts +40 -4
  244. package/src/resources/extensions/async-jobs/await-tool.test.ts +47 -0
  245. package/src/resources/extensions/async-jobs/await-tool.ts +5 -0
  246. package/src/resources/extensions/async-jobs/index.ts +1 -0
  247. package/src/resources/extensions/async-jobs/job-manager.ts +2 -0
  248. package/src/resources/extensions/gsd/auto/loop-deps.ts +0 -1
  249. package/src/resources/extensions/gsd/auto/phases.ts +1 -3
  250. package/src/resources/extensions/gsd/auto-prompts.ts +2 -18
  251. package/src/resources/extensions/gsd/auto-start.ts +7 -10
  252. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -5
  253. package/src/resources/extensions/gsd/detection.ts +19 -0
  254. package/src/resources/extensions/gsd/doctor-checks.ts +32 -1
  255. package/src/resources/extensions/gsd/doctor-providers.ts +13 -0
  256. package/src/resources/extensions/gsd/doctor-types.ts +1 -0
  257. package/src/resources/extensions/gsd/forensics.ts +92 -0
  258. package/src/resources/extensions/gsd/git-constants.ts +1 -0
  259. package/src/resources/extensions/gsd/git-service.ts +0 -1
  260. package/src/resources/extensions/gsd/gitignore.ts +1 -1
  261. package/src/resources/extensions/gsd/native-git-bridge.ts +1 -0
  262. package/src/resources/extensions/gsd/preferences-types.ts +3 -0
  263. package/src/resources/extensions/gsd/preferences.ts +62 -6
  264. package/src/resources/extensions/gsd/prompts/forensics.md +12 -5
  265. package/src/resources/extensions/gsd/repo-identity.ts +48 -5
  266. package/src/resources/extensions/gsd/service-tier.ts +17 -4
  267. package/src/resources/extensions/gsd/session-lock.ts +2 -2
  268. package/src/resources/extensions/gsd/tests/activity-log.test.ts +31 -69
  269. package/src/resources/extensions/gsd/tests/forensics-dedup.test.ts +48 -0
  270. package/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts +43 -0
  271. package/src/resources/extensions/gsd/tests/git-locale.test.ts +133 -0
  272. package/src/resources/extensions/gsd/tests/git-service.test.ts +44 -0
  273. package/src/resources/extensions/gsd/tests/journal.test.ts +82 -127
  274. package/src/resources/extensions/gsd/tests/manifest-status.test.ts +73 -82
  275. package/src/resources/extensions/gsd/tests/service-tier.test.ts +30 -1
  276. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +56 -3
  277. package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +151 -0
  278. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +156 -263
  279. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +35 -78
  280. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +81 -74
  281. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +1 -2
  282. package/src/resources/extensions/gsd/worktree-resolver.ts +2 -3
  283. package/src/resources/extensions/gsd/worktree.ts +2 -2
  284. package/src/resources/extensions/mcp-client/index.ts +5 -1
  285. package/src/resources/extensions/search-the-web/tool-search.ts +3 -3
  286. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-e07acdb7dd069836.js +0 -1
  287. package/dist/web/standalone/.next/static/chunks/app/layout-745c6ed5fea5fb06.js +0 -1
  288. package/dist/web/standalone/.next/static/chunks/app/page-801b53eff6e83579.js +0 -1
  289. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-e6255954dccfcf0a.js +0 -1
  290. /package/dist/web/standalone/.next/static/{QSuuaY2tP8h_zzhHcVxNj → 5ULZcR9XhHFzlAZFiSKRl}/_buildManifest.js +0 -0
  291. /package/dist/web/standalone/.next/static/{QSuuaY2tP8h_zzhHcVxNj → 5ULZcR9XhHFzlAZFiSKRl}/_ssgManifest.js +0 -0
@@ -1519,6 +1519,13 @@ export class InteractiveMode {
1519
1519
  options: string[],
1520
1520
  opts?: ExtensionUIDialogOptions,
1521
1521
  ): Promise<string | undefined> {
1522
+ // If a previous selector is still active, dispose it before creating a
1523
+ // new one. This avoids leaking the previous promise and DOM state when
1524
+ // showExtensionSelector is called rapidly.
1525
+ if (this.extensionSelector) {
1526
+ this.hideExtensionSelector();
1527
+ }
1528
+
1522
1529
  return new Promise((resolve) => {
1523
1530
  if (opts?.signal?.aborted) {
1524
1531
  resolve(undefined);
@@ -2331,18 +2338,24 @@ export class InteractiveMode {
2331
2338
  const ignoreSigint = () => {};
2332
2339
  process.on("SIGINT", ignoreSigint);
2333
2340
 
2334
- // Set up handler to restore TUI when resumed
2335
- process.once("SIGCONT", () => {
2336
- process.removeListener("SIGINT", ignoreSigint);
2337
- this.ui.start();
2338
- this.ui.requestRender(true);
2339
- });
2341
+ try {
2342
+ // Set up handler to restore TUI when resumed
2343
+ process.once("SIGCONT", () => {
2344
+ process.removeListener("SIGINT", ignoreSigint);
2345
+ this.ui.start();
2346
+ this.ui.requestRender(true);
2347
+ });
2340
2348
 
2341
- // Stop the TUI (restore terminal to normal mode)
2342
- this.ui.stop();
2349
+ // Stop the TUI (restore terminal to normal mode)
2350
+ this.ui.stop();
2343
2351
 
2344
- // Send SIGTSTP to process group (pid=0 means all processes in group)
2345
- process.kill(0, "SIGTSTP");
2352
+ // Send SIGTSTP to process group (pid=0 means all processes in group)
2353
+ process.kill(0, "SIGTSTP");
2354
+ } catch {
2355
+ // If suspend fails (e.g. SIGTSTP not supported), ensure the
2356
+ // SIGINT listener doesn't leak.
2357
+ process.removeListener("SIGINT", ignoreSigint);
2358
+ }
2346
2359
  }
2347
2360
 
2348
2361
  private async handleFollowUp(): Promise<void> {
@@ -2460,7 +2473,14 @@ export class InteractiveMode {
2460
2473
  // Determine editor (respect $VISUAL, then $EDITOR)
2461
2474
  const editorCmd = process.env.VISUAL || process.env.EDITOR;
2462
2475
  if (!editorCmd) {
2463
- this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
2476
+ let msg = "No editor configured. Set $VISUAL or $EDITOR environment variable.";
2477
+ if (process.env.TERM_PROGRAM === "iTerm.app") {
2478
+ msg +=
2479
+ "\n\nTip: If you meant to open the GSD dashboard (Ctrl+Alt+G), set Left Option Key to" +
2480
+ " \"Esc+\" in iTerm2 → Profiles → Keys. With the default \"Normal\" setting," +
2481
+ " Ctrl+Alt+G sends Ctrl+G instead.";
2482
+ }
2483
+ this.showWarning(msg);
2464
2484
  return;
2465
2485
  }
2466
2486
 
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittercowboy/gsd",
3
- "version": "2.42.0",
3
+ "version": "2.43.0",
4
4
  "piConfig": {
5
5
  "name": "gsd",
6
6
  "configDir": ".gsd"
@@ -0,0 +1,122 @@
1
+ /**
2
+ * async-bash-timeout.test.ts — Tests for async_bash timeout behavior.
3
+ *
4
+ * Reproduces issue #2186: when an async bash job exceeds its timeout and
5
+ * the child process ignores SIGTERM, the promise hangs indefinitely.
6
+ * The fix adds a SIGKILL fallback and a hard deadline that force-resolves
7
+ * the promise so execution can continue.
8
+ */
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { createAsyncBashTool } from "./async-bash-tool.ts";
13
+ import { AsyncJobManager } from "./job-manager.ts";
14
+
15
+ function getTextFromResult(result: { content: Array<{ type: string; text?: string }> }): string {
16
+ return result.content.map((c) => c.text ?? "").join("\n");
17
+ }
18
+
19
+ const noopSignal = new AbortController().signal;
20
+
21
+ test("async_bash with timeout resolves even if process ignores SIGTERM", async () => {
22
+ const manager = new AsyncJobManager();
23
+ const tool = createAsyncBashTool(() => manager, () => process.cwd());
24
+
25
+ // Start a job that traps SIGTERM (ignores it), with a 2s timeout.
26
+ // The process installs a SIGTERM trap and sleeps for 60s.
27
+ // Before the fix, this would hang forever because SIGTERM is ignored
28
+ // and the close event never fires.
29
+ const result = await tool.execute(
30
+ "tc-timeout",
31
+ {
32
+ command: "trap '' TERM; sleep 60",
33
+ timeout: 2,
34
+ label: "sigterm-resistant",
35
+ },
36
+ noopSignal,
37
+ () => {},
38
+ undefined as never,
39
+ );
40
+
41
+ const text = getTextFromResult(result);
42
+ assert.match(text, /sigterm-resistant/);
43
+
44
+ const jobId = text.match(/\*\*(bg_[a-f0-9]+)\*\*/)?.[1];
45
+ assert.ok(jobId, "Should have returned a job ID");
46
+
47
+ // Now await the job — it should resolve within a reasonable time
48
+ // (timeout 2s + SIGKILL grace 5s + buffer = well under 15s)
49
+ const start = Date.now();
50
+ const job = manager.getJob(jobId)!;
51
+ assert.ok(job, "Job should exist");
52
+
53
+ await Promise.race([
54
+ job.promise,
55
+ new Promise<never>((_, reject) => {
56
+ const t = setTimeout(() => reject(new Error(
57
+ `Job promise hung for ${Date.now() - start}ms — ` +
58
+ `this is the bug from issue #2186: timeout hangs indefinitely`,
59
+ )), 15_000);
60
+ if (typeof t === "object" && "unref" in t) t.unref();
61
+ }),
62
+ ]);
63
+
64
+ const elapsed = Date.now() - start;
65
+ // Should have resolved well within 15s (timeout 2s + kill grace ~5s)
66
+ assert.ok(elapsed < 15_000, `Job took ${elapsed}ms — expected <15s`);
67
+
68
+ // Job should have completed (resolved, not rejected) with timeout message
69
+ assert.ok(
70
+ job.status === "completed" || job.status === "failed",
71
+ `Job status should be completed or failed, got: ${job.status}`,
72
+ );
73
+
74
+ if (job.status === "completed") {
75
+ assert.ok(
76
+ job.resultText?.includes("timed out") || job.resultText?.includes("Timed out"),
77
+ `Result should mention timeout, got: ${job.resultText}`,
78
+ );
79
+ }
80
+
81
+ manager.shutdown();
82
+ });
83
+
84
+ test("async_bash with timeout resolves normally when process exits on SIGTERM", async () => {
85
+ const manager = new AsyncJobManager();
86
+ const tool = createAsyncBashTool(() => manager, () => process.cwd());
87
+
88
+ // Start a normal sleep that will die on SIGTERM, with a 1s timeout
89
+ const result = await tool.execute(
90
+ "tc-normal-timeout",
91
+ {
92
+ command: "sleep 60",
93
+ timeout: 1,
94
+ label: "normal-timeout",
95
+ },
96
+ noopSignal,
97
+ () => {},
98
+ undefined as never,
99
+ );
100
+
101
+ const text = getTextFromResult(result);
102
+ const jobId = text.match(/\*\*(bg_[a-f0-9]+)\*\*/)?.[1];
103
+ assert.ok(jobId, "Should have returned a job ID");
104
+
105
+ const job = manager.getJob(jobId)!;
106
+ const start = Date.now();
107
+
108
+ await Promise.race([
109
+ job.promise,
110
+ new Promise<never>((_, reject) => {
111
+ const t = setTimeout(() => reject(new Error("Job hung")), 10_000);
112
+ if (typeof t === "object" && "unref" in t) t.unref();
113
+ }),
114
+ ]);
115
+
116
+ const elapsed = Date.now() - start;
117
+ assert.ok(elapsed < 5_000, `Expected quick resolution after SIGTERM, took ${elapsed}ms`);
118
+ assert.equal(job.status, "completed");
119
+ assert.ok(job.resultText?.includes("timed out"), `Should mention timeout: ${job.resultText}`);
120
+
121
+ manager.shutdown();
122
+ });
@@ -109,6 +109,10 @@ function executeBashInBackground(
109
109
  timeout?: number,
110
110
  ): Promise<string> {
111
111
  return new Promise<string>((resolve, reject) => {
112
+ let settled = false;
113
+ const safeResolve = (value: string) => { if (!settled) { settled = true; resolve(value); } };
114
+ const safeReject = (err: unknown) => { if (!settled) { settled = true; reject(err); } };
115
+
112
116
  const { shell, args } = getShellConfig();
113
117
  const resolvedCommand = sanitizeCommand(command);
114
118
 
@@ -121,11 +125,39 @@ function executeBashInBackground(
121
125
 
122
126
  let timedOut = false;
123
127
  let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
128
+ let sigkillHandle: ReturnType<typeof setTimeout> | undefined;
129
+ let hardDeadlineHandle: ReturnType<typeof setTimeout> | undefined;
130
+
131
+ /** Grace period (ms) between SIGTERM and SIGKILL. */
132
+ const SIGKILL_GRACE_MS = 5_000;
133
+ /** Hard deadline (ms) after SIGKILL to force-resolve the promise. */
134
+ const HARD_DEADLINE_MS = 3_000;
124
135
 
125
136
  if (timeout !== undefined && timeout > 0) {
126
137
  timeoutHandle = setTimeout(() => {
127
138
  timedOut = true;
128
139
  if (child.pid) killTree(child.pid);
140
+
141
+ // If the process ignores SIGTERM, escalate to SIGKILL
142
+ sigkillHandle = setTimeout(() => {
143
+ if (child.pid) {
144
+ try { process.kill(-child.pid, "SIGKILL"); } catch { /* ignore */ }
145
+ try { process.kill(child.pid, "SIGKILL"); } catch { /* ignore */ }
146
+ }
147
+
148
+ // Hard deadline: if even SIGKILL doesn't trigger 'close',
149
+ // force-resolve so the job doesn't hang forever (#2186).
150
+ hardDeadlineHandle = setTimeout(() => {
151
+ const output = Buffer.concat(chunks).toString("utf-8");
152
+ safeResolve(
153
+ output
154
+ ? `${output}\n\nCommand timed out after ${timeout} seconds (force-killed)`
155
+ : `Command timed out after ${timeout} seconds (force-killed)`,
156
+ );
157
+ }, HARD_DEADLINE_MS);
158
+ if (typeof hardDeadlineHandle === "object" && "unref" in hardDeadlineHandle) hardDeadlineHandle.unref();
159
+ }, SIGKILL_GRACE_MS);
160
+ if (typeof sigkillHandle === "object" && "unref" in sigkillHandle) sigkillHandle.unref();
129
161
  }, timeout * 1000);
130
162
  }
131
163
 
@@ -168,24 +200,28 @@ function executeBashInBackground(
168
200
 
169
201
  child.on("error", (err) => {
170
202
  if (timeoutHandle) clearTimeout(timeoutHandle);
203
+ if (sigkillHandle) clearTimeout(sigkillHandle);
204
+ if (hardDeadlineHandle) clearTimeout(hardDeadlineHandle);
171
205
  signal.removeEventListener("abort", onAbort);
172
- reject(err);
206
+ safeReject(err);
173
207
  });
174
208
 
175
209
  child.on("close", (code) => {
176
210
  if (timeoutHandle) clearTimeout(timeoutHandle);
211
+ if (sigkillHandle) clearTimeout(sigkillHandle);
212
+ if (hardDeadlineHandle) clearTimeout(hardDeadlineHandle);
177
213
  signal.removeEventListener("abort", onAbort);
178
214
  if (spillStream) spillStream.end();
179
215
 
180
216
  if (signal.aborted) {
181
217
  const output = Buffer.concat(chunks).toString("utf-8");
182
- resolve(output ? `${output}\n\nCommand aborted` : "Command aborted");
218
+ safeResolve(output ? `${output}\n\nCommand aborted` : "Command aborted");
183
219
  return;
184
220
  }
185
221
 
186
222
  if (timedOut) {
187
223
  const output = Buffer.concat(chunks).toString("utf-8");
188
- resolve(output ? `${output}\n\nCommand timed out after ${timeout} seconds` : `Command timed out after ${timeout} seconds`);
224
+ safeResolve(output ? `${output}\n\nCommand timed out after ${timeout} seconds` : `Command timed out after ${timeout} seconds`);
189
225
  return;
190
226
  }
191
227
 
@@ -208,7 +244,7 @@ function executeBashInBackground(
208
244
  text += `\n\nCommand exited with code ${code}`;
209
245
  }
210
246
 
211
- resolve(text);
247
+ safeResolve(text);
212
248
  });
213
249
  });
214
250
  }
@@ -118,3 +118,50 @@ test("await_job returns not-found message for invalid job IDs", async () => {
118
118
 
119
119
  manager.shutdown();
120
120
  });
121
+
122
+ test("await_job marks jobs as awaited to suppress follow-up delivery (#2248)", async () => {
123
+ const followUps: string[] = [];
124
+ const manager = new AsyncJobManager({
125
+ onJobComplete: (job) => {
126
+ if (!job.awaited) followUps.push(job.id);
127
+ },
128
+ });
129
+ const tool = createAwaitTool(() => manager);
130
+
131
+ // Register a job that completes in 50ms
132
+ const jobId = manager.register("bash", "awaited-job", async () => {
133
+ return new Promise<string>((resolve) => setTimeout(() => resolve("result"), 50));
134
+ });
135
+
136
+ // await_job consumes the result — should mark as awaited before promise resolves
137
+ await tool.execute("tc7", { jobs: [jobId] }, noopSignal, () => {}, undefined as never);
138
+
139
+ // Give the onJobComplete callback a tick to fire
140
+ await new Promise((r) => setTimeout(r, 50));
141
+
142
+ assert.equal(followUps.length, 0, "onJobComplete should not deliver follow-up for awaited jobs");
143
+
144
+ manager.shutdown();
145
+ });
146
+
147
+ test("unawaited jobs still get follow-up delivery (#2248)", async () => {
148
+ const followUps: string[] = [];
149
+ const manager = new AsyncJobManager({
150
+ onJobComplete: (job) => {
151
+ if (!job.awaited) followUps.push(job.id);
152
+ },
153
+ });
154
+
155
+ // Register a fire-and-forget job
156
+ const jobId = manager.register("bash", "fire-and-forget", async () => "done");
157
+ const job = manager.getJob(jobId)!;
158
+ await job.promise;
159
+
160
+ // Give the callback a tick
161
+ await new Promise((r) => setTimeout(r, 50));
162
+
163
+ assert.equal(followUps.length, 1, "onJobComplete should deliver follow-up for unawaited jobs");
164
+ assert.equal(followUps[0], jobId);
165
+
166
+ manager.shutdown();
167
+ });
@@ -66,6 +66,11 @@ export function createAwaitTool(getManager: () => AsyncJobManager): ToolDefiniti
66
66
  }
67
67
  }
68
68
 
69
+ // Mark all watched jobs as awaited upfront so the onJobComplete
70
+ // callback (which fires synchronously in the promise .then()) knows
71
+ // to suppress the follow-up message.
72
+ for (const j of watched) j.awaited = true;
73
+
69
74
  // If all watched jobs are already done, return immediately
70
75
  const running = watched.filter((j) => j.status === "running");
71
76
  if (running.length === 0) {
@@ -42,6 +42,7 @@ export default function AsyncJobs(pi: ExtensionAPI) {
42
42
 
43
43
  manager = new AsyncJobManager({
44
44
  onJobComplete: (job) => {
45
+ if (job.awaited) return;
45
46
  const statusEmoji = job.status === "completed" ? "done" : "error";
46
47
  const elapsed = ((Date.now() - job.startTime) / 1000).toFixed(1);
47
48
  const output = job.status === "completed"
@@ -22,6 +22,8 @@ export interface Job {
22
22
  promise: Promise<void>;
23
23
  resultText?: string;
24
24
  errorText?: string;
25
+ /** Set by await_job when results are consumed. Suppresses follow-up delivery. */
26
+ awaited?: boolean;
25
27
  }
26
28
 
27
29
  export interface JobManagerOptions {
@@ -109,7 +109,6 @@ export interface LoopDeps {
109
109
  captureIntegrationBranch: (
110
110
  basePath: string,
111
111
  mid: string,
112
- opts?: { commitDocs?: boolean },
113
112
  ) => void;
114
113
  getIsolationMode: () => string;
115
114
  getCurrentBranch: (basePath: string) => string;
@@ -261,9 +261,7 @@ export async function runPreDispatch(
261
261
 
262
262
  if (mid) {
263
263
  if (deps.getIsolationMode() !== "none") {
264
- deps.captureIntegrationBranch(s.basePath, mid, {
265
- commitDocs: prefs?.git?.commit_docs,
266
- });
264
+ deps.captureIntegrationBranch(s.basePath, mid);
267
265
  }
268
266
  deps.resolver.enterMilestone(mid, ctx.ui);
269
267
  } else {
@@ -420,8 +420,6 @@ export function buildSkillActivationBlock(params: {
420
420
  params.sliceTitle,
421
421
  params.taskId,
422
422
  params.taskTitle,
423
- ...(params.extraContext ?? []),
424
- params.taskPlanContent ?? undefined,
425
423
  );
426
424
 
427
425
  const visibleSkills = (typeof getLoadedSkills === 'function' ? getLoadedSkills() : []).filter(skill => !skill.disableModelInvocation);
@@ -452,12 +450,6 @@ export function buildSkillActivationBlock(params: {
452
450
  }
453
451
  }
454
452
 
455
- for (const skill of visibleSkills) {
456
- if (skillMatchesContext(skill, contextTokens)) {
457
- matched.add(normalizeSkillReference(skill.name));
458
- }
459
- }
460
-
461
453
  const ordered = [...matched]
462
454
  .filter(name => installedNames.has(name) && !avoided.has(name))
463
455
  .sort();
@@ -983,11 +975,7 @@ export async function buildPlanSlicePrompt(
983
975
  const executorContextConstraints = formatExecutorConstraints();
984
976
 
985
977
  const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
986
- const prefs = loadEffectiveGSDPreferences();
987
- const commitDocsEnabled = prefs?.preferences?.git?.commit_docs !== false;
988
- const commitInstruction = commitDocsEnabled
989
- ? `Commit the plan files only: \`git add ${relSlicePath(base, mid, sid)}/ .gsd/DECISIONS.md .gitignore && git commit -m "docs(${sid}): add slice plan"\`. Do not stage .gsd/STATE.md or other runtime files — the system manages those.`
990
- : "Do not commit — planning docs are not tracked in git for this project.";
978
+ const commitInstruction = "Do not commit — .gsd/ planning docs are managed externally and not tracked in git.";
991
979
  return loadPrompt("plan-slice", {
992
980
  workingDirectory: base,
993
981
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
@@ -1485,11 +1473,7 @@ export async function buildReassessRoadmapPrompt(
1485
1473
  // Non-fatal — captures module may not be available
1486
1474
  }
1487
1475
 
1488
- const reassessPrefs = loadEffectiveGSDPreferences();
1489
- const reassessCommitDocsEnabled = reassessPrefs?.preferences?.git?.commit_docs !== false;
1490
- const reassessCommitInstruction = reassessCommitDocsEnabled
1491
- ? `Commit: \`docs(${mid}): reassess roadmap after ${completedSliceId}\`. Stage only the .gsd/milestones/ files you changed — do not stage .gsd/STATE.md or other runtime files.`
1492
- : "Do not commit — planning docs are not tracked in git for this project.";
1476
+ const reassessCommitInstruction = "Do not commit — .gsd/ planning docs are managed externally and not tracked in git.";
1493
1477
 
1494
1478
  return loadPrompt("reassess-roadmap", {
1495
1479
  workingDirectory: base,
@@ -167,22 +167,19 @@ export async function bootstrapAutoSession(
167
167
  // ensureGitignore checks for git-tracked .gsd/ files and skips the
168
168
  // ".gsd" pattern if the project intentionally tracks .gsd/ in git.
169
169
  const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
170
- const commitDocs = gitPrefs?.commit_docs;
171
170
  const manageGitignore = gitPrefs?.manage_gitignore;
172
- ensureGitignore(base, { commitDocs, manageGitignore });
171
+ ensureGitignore(base, { manageGitignore });
173
172
  if (manageGitignore !== false) untrackRuntimeFiles(base);
174
173
 
175
174
  // Bootstrap .gsd/ if it doesn't exist
176
175
  const gsdDir = join(base, ".gsd");
177
176
  if (!existsSync(gsdDir)) {
178
177
  mkdirSync(join(gsdDir, "milestones"), { recursive: true });
179
- if (commitDocs !== false) {
180
- try {
181
- nativeAddAll(base);
182
- nativeCommit(base, "chore: init gsd");
183
- } catch {
184
- /* nothing to commit */
185
- }
178
+ try {
179
+ nativeAddAll(base);
180
+ nativeCommit(base, "chore: init gsd");
181
+ } catch {
182
+ /* nothing to commit */
186
183
  }
187
184
  }
188
185
 
@@ -487,7 +484,7 @@ export async function bootstrapAutoSession(
487
484
  // Capture integration branch
488
485
  if (s.currentMilestoneId) {
489
486
  if (getIsolationMode() !== "none") {
490
- captureIntegrationBranch(base, s.currentMilestoneId, { commitDocs });
487
+ captureIntegrationBranch(base, s.currentMilestoneId);
491
488
  }
492
489
  setActiveMilestoneId(base, s.currentMilestoneId);
493
490
  }
@@ -20,21 +20,27 @@ import { saveActivityLog } from "../activity-log.js";
20
20
  // printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
21
21
  let isFirstSession = true;
22
22
 
23
+ async function syncServiceTierStatus(ctx: ExtensionContext): Promise<void> {
24
+ const { getEffectiveServiceTier, formatServiceTierFooterStatus } = await import("../service-tier.js");
25
+ ctx.ui.setStatus("gsd-fast", formatServiceTierFooterStatus(getEffectiveServiceTier(), ctx.model?.id));
26
+ }
27
+
23
28
  export function registerHooks(pi: ExtensionAPI): void {
24
29
  pi.on("session_start", async (_event, ctx) => {
25
30
  resetWriteGateState();
26
31
  resetToolCallLoopGuard();
32
+ await syncServiceTierStatus(ctx);
27
33
  if (isFirstSession) {
28
34
  isFirstSession = false;
29
35
  } else {
30
36
  try {
31
37
  const gsdBinPath = process.env.GSD_BIN_PATH;
32
38
  if (gsdBinPath) {
33
- const { dirname } = await import('node:path');
39
+ const { dirname } = await import("node:path");
34
40
  const { printWelcomeScreen } = await import(
35
- join(dirname(gsdBinPath), 'welcome-screen.js')
41
+ join(dirname(gsdBinPath), "welcome-screen.js")
36
42
  ) as { printWelcomeScreen: (opts: { version: string; modelName?: string; provider?: string }) => void };
37
- printWelcomeScreen({ version: process.env.GSD_VERSION || '0.0.0' });
43
+ printWelcomeScreen({ version: process.env.GSD_VERSION || "0.0.0" });
38
44
  }
39
45
  } catch { /* non-fatal */ }
40
46
  }
@@ -192,8 +198,11 @@ export function registerHooks(pi: ExtensionAPI): void {
192
198
  markToolEnd(event.toolCallId);
193
199
  });
194
200
 
201
+ pi.on("model_select", async (_event, ctx) => {
202
+ await syncServiceTierStatus(ctx);
203
+ });
204
+
195
205
  pi.on("before_provider_request", async (event) => {
196
- if (!isAutoActive()) return;
197
206
  const modelId = event.model?.id;
198
207
  if (!modelId) return;
199
208
  const { getEffectiveServiceTier, supportsServiceTier } = await import("../service-tier.js");
@@ -205,4 +214,3 @@ export function registerHooks(pi: ExtensionAPI): void {
205
214
  return payload;
206
215
  });
207
216
  }
208
-
@@ -87,6 +87,18 @@ export const PROJECT_FILES = [
87
87
  "mix.exs",
88
88
  "deno.json",
89
89
  "deno.jsonc",
90
+ // .NET
91
+ ".sln",
92
+ ".csproj",
93
+ "Directory.Build.props",
94
+ // Git submodules
95
+ ".gitmodules",
96
+ // Xcode
97
+ "project.yml",
98
+ ".xcodeproj",
99
+ ".xcworkspace",
100
+ // Docker
101
+ "Dockerfile",
90
102
  ] as const;
91
103
 
92
104
  const LANGUAGE_MAP: Record<string, string> = {
@@ -106,6 +118,13 @@ const LANGUAGE_MAP: Record<string, string> = {
106
118
  "mix.exs": "elixir",
107
119
  "deno.json": "typescript/deno",
108
120
  "deno.jsonc": "typescript/deno",
121
+ ".sln": "dotnet",
122
+ ".csproj": "dotnet",
123
+ "Directory.Build.props": "dotnet",
124
+ "project.yml": "swift/xcode",
125
+ ".xcodeproj": "swift/xcode",
126
+ ".xcworkspace": "swift/xcode",
127
+ "Dockerfile": "docker",
109
128
  };
110
129
 
111
130
  const MONOREPO_MARKERS = [
@@ -2,7 +2,7 @@ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, rmSync,
2
2
  import { basename, dirname, join, sep } from "node:path";
3
3
 
4
4
  import type { DoctorIssue, DoctorIssueCode } from "./doctor-types.js";
5
- import { readRepoMeta, externalProjectsRoot } from "./repo-identity.js";
5
+ import { readRepoMeta, externalProjectsRoot, cleanNumberedGsdVariants } from "./repo-identity.js";
6
6
  import { loadFile, parseRoadmap } from "./files.js";
7
7
  import { resolveMilestoneFile, milestonesDir, gsdRoot, resolveGsdRootFile, relGsdRootFile } from "./paths.js";
8
8
  import { deriveState, isMilestoneComplete } from "./state.js";
@@ -776,6 +776,37 @@ export async function checkRuntimeHealth(
776
776
  // Non-fatal — external state check failed
777
777
  }
778
778
 
779
+ // ── Numbered .gsd collision variants (#2205) ───────────────────────────
780
+ // macOS APFS can create ".gsd 2", ".gsd 3" etc. when a directory blocks
781
+ // symlink creation. These must be removed so the canonical .gsd is used.
782
+ try {
783
+ const variantPattern = /^\.gsd \d+$/;
784
+ const entries = readdirSync(basePath);
785
+ const variants = entries.filter(e => variantPattern.test(e));
786
+ if (variants.length > 0) {
787
+ for (const v of variants) {
788
+ issues.push({
789
+ severity: "warning",
790
+ code: "numbered_gsd_variant",
791
+ scope: "project",
792
+ unitId: "project",
793
+ message: `Found macOS collision variant "${v}" — this can cause GSD state to appear deleted.`,
794
+ file: v,
795
+ fixable: true,
796
+ });
797
+ }
798
+
799
+ if (shouldFix("numbered_gsd_variant")) {
800
+ const removed = cleanNumberedGsdVariants(basePath);
801
+ for (const name of removed) {
802
+ fixesApplied.push(`removed numbered .gsd variant: ${name}`);
803
+ }
804
+ }
805
+ }
806
+ } catch {
807
+ // Non-fatal — variant check failed
808
+ }
809
+
779
810
  // ── Metrics ledger integrity ───────────────────────────────────────────
780
811
  try {
781
812
  const metricsPath = join(root, "metrics.json");
@@ -305,11 +305,24 @@ function checkOptionalProviders(): ProviderCheckResult[] {
305
305
  const optional = ["brave", "tavily", "jina", "context7"] as const;
306
306
  const results: ProviderCheckResult[] = [];
307
307
 
308
+ // Determine which search providers are configured so we can suppress
309
+ // "not configured" noise for alternative search providers when at least
310
+ // one is already active (e.g. don't warn about missing BRAVE_API_KEY
311
+ // when Tavily is configured).
312
+ const searchProviderIds = ["brave", "tavily"] as const;
313
+ const hasAnySearchProvider = searchProviderIds.some(id => resolveKey(id).found);
314
+
308
315
  for (const providerId of optional) {
309
316
  const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
310
317
  if (!info) continue;
311
318
 
312
319
  const lookup = resolveKey(providerId);
320
+
321
+ // Skip unconfigured search providers when another search provider is active
322
+ if (!lookup.found && hasAnySearchProvider && info.category === "search") {
323
+ continue;
324
+ }
325
+
313
326
  results.push({
314
327
  name: providerId,
315
328
  label: info.label,
@@ -33,6 +33,7 @@ export type DoctorIssueCode =
33
33
  | "unresolvable_dependency"
34
34
  | "failed_migration"
35
35
  | "broken_symlink"
36
+ | "numbered_gsd_variant"
36
37
  // Environment health checks (#1221)
37
38
  | "env_node_version"
38
39
  | "env_dependencies"