gsd-pi 2.20.0 → 2.22.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 (279) hide show
  1. package/README.md +12 -0
  2. package/dist/cli.js +21 -18
  3. package/dist/help-text.d.ts +2 -0
  4. package/dist/help-text.js +47 -0
  5. package/dist/loader.js +2 -16
  6. package/dist/mcp-server.d.ts +18 -0
  7. package/dist/mcp-server.js +53 -0
  8. package/dist/onboarding.d.ts +0 -6
  9. package/dist/onboarding.js +0 -28
  10. package/dist/resources/agents/javascript-pro.md +280 -0
  11. package/dist/resources/agents/typescript-pro.md +255 -0
  12. package/dist/resources/extensions/bg-shell/index.ts +14 -2
  13. package/dist/resources/extensions/bg-shell/utilities.ts +14 -0
  14. package/dist/resources/extensions/browser-tools/{core.js → core.ts} +329 -190
  15. package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -1
  16. package/dist/resources/extensions/gsd/auto-prompts.ts +42 -1
  17. package/dist/resources/extensions/gsd/auto-worktree.ts +35 -2
  18. package/dist/resources/extensions/gsd/auto.ts +116 -10
  19. package/dist/resources/extensions/gsd/claude-import.ts +656 -0
  20. package/dist/resources/extensions/gsd/collision-diagnostics.ts +332 -0
  21. package/dist/resources/extensions/gsd/commands.ts +60 -7
  22. package/dist/resources/extensions/gsd/diff-context.ts +220 -0
  23. package/dist/resources/extensions/gsd/docs/claude-marketplace-import.md +214 -0
  24. package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -1
  25. package/dist/resources/extensions/gsd/doctor.ts +12 -4
  26. package/dist/resources/extensions/gsd/file-watcher.ts +97 -0
  27. package/dist/resources/extensions/gsd/files.ts +1 -1
  28. package/dist/resources/extensions/gsd/forensics.ts +596 -0
  29. package/dist/resources/extensions/gsd/git-service.ts +2 -1
  30. package/dist/resources/extensions/gsd/github-client.ts +235 -0
  31. package/dist/resources/extensions/gsd/gitignore.ts +1 -0
  32. package/dist/resources/extensions/gsd/marketplace-discovery.ts +507 -0
  33. package/dist/resources/extensions/gsd/mcp-server.ts +87 -0
  34. package/dist/resources/extensions/gsd/namespaced-registry.ts +467 -0
  35. package/dist/resources/extensions/gsd/namespaced-resolver.ts +307 -0
  36. package/dist/resources/extensions/gsd/plugin-importer.ts +410 -0
  37. package/dist/resources/extensions/gsd/preferences.ts +14 -3
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  39. package/dist/resources/extensions/gsd/prompts/forensics.md +71 -0
  40. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  41. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  42. package/dist/resources/extensions/gsd/prompts/system.md +12 -3
  43. package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
  44. package/dist/resources/extensions/gsd/session-forensics.ts +5 -5
  45. package/dist/resources/extensions/gsd/tests/claude-import-tui.test.ts +351 -0
  46. package/dist/resources/extensions/gsd/tests/collision-diagnostics.test.ts +705 -0
  47. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +13 -0
  48. package/dist/resources/extensions/gsd/tests/diff-context.test.ts +136 -0
  49. package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +118 -0
  50. package/dist/resources/extensions/gsd/tests/marketplace-discovery.test.ts +202 -0
  51. package/dist/resources/extensions/gsd/tests/marketplace-test-fixtures.ts +91 -0
  52. package/dist/resources/extensions/gsd/tests/namespaced-registry.test.ts +1027 -0
  53. package/dist/resources/extensions/gsd/tests/namespaced-resolver.test.ts +671 -0
  54. package/dist/resources/extensions/gsd/tests/none-mode-gates.test.ts +105 -0
  55. package/dist/resources/extensions/gsd/tests/plugin-importer-live.test.ts +481 -0
  56. package/dist/resources/extensions/gsd/tests/plugin-importer.test.ts +1383 -0
  57. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +21 -2
  58. package/dist/resources/extensions/gsd/tests/preferences-schema-validation.test.ts +8 -1
  59. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +11 -0
  60. package/dist/resources/extensions/gsd/tests/worktree-bugfix.test.ts +120 -0
  61. package/dist/resources/extensions/gsd/token-counter.ts +45 -0
  62. package/dist/resources/extensions/gsd/visualizer-data.ts +2 -2
  63. package/dist/resources/extensions/gsd/worktree-manager.ts +29 -1
  64. package/dist/resources/extensions/gsd/worktree.ts +3 -0
  65. package/dist/resources/extensions/mcporter/index.ts +90 -7
  66. package/dist/resources/extensions/search-the-web/native-search.ts +2 -0
  67. package/dist/resources/extensions/search-the-web/tool-fetch-page.ts +9 -1
  68. package/dist/resources/extensions/search-the-web/url-utils.ts +35 -1
  69. package/dist/resources/extensions/shared/terminal.ts +1 -1
  70. package/dist/resources/extensions/universal-config/discovery.ts +4 -0
  71. package/dist/resources/extensions/universal-config/format.ts +35 -4
  72. package/dist/resources/extensions/universal-config/index.ts +5 -3
  73. package/dist/resources/extensions/universal-config/scanners.ts +65 -2
  74. package/dist/resources/extensions/universal-config/tests/discovery.test.ts +9 -1
  75. package/dist/resources/extensions/universal-config/tests/format.test.ts +22 -6
  76. package/dist/resources/extensions/universal-config/tests/scanners.test.ts +18 -0
  77. package/dist/resources/extensions/universal-config/types.ts +20 -1
  78. package/dist/resources/skills/lint/SKILL.md +141 -0
  79. package/dist/resources/skills/review/SKILL.md +214 -0
  80. package/dist/resources/skills/test/SKILL.md +201 -0
  81. package/dist/tool-bootstrap.js +2 -1
  82. package/dist/wizard.js +2 -0
  83. package/package.json +12 -7
  84. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  85. package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -1
  86. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  87. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  88. package/packages/pi-ai/dist/providers/openai-responses.js +12 -1
  89. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  90. package/packages/pi-ai/node_modules/@smithy/node-http-handler/LICENSE +201 -0
  91. package/packages/pi-ai/node_modules/@smithy/node-http-handler/README.md +9 -0
  92. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-cjs/index.js +762 -0
  93. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/build-abort-error.js +19 -0
  94. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/constants.js +1 -0
  95. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/get-transformed-headers.js +9 -0
  96. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/index.js +3 -0
  97. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/node-http-handler.js +230 -0
  98. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/node-http2-connection-manager.js +87 -0
  99. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/node-http2-connection-pool.js +32 -0
  100. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/node-http2-handler.js +169 -0
  101. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/readable.mock.js +21 -0
  102. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/server.mock.js +88 -0
  103. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/set-connection-timeout.js +36 -0
  104. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/set-request-timeout.js +21 -0
  105. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/set-socket-keep-alive.js +22 -0
  106. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/set-socket-timeout.js +23 -0
  107. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/stream-collector/collector.js +8 -0
  108. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/stream-collector/index.js +41 -0
  109. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/stream-collector/readable.mock.js +21 -0
  110. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/timing.js +4 -0
  111. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/write-request-body.js +63 -0
  112. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/build-abort-error.d.ts +10 -0
  113. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/constants.d.ts +5 -0
  114. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/get-transformed-headers.d.ts +4 -0
  115. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/index.d.ts +3 -0
  116. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/node-http-handler.d.ts +46 -0
  117. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/node-http2-connection-manager.d.ts +24 -0
  118. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/node-http2-connection-pool.d.ts +12 -0
  119. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/node-http2-handler.d.ts +63 -0
  120. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/readable.mock.d.ts +13 -0
  121. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/server.mock.d.ts +12 -0
  122. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/set-connection-timeout.d.ts +2 -0
  123. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/set-request-timeout.d.ts +6 -0
  124. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/set-socket-keep-alive.d.ts +6 -0
  125. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/set-socket-timeout.d.ts +2 -0
  126. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/stream-collector/collector.d.ts +5 -0
  127. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/stream-collector/index.d.ts +6 -0
  128. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/stream-collector/readable.mock.d.ts +13 -0
  129. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/timing.d.ts +8 -0
  130. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/build-abort-error.d.ts +10 -0
  131. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/constants.d.ts +5 -0
  132. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/get-transformed-headers.d.ts +4 -0
  133. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/index.d.ts +3 -0
  134. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/node-http-handler.d.ts +46 -0
  135. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/node-http2-connection-manager.d.ts +24 -0
  136. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/node-http2-connection-pool.d.ts +12 -0
  137. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/node-http2-handler.d.ts +63 -0
  138. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/readable.mock.d.ts +13 -0
  139. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/server.mock.d.ts +12 -0
  140. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/set-connection-timeout.d.ts +2 -0
  141. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/set-request-timeout.d.ts +6 -0
  142. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/set-socket-keep-alive.d.ts +6 -0
  143. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/set-socket-timeout.d.ts +2 -0
  144. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/stream-collector/collector.d.ts +5 -0
  145. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/stream-collector/index.d.ts +6 -0
  146. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/stream-collector/readable.mock.d.ts +13 -0
  147. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/timing.d.ts +8 -0
  148. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/write-request-body.d.ts +12 -0
  149. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/write-request-body.d.ts +12 -0
  150. package/packages/pi-ai/node_modules/@smithy/node-http-handler/package.json +68 -0
  151. package/packages/pi-ai/package.json +3 -0
  152. package/packages/pi-ai/pnpm-lock.yaml +2022 -0
  153. package/packages/pi-ai/src/providers/azure-openai-responses.ts +12 -1
  154. package/packages/pi-ai/src/providers/openai-responses.ts +12 -1
  155. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts +1 -1
  156. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/extensions/index.js +1 -1
  158. package/packages/pi-coding-agent/dist/core/extensions/index.js.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +1 -0
  160. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  161. package/packages/pi-coding-agent/dist/core/extensions/loader.js +12 -1
  162. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  163. package/packages/pi-coding-agent/dist/core/extensions/loader.test.d.ts +2 -0
  164. package/packages/pi-coding-agent/dist/core/extensions/loader.test.d.ts.map +1 -0
  165. package/packages/pi-coding-agent/dist/core/extensions/loader.test.js +113 -0
  166. package/packages/pi-coding-agent/dist/core/extensions/loader.test.js.map +1 -0
  167. package/packages/pi-coding-agent/dist/core/extensions/project-trust.d.ts +4 -0
  168. package/packages/pi-coding-agent/dist/core/extensions/project-trust.d.ts.map +1 -0
  169. package/packages/pi-coding-agent/dist/core/extensions/project-trust.js +42 -0
  170. package/packages/pi-coding-agent/dist/core/extensions/project-trust.js.map +1 -0
  171. package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts +1 -0
  172. package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts.map +1 -1
  173. package/packages/pi-coding-agent/dist/core/resolve-config-value.js +18 -0
  174. package/packages/pi-coding-agent/dist/core/resolve-config-value.js.map +1 -1
  175. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.d.ts +2 -0
  176. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.d.ts.map +1 -0
  177. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +114 -0
  178. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -0
  179. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  180. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +8 -2
  181. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
  182. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts +2 -0
  183. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
  184. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +23 -9
  185. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
  186. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.d.ts +2 -0
  187. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.d.ts.map +1 -0
  188. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js +67 -0
  189. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js.map +1 -0
  190. package/packages/pi-coding-agent/package.json +1 -1
  191. package/packages/pi-coding-agent/pnpm-lock.yaml +454 -0
  192. package/packages/pi-coding-agent/scripts/copy-assets.cjs +24 -0
  193. package/packages/pi-coding-agent/src/core/extensions/index.ts +3 -0
  194. package/packages/pi-coding-agent/src/core/extensions/loader.test.ts +141 -0
  195. package/packages/pi-coding-agent/src/core/extensions/loader.ts +14 -1
  196. package/packages/pi-coding-agent/src/core/extensions/project-trust.ts +51 -0
  197. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +132 -0
  198. package/packages/pi-coding-agent/src/core/resolve-config-value.ts +20 -0
  199. package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +7 -2
  200. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts +98 -0
  201. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +24 -9
  202. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  203. package/packages/pi-tui/dist/tui.js +2 -2
  204. package/packages/pi-tui/dist/tui.js.map +1 -1
  205. package/packages/pi-tui/package.json +3 -1
  206. package/packages/pi-tui/src/tui.ts +2 -2
  207. package/src/resources/agents/javascript-pro.md +280 -0
  208. package/src/resources/agents/typescript-pro.md +255 -0
  209. package/src/resources/extensions/bg-shell/index.ts +14 -2
  210. package/src/resources/extensions/bg-shell/utilities.ts +14 -0
  211. package/src/resources/extensions/browser-tools/{core.js → core.ts} +329 -190
  212. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -1
  213. package/src/resources/extensions/gsd/auto-prompts.ts +42 -1
  214. package/src/resources/extensions/gsd/auto-worktree.ts +35 -2
  215. package/src/resources/extensions/gsd/auto.ts +116 -10
  216. package/src/resources/extensions/gsd/claude-import.ts +656 -0
  217. package/src/resources/extensions/gsd/collision-diagnostics.ts +332 -0
  218. package/src/resources/extensions/gsd/commands.ts +60 -7
  219. package/src/resources/extensions/gsd/diff-context.ts +220 -0
  220. package/src/resources/extensions/gsd/docs/claude-marketplace-import.md +214 -0
  221. package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -1
  222. package/src/resources/extensions/gsd/doctor.ts +12 -4
  223. package/src/resources/extensions/gsd/file-watcher.ts +97 -0
  224. package/src/resources/extensions/gsd/files.ts +1 -1
  225. package/src/resources/extensions/gsd/forensics.ts +596 -0
  226. package/src/resources/extensions/gsd/git-service.ts +2 -1
  227. package/src/resources/extensions/gsd/github-client.ts +235 -0
  228. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  229. package/src/resources/extensions/gsd/marketplace-discovery.ts +507 -0
  230. package/src/resources/extensions/gsd/mcp-server.ts +87 -0
  231. package/src/resources/extensions/gsd/namespaced-registry.ts +467 -0
  232. package/src/resources/extensions/gsd/namespaced-resolver.ts +307 -0
  233. package/src/resources/extensions/gsd/plugin-importer.ts +410 -0
  234. package/src/resources/extensions/gsd/preferences.ts +14 -3
  235. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  236. package/src/resources/extensions/gsd/prompts/forensics.md +71 -0
  237. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  238. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  239. package/src/resources/extensions/gsd/prompts/system.md +12 -3
  240. package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
  241. package/src/resources/extensions/gsd/session-forensics.ts +5 -5
  242. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +351 -0
  243. package/src/resources/extensions/gsd/tests/collision-diagnostics.test.ts +705 -0
  244. package/src/resources/extensions/gsd/tests/context-compression.test.ts +13 -0
  245. package/src/resources/extensions/gsd/tests/diff-context.test.ts +136 -0
  246. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +118 -0
  247. package/src/resources/extensions/gsd/tests/marketplace-discovery.test.ts +202 -0
  248. package/src/resources/extensions/gsd/tests/marketplace-test-fixtures.ts +91 -0
  249. package/src/resources/extensions/gsd/tests/namespaced-registry.test.ts +1027 -0
  250. package/src/resources/extensions/gsd/tests/namespaced-resolver.test.ts +671 -0
  251. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +105 -0
  252. package/src/resources/extensions/gsd/tests/plugin-importer-live.test.ts +481 -0
  253. package/src/resources/extensions/gsd/tests/plugin-importer.test.ts +1383 -0
  254. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +21 -2
  255. package/src/resources/extensions/gsd/tests/preferences-schema-validation.test.ts +8 -1
  256. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +11 -0
  257. package/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts +120 -0
  258. package/src/resources/extensions/gsd/token-counter.ts +45 -0
  259. package/src/resources/extensions/gsd/visualizer-data.ts +2 -2
  260. package/src/resources/extensions/gsd/worktree-manager.ts +29 -1
  261. package/src/resources/extensions/gsd/worktree.ts +3 -0
  262. package/src/resources/extensions/mcporter/index.ts +90 -7
  263. package/src/resources/extensions/search-the-web/native-search.ts +2 -0
  264. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +9 -1
  265. package/src/resources/extensions/search-the-web/url-utils.ts +35 -1
  266. package/src/resources/extensions/shared/terminal.ts +1 -1
  267. package/src/resources/extensions/universal-config/discovery.ts +4 -0
  268. package/src/resources/extensions/universal-config/format.ts +35 -4
  269. package/src/resources/extensions/universal-config/index.ts +5 -3
  270. package/src/resources/extensions/universal-config/scanners.ts +65 -2
  271. package/src/resources/extensions/universal-config/tests/discovery.test.ts +9 -1
  272. package/src/resources/extensions/universal-config/tests/format.test.ts +22 -6
  273. package/src/resources/extensions/universal-config/tests/scanners.test.ts +18 -0
  274. package/src/resources/extensions/universal-config/types.ts +20 -1
  275. package/src/resources/skills/lint/SKILL.md +141 -0
  276. package/src/resources/skills/review/SKILL.md +214 -0
  277. package/src/resources/skills/test/SKILL.md +201 -0
  278. package/dist/resources/extensions/browser-tools/core.d.ts +0 -205
  279. package/src/resources/extensions/browser-tools/core.d.ts +0 -205
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Collision Diagnostics Module
3
+ *
4
+ * Bridges NamespacedRegistry collision data and NamespacedResolver ambiguous
5
+ * resolution into a classified diagnostic taxonomy. Provides two functions:
6
+ * - analyzeCollisions: Scans registry and resolver state to produce classified diagnostics
7
+ * - doctorReport: Formats diagnostics into human-readable output with severity and remediation
8
+ *
9
+ * This module implements R010 (collision reporting) and R011 (doctor advice) for the
10
+ * namespaced component system.
11
+ */
12
+
13
+ import type { NamespacedRegistry, RegistryDiagnostic } from './namespaced-registry.js';
14
+ import type { NamespacedResolver, ResolutionResult } from './namespaced-resolver.js';
15
+
16
+ // ============================================================================
17
+ // Type Definitions
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Classification of collision type.
22
+ * - canonical-conflict: Two plugins registered the same canonical name (hard error)
23
+ * - shorthand-overlap: Same bare name exists in multiple namespaces (ambiguity)
24
+ * - alias-conflict: Alias shadows a canonical name or bare component name
25
+ */
26
+ export type CollisionClass = 'canonical-conflict' | 'shorthand-overlap' | 'alias-conflict';
27
+
28
+ /**
29
+ * Severity level for diagnostics.
30
+ * - error: Hard collision that prevents correct resolution
31
+ * - warning: Ambiguity that may cause surprising behavior
32
+ */
33
+ export type DiagnosticSeverity = 'error' | 'warning';
34
+
35
+ /**
36
+ * A classified diagnostic with full context for remediation.
37
+ */
38
+ export interface ClassifiedDiagnostic {
39
+ /** The collision classification */
40
+ class: CollisionClass;
41
+
42
+ /** Severity level */
43
+ severity: DiagnosticSeverity;
44
+
45
+ /** All canonical names involved in the collision */
46
+ involvedCanonicalNames: string[];
47
+
48
+ /** File paths to the conflicting components */
49
+ filePaths: string[];
50
+
51
+ /** Human-readable remediation advice */
52
+ remediation: string;
53
+
54
+ /** Optional: the bare name causing ambiguity (shorthand-overlap only) */
55
+ ambiguousBareName?: string;
56
+
57
+ /** Optional: the alias string (alias-conflict only) */
58
+ alias?: string;
59
+
60
+ /** Optional: the canonical name the alias points to (alias-conflict only) */
61
+ aliasTarget?: string;
62
+
63
+ /** Optional: type of alias conflict */
64
+ aliasConflictType?: 'shadows-canonical' | 'shadows-bare-name';
65
+ }
66
+
67
+ /**
68
+ * Doctor report with summary statistics and formatted entries.
69
+ */
70
+ export interface DoctorReport {
71
+ /** Summary counts by class */
72
+ summary: {
73
+ /** Total diagnostics */
74
+ total: number;
75
+ /** Canonical conflicts (errors) */
76
+ canonicalConflicts: number;
77
+ /** Shorthand overlaps (warnings) */
78
+ shorthandOverlaps: number;
79
+ /** Alias conflicts (warnings) */
80
+ aliasConflicts: number;
81
+ };
82
+
83
+ /** Formatted report entries */
84
+ entries: string[];
85
+ }
86
+
87
+ // ============================================================================
88
+ // Implementation
89
+ // ============================================================================
90
+
91
+ /**
92
+ * Analyze a registry and resolver to produce classified diagnostics.
93
+ *
94
+ * This function:
95
+ * 1. Reads registry.getDiagnostics() for canonical conflicts (→ error severity)
96
+ * 2. Groups registry.getAll() by bare component.name
97
+ * 3. For groups with 2+ entries, calls resolver.resolve(bareName) to confirm ambiguity
98
+ * 4. Produces warning diagnostics for ambiguous shorthand resolution
99
+ *
100
+ * @param registry - The namespaced registry to analyze
101
+ * @param resolver - The resolver to test ambiguity
102
+ * @returns Array of classified diagnostics
103
+ */
104
+ export function analyzeCollisions(
105
+ registry: NamespacedRegistry,
106
+ resolver: NamespacedResolver
107
+ ): ClassifiedDiagnostic[] {
108
+ const diagnostics: ClassifiedDiagnostic[] = [];
109
+
110
+ // Step 1: Process canonical conflicts from registry diagnostics
111
+ const registryDiagnostics = registry.getDiagnostics();
112
+ for (const diag of registryDiagnostics) {
113
+ if (diag.type === 'collision') {
114
+ diagnostics.push({
115
+ class: 'canonical-conflict',
116
+ severity: 'error',
117
+ involvedCanonicalNames: [diag.collision.canonicalName],
118
+ filePaths: [diag.collision.winnerPath, diag.collision.loserPath],
119
+ remediation: `Canonical name "${diag.collision.canonicalName}" registered multiple times. ` +
120
+ `The first registration (${diag.collision.winnerSource ?? 'unknown source'}) ` +
121
+ `took precedence over subsequent registration (${diag.collision.loserSource ?? 'unknown source'}). ` +
122
+ `Rename one of the conflicting components to resolve.`,
123
+ });
124
+ }
125
+ }
126
+
127
+ // Step 2: Find shorthand overlaps by grouping components by bare name
128
+ const components = registry.getAll();
129
+ const byBareName = new Map<string, typeof components>();
130
+
131
+ for (const component of components) {
132
+ const bareName = component.name;
133
+ if (!byBareName.has(bareName)) {
134
+ byBareName.set(bareName, []);
135
+ }
136
+ byBareName.get(bareName)!.push(component);
137
+ }
138
+
139
+ // Step 3: For groups with 2+ entries, check if resolver confirms ambiguity
140
+ for (const [bareName, candidates] of byBareName) {
141
+ if (candidates.length >= 2) {
142
+ // Use resolver to confirm ambiguity
143
+ const result = resolver.resolve(bareName);
144
+
145
+ if (result.resolution === 'ambiguous') {
146
+ // This is a shorthand overlap
147
+ const canonicalNames = candidates.map(c => c.canonicalName);
148
+ const filePaths = candidates.map(c => c.filePath);
149
+
150
+ diagnostics.push({
151
+ class: 'shorthand-overlap',
152
+ severity: 'warning',
153
+ involvedCanonicalNames: canonicalNames,
154
+ filePaths,
155
+ remediation: formatShorthandRemediation(bareName, canonicalNames),
156
+ ambiguousBareName: bareName,
157
+ });
158
+ }
159
+ // If resolution is 'shorthand' or 'local-first', the overlap is resolved
160
+ // unambiguously by the resolver, so we don't warn
161
+ }
162
+ }
163
+
164
+ // Step 4: Check for alias conflicts
165
+ const aliases = registry.getAliases();
166
+ const canonicalNamesSet = new Set(components.map(c => c.canonicalName));
167
+
168
+ for (const [alias, targetCanonical] of aliases) {
169
+ // Check if alias shadows a canonical name
170
+ // (This can happen if a component was registered AFTER the alias was created)
171
+ if (canonicalNamesSet.has(alias)) {
172
+ const shadowedComponent = components.find(c => c.canonicalName === alias);
173
+ const aliasedComponent = components.find(c => c.canonicalName === targetCanonical);
174
+
175
+ diagnostics.push({
176
+ class: 'alias-conflict',
177
+ severity: 'warning',
178
+ involvedCanonicalNames: [alias, targetCanonical],
179
+ filePaths: [
180
+ shadowedComponent?.filePath ?? '<unknown>',
181
+ aliasedComponent?.filePath ?? '<unknown>',
182
+ ],
183
+ remediation: formatAliasShadowsCanonicalRemediation(alias, targetCanonical),
184
+ alias,
185
+ aliasTarget: targetCanonical,
186
+ aliasConflictType: 'shadows-canonical',
187
+ });
188
+ continue; // Skip further checks for this alias
189
+ }
190
+
191
+ // Check if alias shadows a bare name (matches component.name in any namespace)
192
+ const matchingBareNames = components.filter(c => c.name === alias);
193
+ if (matchingBareNames.length > 0) {
194
+ const filePaths = matchingBareNames.map(c => c.filePath);
195
+ const aliasedComponent = components.find(c => c.canonicalName === targetCanonical);
196
+ if (aliasedComponent) filePaths.push(aliasedComponent.filePath);
197
+
198
+ diagnostics.push({
199
+ class: 'alias-conflict',
200
+ severity: 'warning',
201
+ involvedCanonicalNames: [targetCanonical, ...matchingBareNames.map(c => c.canonicalName)],
202
+ filePaths,
203
+ remediation: formatAliasShadowsBareNameRemediation(alias, targetCanonical, matchingBareNames.map(c => c.canonicalName)),
204
+ alias,
205
+ aliasTarget: targetCanonical,
206
+ aliasConflictType: 'shadows-bare-name',
207
+ });
208
+ }
209
+ }
210
+
211
+ return diagnostics;
212
+ }
213
+
214
+ /**
215
+ * Format remediation advice for shorthand overlap.
216
+ *
217
+ * @param bareName - The ambiguous bare name
218
+ * @param canonicalNames - All canonical names that match
219
+ * @returns Human-readable remediation message
220
+ */
221
+ function formatShorthandRemediation(bareName: string, canonicalNames: string[]): string {
222
+ const suggestions = canonicalNames
223
+ .map(cn => `\`${cn}\``)
224
+ .join(', ');
225
+
226
+ return `Bare name "${bareName}" is ambiguous across ${canonicalNames.length} namespaces. ` +
227
+ `Use a canonical name (${suggestions}) to avoid ambiguity.`;
228
+ }
229
+
230
+ /**
231
+ * Format remediation advice for alias shadowing a canonical name.
232
+ *
233
+ * @param alias - The alias that shadows a canonical name
234
+ * @param targetCanonical - The canonical name the alias points to
235
+ * @returns Human-readable remediation message
236
+ */
237
+ function formatAliasShadowsCanonicalRemediation(alias: string, targetCanonical: string): string {
238
+ return `Alias "${alias}" shadows an existing canonical name. ` +
239
+ `The alias points to "${targetCanonical}", but resolving "${alias}" will now match the component, not the alias. ` +
240
+ `Consider rename or remove the alias to avoid confusion.`;
241
+ }
242
+
243
+ /**
244
+ * Format remediation advice for alias shadowing a bare name.
245
+ *
246
+ * @param alias - The alias that shadows bare names
247
+ * @param targetCanonical - The canonical name the alias points to
248
+ * @param shadowedCanonicals - The canonical names whose bare names are shadowed
249
+ * @returns Human-readable remediation message
250
+ */
251
+ function formatAliasShadowsBareNameRemediation(
252
+ alias: string,
253
+ targetCanonical: string,
254
+ shadowedCanonicals: string[]
255
+ ): string {
256
+ const shadowed = shadowedCanonicals.map(cn => `\`${cn}\``).join(', ');
257
+ return `Alias "${alias}" shadows ${shadowedCanonicals.length} component(s) with the same bare name (${shadowed}). ` +
258
+ `Resolving "${alias}" will use the alias (pointing to "${targetCanonical}"), not shorthand resolution. ` +
259
+ `Use canonical names to be explicit, or rename the alias if this is unintended.`;
260
+ }
261
+
262
+ /**
263
+ * Format diagnostics into a human-readable doctor report.
264
+ *
265
+ * Each diagnostic is formatted with:
266
+ * - Severity icon (❌ error / ⚠️ warning)
267
+ * - Description of the issue
268
+ * - Involved file paths
269
+ * - Remediation advice
270
+ *
271
+ * @param diagnostics - Array of classified diagnostics
272
+ * @returns Doctor report with summary and formatted entries
273
+ */
274
+ export function doctorReport(diagnostics: ClassifiedDiagnostic[]): DoctorReport {
275
+ const summary = {
276
+ total: diagnostics.length,
277
+ canonicalConflicts: diagnostics.filter(d => d.class === 'canonical-conflict').length,
278
+ shorthandOverlaps: diagnostics.filter(d => d.class === 'shorthand-overlap').length,
279
+ aliasConflicts: diagnostics.filter(d => d.class === 'alias-conflict').length,
280
+ };
281
+
282
+ const entries = diagnostics.map(diagnostic => formatDiagnosticEntry(diagnostic));
283
+
284
+ return { summary, entries };
285
+ }
286
+
287
+ /**
288
+ * Format a single diagnostic entry for display.
289
+ *
290
+ * @param diagnostic - The diagnostic to format
291
+ * @returns Formatted string entry
292
+ */
293
+ function formatDiagnosticEntry(diagnostic: ClassifiedDiagnostic): string {
294
+ const icon = diagnostic.severity === 'error' ? '❌' : '⚠️';
295
+ const lines: string[] = [];
296
+
297
+ // Header with severity and class
298
+ lines.push(`${icon} ${diagnostic.class.toUpperCase()}`);
299
+
300
+ // Description
301
+ if (diagnostic.class === 'canonical-conflict') {
302
+ lines.push(` Canonical name conflict: ${diagnostic.involvedCanonicalNames[0]}`);
303
+ } else if (diagnostic.class === 'alias-conflict') {
304
+ if (diagnostic.aliasConflictType === 'shadows-canonical') {
305
+ lines.push(` Alias "${diagnostic.alias}" shadows canonical name (points to ${diagnostic.aliasTarget})`);
306
+ } else {
307
+ lines.push(` Alias "${diagnostic.alias}" shadows bare name (points to ${diagnostic.aliasTarget})`);
308
+ }
309
+ } else {
310
+ lines.push(` Shorthand overlap: "${diagnostic.ambiguousBareName}" matches ${diagnostic.involvedCanonicalNames.length} components`);
311
+ }
312
+
313
+ // File paths
314
+ lines.push(' Files:');
315
+ for (const path of diagnostic.filePaths) {
316
+ lines.push(` - ${path}`);
317
+ }
318
+
319
+ // Remediation
320
+ lines.push(` Remediation: ${diagnostic.remediation}`);
321
+
322
+ return lines.join('\n');
323
+ }
324
+
325
+ // ============================================================================
326
+ // Exports
327
+ // ============================================================================
328
+
329
+ export default {
330
+ analyzeCollisions,
331
+ doctorReport,
332
+ };
@@ -26,7 +26,8 @@ import {
26
26
  loadEffectiveGSDPreferences,
27
27
  resolveAllSkillReferences,
28
28
  } from "./preferences.js";
29
- import { loadFile, saveFile, appendOverride, appendKnowledge } from "./files.js";
29
+ import { loadFile, saveFile, appendOverride, appendKnowledge, splitFrontmatter, parseFrontmatterMap } from "./files.js";
30
+ import { runClaudeImportFlow } from "./claude-import.js";
30
31
  import {
31
32
  formatDoctorIssuesForPrompt,
32
33
  formatDoctorReport,
@@ -68,13 +69,13 @@ function projectRoot(): string {
68
69
 
69
70
  export function registerGSDCommand(pi: ExtensionAPI): void {
70
71
  pi.registerCommand("gsd", {
71
- description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|migrate|remote|steer|knowledge",
72
+ description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge",
72
73
  getArgumentCompletions: (prefix: string) => {
73
74
  const subcommands = [
74
75
  "help", "next", "auto", "stop", "pause", "status", "visualize", "queue", "quick", "discuss",
75
76
  "capture", "triage",
76
77
  "history", "undo", "skip", "export", "cleanup", "mode", "prefs",
77
- "config", "hooks", "run-hook", "skill-health", "doctor", "migrate", "remote", "steer", "inspect", "knowledge",
78
+ "config", "hooks", "run-hook", "skill-health", "doctor", "forensics", "migrate", "remote", "steer", "inspect", "knowledge",
78
79
  ];
79
80
  const parts = prefix.trim().split(/\s+/);
80
81
 
@@ -100,7 +101,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
100
101
 
101
102
  if (parts[0] === "prefs" && parts.length <= 2) {
102
103
  const subPrefix = parts[1] ?? "";
103
- return ["global", "project", "status", "wizard", "setup"]
104
+ return ["global", "project", "status", "wizard", "setup", "import-claude"]
104
105
  .filter((cmd) => cmd.startsWith(subPrefix))
105
106
  .map((cmd) => ({ value: `prefs ${cmd}`, label: cmd }));
106
107
  }
@@ -204,6 +205,12 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
204
205
  return;
205
206
  }
206
207
 
208
+ if (trimmed === "forensics" || trimmed.startsWith("forensics ")) {
209
+ const { handleForensics } = await import("./forensics.js");
210
+ await handleForensics(trimmed.replace(/^forensics\s*/, "").trim(), ctx, pi);
211
+ return;
212
+ }
213
+
207
214
  if (trimmed === "next" || trimmed.startsWith("next ")) {
208
215
  if (trimmed.includes("--dry-run")) {
209
216
  await handleDryRun(ctx, projectRoot());
@@ -274,6 +281,12 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
274
281
  return;
275
282
  }
276
283
 
284
+ if (trimmed === "cleanup") {
285
+ await handleCleanupBranches(ctx, projectRoot());
286
+ await handleCleanupSnapshots(ctx, projectRoot());
287
+ return;
288
+ }
289
+
277
290
  if (trimmed === "cleanup branches") {
278
291
  await handleCleanupBranches(ctx, projectRoot());
279
292
  return;
@@ -507,6 +520,15 @@ async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<
507
520
  return;
508
521
  }
509
522
 
523
+ if (trimmed === "import-claude" || trimmed === "import-claude global") {
524
+ await handleImportClaude(ctx, "global");
525
+ return;
526
+ }
527
+
528
+ if (trimmed === "import-claude project") {
529
+ await handleImportClaude(ctx, "project");
530
+ return;
531
+ }
510
532
  if (trimmed === "status") {
511
533
  const globalPrefs = loadGlobalGSDPreferences();
512
534
  const projectPrefs = loadProjectGSDPreferences();
@@ -537,7 +559,38 @@ async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<
537
559
  return;
538
560
  }
539
561
 
540
- ctx.ui.notify("Usage: /gsd prefs [global|project|status|wizard|setup]", "info");
562
+ ctx.ui.notify("Usage: /gsd prefs [global|project|status|wizard|setup|import-claude [global|project]]", "info");
563
+ }
564
+
565
+ async function handleImportClaude(ctx: ExtensionCommandContext, scope: "global" | "project"): Promise<void> {
566
+ const path = scope === "project" ? getProjectGSDPreferencesPath() : getGlobalGSDPreferencesPath();
567
+ if (!existsSync(path)) {
568
+ await ensurePreferencesFile(path, ctx, scope);
569
+ }
570
+
571
+ const readPrefs = (): Record<string, unknown> => {
572
+ if (!existsSync(path)) return { version: 1 };
573
+ const content = readFileSync(path, "utf-8");
574
+ const [frontmatterLines] = splitFrontmatter(content);
575
+ return frontmatterLines ? parseFrontmatterMap(frontmatterLines) : { version: 1 };
576
+ };
577
+
578
+ const writePrefs = async (prefs: Record<string, unknown>): Promise<void> => {
579
+ prefs.version = prefs.version || 1;
580
+ const frontmatter = serializePreferencesToFrontmatter(prefs);
581
+ let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
582
+ if (existsSync(path)) {
583
+ const existingContent = readFileSync(path, "utf-8");
584
+ const closingIdx = existingContent.indexOf("\n---", existingContent.indexOf("---"));
585
+ if (closingIdx !== -1) {
586
+ const afterFrontmatter = existingContent.slice(closingIdx + 4);
587
+ if (afterFrontmatter.trim()) body = afterFrontmatter;
588
+ }
589
+ }
590
+ await saveFile(path, `---\n${frontmatter}---${body}`);
591
+ };
592
+
593
+ await runClaudeImportFlow(ctx, scope, readPrefs, writePrefs);
541
594
  }
542
595
 
543
596
  async function handlePrefsMode(ctx: ExtensionCommandContext, scope: "global" | "project"): Promise<void> {
@@ -991,7 +1044,7 @@ async function configureGit(ctx: ExtensionCommandContext, prefs: Record<string,
991
1044
  const currentIsolation = git.isolation ? String(git.isolation) : "";
992
1045
  const isolationChoice = await ctx.ui.select(
993
1046
  `Git isolation strategy${currentIsolation ? ` (current: ${currentIsolation})` : " (default: worktree)"}:`,
994
- ["worktree", "branch", "(keep current)"],
1047
+ ["worktree", "branch", "none", "(keep current)"],
995
1048
  );
996
1049
  if (isolationChoice && isolationChoice !== "(keep current)") {
997
1050
  git.isolation = isolationChoice;
@@ -1095,7 +1148,7 @@ async function configureNotifications(ctx: ExtensionCommandContext, prefs: Recor
1095
1148
 
1096
1149
  for (const field of notifFields) {
1097
1150
  const current = notif[field.key];
1098
- const currentStr = current !== undefined ? String(current) : "";
1151
+ const currentStr = current !== undefined && typeof current === "boolean" ? String(current) : "";
1099
1152
  const choice = await ctx.ui.select(
1100
1153
  `${field.label}${currentStr ? ` (current: ${currentStr})` : ` (default: ${field.defaultVal})`}:`,
1101
1154
  ["true", "false", "(keep current)"],
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Diff-aware context module — prioritizes recently-changed files when building
3
+ * context for the AI agent. Uses git diff/status to discover changes, then
4
+ * provides ranking utilities for context-window budget allocation.
5
+ *
6
+ * Standalone module: only imports node:child_process and node:path.
7
+ */
8
+
9
+ import { execFileSync } from "node:child_process";
10
+ import { resolve } from "node:path";
11
+
12
+ // ─── Types ──────────────────────────────────────────────────────────────────
13
+
14
+ export interface ChangedFileInfo {
15
+ path: string;
16
+ changeType: "modified" | "added" | "deleted" | "staged";
17
+ linesChanged?: number;
18
+ }
19
+
20
+ export interface RecentFilesOptions {
21
+ /** Maximum number of files to return (default 20) */
22
+ maxFiles?: number;
23
+ /** Only consider commits within this many days (default 7) */
24
+ sinceDays?: number;
25
+ }
26
+
27
+ // ─── Helpers ────────────────────────────────────────────────────────────────
28
+
29
+ const EXEC_OPTS = {
30
+ encoding: "utf-8" as const,
31
+ timeout: 5000,
32
+ stdio: ["pipe", "pipe", "pipe"] as ["pipe", "pipe", "pipe"],
33
+ };
34
+
35
+ function git(args: string[], cwd: string): string {
36
+ return execFileSync("git", args, { ...EXEC_OPTS, cwd }).trim();
37
+ }
38
+
39
+ function splitLines(output: string): string[] {
40
+ return output
41
+ .split("\n")
42
+ .map((l) => l.trim())
43
+ .filter(Boolean);
44
+ }
45
+
46
+ // ─── Public API ─────────────────────────────────────────────────────────────
47
+
48
+ /**
49
+ * Returns recently-changed file paths, deduplicated and sorted by recency
50
+ * (most recent first). Combines committed diffs, staged changes, and
51
+ * unstaged/untracked files from `git status`.
52
+ */
53
+ export async function getRecentlyChangedFiles(
54
+ cwd: string,
55
+ options?: RecentFilesOptions,
56
+ ): Promise<string[]> {
57
+ const maxFiles = options?.maxFiles ?? 20;
58
+ const sinceDays = options?.sinceDays ?? 7;
59
+ const dir = resolve(cwd);
60
+
61
+ try {
62
+ // 1. Committed changes in the last N commits (or since sinceDays)
63
+ let committedFiles: string[] = [];
64
+ try {
65
+ const days = Math.max(1, Math.floor(Number(sinceDays)));
66
+ if (!Number.isFinite(days)) throw new Error("invalid sinceDays");
67
+ const raw = git(["log", "--diff-filter=ACMR", "--name-only", "--pretty=format:", `--since=${days} days ago`], dir);
68
+ committedFiles = splitLines(raw);
69
+ } catch {
70
+ // Fallback: use HEAD~10
71
+ try {
72
+ const raw = git(["diff", "--name-only", "HEAD~10"], dir);
73
+ committedFiles = splitLines(raw);
74
+ } catch {
75
+ // Shallow clone or <10 commits — ignore
76
+ }
77
+ }
78
+
79
+ // 2. Staged changes
80
+ let stagedFiles: string[] = [];
81
+ try {
82
+ const raw = git(["diff", "--cached", "--name-only"], dir);
83
+ stagedFiles = splitLines(raw);
84
+ } catch {
85
+ // ignore
86
+ }
87
+
88
+ // 3. Unstaged / untracked via porcelain status
89
+ let statusFiles: string[] = [];
90
+ try {
91
+ const raw = git(["status", "--porcelain"], dir);
92
+ statusFiles = splitLines(raw).map((line) => line.slice(3)); // strip XY + space
93
+ } catch {
94
+ // ignore
95
+ }
96
+
97
+ // Deduplicate, preserving insertion order (most-recent-first: status → staged → committed)
98
+ const seen = new Set<string>();
99
+ const result: string[] = [];
100
+ for (const file of [...statusFiles, ...stagedFiles, ...committedFiles]) {
101
+ if (!seen.has(file)) {
102
+ seen.add(file);
103
+ result.push(file);
104
+ }
105
+ }
106
+
107
+ return result.slice(0, maxFiles);
108
+ } catch {
109
+ // Non-git directory or git unavailable — graceful fallback
110
+ return [];
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Returns richer change metadata: change type and approximate line counts.
116
+ */
117
+ export async function getChangedFilesWithContext(
118
+ cwd: string,
119
+ ): Promise<ChangedFileInfo[]> {
120
+ const dir = resolve(cwd);
121
+
122
+ try {
123
+ const result: ChangedFileInfo[] = [];
124
+ const seen = new Set<string>();
125
+
126
+ const add = (info: ChangedFileInfo) => {
127
+ if (!seen.has(info.path)) {
128
+ seen.add(info.path);
129
+ result.push(info);
130
+ }
131
+ };
132
+
133
+ // 1. Staged files with numstat
134
+ try {
135
+ const numstat = git(["diff", "--cached", "--numstat"], dir);
136
+ for (const line of splitLines(numstat)) {
137
+ const [added, deleted, filePath] = line.split("\t");
138
+ if (!filePath) continue;
139
+ const lines =
140
+ added === "-" || deleted === "-"
141
+ ? undefined
142
+ : Number(added) + Number(deleted);
143
+ add({ path: filePath, changeType: "staged", linesChanged: lines });
144
+ }
145
+ } catch {
146
+ // ignore
147
+ }
148
+
149
+ // 2. Unstaged modifications with numstat
150
+ try {
151
+ const numstat = git(["diff", "--numstat"], dir);
152
+ for (const line of splitLines(numstat)) {
153
+ const [added, deleted, filePath] = line.split("\t");
154
+ if (!filePath) continue;
155
+ const lines =
156
+ added === "-" || deleted === "-"
157
+ ? undefined
158
+ : Number(added) + Number(deleted);
159
+ add({ path: filePath, changeType: "modified", linesChanged: lines });
160
+ }
161
+ } catch {
162
+ // ignore
163
+ }
164
+
165
+ // 3. Untracked / deleted from porcelain status
166
+ try {
167
+ const raw = git(["status", "--porcelain"], dir);
168
+ for (const line of splitLines(raw)) {
169
+ const code = line.slice(0, 2);
170
+ const filePath = line.slice(3);
171
+ if (seen.has(filePath)) continue;
172
+
173
+ if (code.includes("?")) {
174
+ add({ path: filePath, changeType: "added" });
175
+ } else if (code.includes("D")) {
176
+ add({ path: filePath, changeType: "deleted" });
177
+ } else if (code.includes("A")) {
178
+ add({ path: filePath, changeType: "added" });
179
+ } else {
180
+ add({ path: filePath, changeType: "modified" });
181
+ }
182
+ }
183
+ } catch {
184
+ // ignore
185
+ }
186
+
187
+ return result;
188
+ } catch {
189
+ return [];
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Ranks a file list so that recently-changed files appear first.
195
+ * Files present in `changedFiles` are placed at the front (in their
196
+ * original changedFiles order), followed by unchanged files in their
197
+ * original order.
198
+ */
199
+ export function rankFilesByRelevance(
200
+ files: string[],
201
+ changedFiles: string[],
202
+ ): string[] {
203
+ const changedSet = new Set(changedFiles);
204
+ const changed: string[] = [];
205
+ const rest: string[] = [];
206
+
207
+ for (const f of files) {
208
+ if (changedSet.has(f)) {
209
+ changed.push(f);
210
+ } else {
211
+ rest.push(f);
212
+ }
213
+ }
214
+
215
+ // Maintain changedFiles priority order within the changed group
216
+ const changedOrder = new Map(changedFiles.map((f, i) => [f, i]));
217
+ changed.sort((a, b) => (changedOrder.get(a) ?? 0) - (changedOrder.get(b) ?? 0));
218
+
219
+ return [...changed, ...rest];
220
+ }