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
@@ -30,6 +30,9 @@ import { loadPrompt } from "./prompt-loader.js";
30
30
  import { gsdRoot } from "./paths.js";
31
31
  import { formatDuration } from "../shared/format-utils.js";
32
32
  import { getAutoWorktreePath } from "./auto-worktree.js";
33
+ import { loadEffectiveGSDPreferences, loadGlobalGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
34
+ import { showNextAction } from "../shared/tui.js";
35
+ import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
33
36
 
34
37
  // ─── Types ────────────────────────────────────────────────────────────────────
35
38
 
@@ -67,6 +70,71 @@ interface ForensicReport {
67
70
  recentUnits: { type: string; id: string; cost: number; duration: number; model: string; finishedAt: number }[];
68
71
  }
69
72
 
73
+ // ─── Duplicate Detection ──────────────────────────────────────────────────────
74
+
75
+ const DEDUP_PROMPT_SECTION = `
76
+ ## Duplicate Detection (REQUIRED before issue creation)
77
+
78
+ Before offering to create a GitHub issue, you MUST search for existing issues and PRs that may already address this bug. This step uses the user's AI tokens for analysis.
79
+
80
+ ### Search Steps
81
+
82
+ 1. **Search closed issues** for similar keywords from your diagnosis:
83
+ \`\`\`
84
+ gh issue list --repo gsd-build/gsd-2 --state closed --search "<keywords from root cause>" --limit 20
85
+ \`\`\`
86
+
87
+ 2. **Search open PRs** that might contain the fix:
88
+ \`\`\`
89
+ gh pr list --repo gsd-build/gsd-2 --state open --search "<keywords>" --limit 10
90
+ \`\`\`
91
+
92
+ 3. **Search merged PRs** that may have already fixed this:
93
+ \`\`\`
94
+ gh pr list --repo gsd-build/gsd-2 --state merged --search "<keywords>" --limit 10
95
+ \`\`\`
96
+
97
+ ### Analysis
98
+
99
+ For each result, compare it against your root-cause diagnosis:
100
+ - Does the issue describe the same code path or file?
101
+ - Does the PR modify the same file:line you identified?
102
+ - Is the symptom description semantically similar even if keywords differ?
103
+
104
+ ### Present Findings
105
+
106
+ If you find potential matches, present them to the user:
107
+
108
+ 1. **"Already fixed by PR #X — skip issue creation"** — when a merged PR or closed issue clearly addresses the same root cause. Explain why you believe it matches.
109
+ 2. **"Add my findings to existing issue #Y"** — when an open issue exists for the same bug. Use \`gh issue comment #Y --repo gsd-build/gsd-2\` to add forensic evidence.
110
+ 3. **"Create new issue anyway"** — when existing results do not cover this specific failure.
111
+
112
+ Only proceed to issue creation if no matches were found OR the user explicitly chooses "Create new issue anyway".
113
+ `;
114
+
115
+ async function writeForensicsDedupPref(ctx: ExtensionCommandContext, enabled: boolean): Promise<void> {
116
+ const prefsPath = getGlobalGSDPreferencesPath();
117
+ await ensurePreferencesFile(prefsPath, ctx, "global");
118
+ const existing = loadGlobalGSDPreferences();
119
+ const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : {};
120
+ prefs.version = prefs.version || 1;
121
+ prefs.forensics_dedup = enabled;
122
+
123
+ const frontmatter = serializePreferencesToFrontmatter(prefs);
124
+ const raw = existsSync(prefsPath) ? readFileSync(prefsPath, "utf-8") : "";
125
+ let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
126
+ const start = raw.startsWith("---\n") ? 4 : raw.startsWith("---\r\n") ? 5 : -1;
127
+ if (start !== -1) {
128
+ const closingIdx = raw.indexOf("\n---", start);
129
+ if (closingIdx !== -1) {
130
+ const after = raw.slice(closingIdx + 4);
131
+ if (after.trim()) body = after;
132
+ }
133
+ }
134
+
135
+ writeFileSync(prefsPath, `---\n${frontmatter}---${body}`, "utf-8");
136
+ }
137
+
70
138
  // ─── Entry Point ──────────────────────────────────────────────────────────────
71
139
 
72
140
  export async function handleForensics(
@@ -98,6 +166,29 @@ export async function handleForensics(
98
166
  return;
99
167
  }
100
168
 
169
+ // ─── Duplicate detection opt-in ─────────────────────────────────────────────
170
+ const effectivePrefs = loadEffectiveGSDPreferences()?.preferences;
171
+ let dedupEnabled = effectivePrefs?.forensics_dedup === true;
172
+
173
+ if (effectivePrefs?.forensics_dedup === undefined) {
174
+ const choice = await showNextAction(ctx, {
175
+ title: "Duplicate detection available",
176
+ summary: ["Before filing a GitHub issue, forensics can search existing issues and PRs to avoid duplicates.", "This uses additional AI tokens for analysis."],
177
+ actions: [
178
+ { id: "enable", label: "Enable duplicate detection", description: "Search issues/PRs before filing (recommended)", recommended: true },
179
+ { id: "skip", label: "Skip for now", description: "File without checking for duplicates" },
180
+ ],
181
+ notYetMessage: "You can enable this later via preferences (forensics_dedup: true).",
182
+ });
183
+
184
+ if (choice === "enable") {
185
+ await writeForensicsDedupPref(ctx, true);
186
+ dedupEnabled = true;
187
+ }
188
+ }
189
+
190
+ const dedupSection = dedupEnabled ? DEDUP_PROMPT_SECTION : "";
191
+
101
192
  ctx.ui.notify("Building forensic report...", "info");
102
193
 
103
194
  const report = await buildForensicReport(basePath);
@@ -117,6 +208,7 @@ export async function handleForensics(
117
208
  problemDescription,
118
209
  forensicData,
119
210
  gsdSourceDir,
211
+ dedupSection,
120
212
  });
121
213
 
122
214
  ctx.ui.notify(`Forensic report saved: ${relative(basePath, savedPath)}`, "info");
@@ -8,4 +8,5 @@ export const GIT_NO_PROMPT_ENV = {
8
8
  GIT_TERMINAL_PROMPT: "0",
9
9
  GIT_ASKPASS: "",
10
10
  GIT_SVN_ID: "",
11
+ LC_ALL: "C", // force English git output so stderr string checks work on all locales (#1997)
11
12
  };
@@ -245,7 +245,6 @@ export function writeIntegrationBranch(
245
245
  basePath: string,
246
246
  milestoneId: string,
247
247
  branch: string,
248
- _options?: { commitDocs?: boolean },
249
248
  ): void {
250
249
  // Don't record slice branches as the integration target
251
250
  if (SLICE_BRANCH_RE.test(branch)) return;
@@ -137,7 +137,7 @@ export function hasGitTrackedGsdFiles(basePath: string): boolean {
137
137
  */
138
138
  export function ensureGitignore(
139
139
  basePath: string,
140
- options?: { manageGitignore?: boolean; commitDocs?: boolean },
140
+ options?: { manageGitignore?: boolean },
141
141
  ): boolean {
142
142
  // If manage_gitignore is explicitly false, do not touch .gitignore at all
143
143
  if (options?.manageGitignore === false) return false;
@@ -847,6 +847,7 @@ export function nativeMergeSquash(basePath: string, branch: string): GitMergeRes
847
847
  cwd: basePath,
848
848
  stdio: ["ignore", "pipe", "pipe"],
849
849
  encoding: "utf-8",
850
+ env: GIT_NO_PROMPT_ENV,
850
851
  });
851
852
  return { success: true, conflicts: [] };
852
853
  } catch (err: unknown) {
@@ -89,6 +89,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
89
89
  "reactive_execution",
90
90
  "github",
91
91
  "service_tier",
92
+ "forensics_dedup",
92
93
  ]);
93
94
 
94
95
  /** Canonical list of all dispatch unit types. */
@@ -223,6 +224,8 @@ export interface GSDPreferences {
223
224
  github?: GitHubSyncConfig;
224
225
  /** OpenAI service tier preference. "priority" = 2x cost, faster. "flex" = 0.5x cost, slower. Only affects gpt-5.4 models. */
225
226
  service_tier?: "priority" | "flex";
227
+ /** Opt-in: search existing issues and PRs before filing from /gsd forensics. Uses additional AI tokens. */
228
+ forensics_dedup?: boolean;
226
229
  }
227
230
 
228
231
  export interface LoadedGSDPreferences {
@@ -200,12 +200,22 @@ function loadPreferencesFile(path: string, scope: "global" | "project"): LoadedG
200
200
  export function parsePreferencesMarkdown(content: string): GSDPreferences | null {
201
201
  // Use indexOf instead of [\s\S]*? regex to avoid backtracking (#468)
202
202
  const startMarker = content.startsWith('---\r\n') ? '---\r\n' : '---\n';
203
- if (!content.startsWith(startMarker)) return null;
204
- const searchStart = startMarker.length;
205
- const endIdx = content.indexOf('\n---', searchStart);
206
- if (endIdx === -1) return null;
207
- const block = content.slice(searchStart, endIdx);
208
- return parseFrontmatterBlock(block.replace(/\r/g, ''));
203
+ if (content.startsWith(startMarker)) {
204
+ const searchStart = startMarker.length;
205
+ const endIdx = content.indexOf('\n---', searchStart);
206
+ if (endIdx === -1) return null;
207
+ const block = content.slice(searchStart, endIdx);
208
+ return parseFrontmatterBlock(block.replace(/\r/g, ''));
209
+ }
210
+
211
+ // Fallback: heading+list format (e.g. "## Git\n- isolation: none") (#2036)
212
+ // GSD agents may write preferences files without frontmatter delimiters.
213
+ if (/^##\s+\w/m.test(content)) {
214
+ return parseHeadingListFormat(content);
215
+ }
216
+
217
+ console.warn("[parsePreferencesMarkdown] preferences.md exists but uses an unrecognized format — skipping.");
218
+ return null;
209
219
  }
210
220
 
211
221
  function parseFrontmatterBlock(frontmatter: string): GSDPreferences {
@@ -221,6 +231,51 @@ function parseFrontmatterBlock(frontmatter: string): GSDPreferences {
221
231
  }
222
232
  }
223
233
 
234
+ /**
235
+ * Parse heading+list format into a nested object, then cast to GSDPreferences.
236
+ * Handles markdown like:
237
+ * ## Git
238
+ * - isolation: none
239
+ * - commit_docs: true
240
+ * ## Models
241
+ * - planner: sonnet
242
+ */
243
+ function parseHeadingListFormat(content: string): GSDPreferences {
244
+ const result: Record<string, Record<string, string>> = {};
245
+ let currentSection: string | null = null;
246
+
247
+ for (const rawLine of content.split('\n')) {
248
+ const line = rawLine.replace(/\r$/, '');
249
+ const headingMatch = line.match(/^##\s+(.+)$/);
250
+ if (headingMatch) {
251
+ currentSection = headingMatch[1].trim().toLowerCase().replace(/\s+/g, '_');
252
+ continue;
253
+ }
254
+ if (currentSection) {
255
+ const itemMatch = line.match(/^-\s+([^:]+):\s*(.*)$/);
256
+ if (itemMatch) {
257
+ if (!result[currentSection]) result[currentSection] = {};
258
+ const value = itemMatch[2].trim();
259
+ // Coerce "true"/"false" strings and numbers
260
+ result[currentSection][itemMatch[1].trim()] = value;
261
+ }
262
+ }
263
+ }
264
+
265
+ // Convert string values to appropriate types via YAML parser for each section
266
+ const typed: Record<string, unknown> = {};
267
+ for (const [section, entries] of Object.entries(result)) {
268
+ const yamlLines = Object.entries(entries).map(([k, v]) => `${k}: ${v}`).join('\n');
269
+ try {
270
+ typed[section] = parseYaml(yamlLines);
271
+ } catch {
272
+ typed[section] = entries;
273
+ }
274
+ }
275
+
276
+ return typed as GSDPreferences;
277
+ }
278
+
224
279
  // ─── Merging ────────────────────────────────────────────────────────────────
225
280
 
226
281
  /**
@@ -286,6 +341,7 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
286
341
  ? { ...(base.github ?? {}), ...(override.github ?? {}) } as import("../github-sync/types.js").GitHubSyncConfig
287
342
  : undefined,
288
343
  service_tier: override.service_tier ?? base.service_tier,
344
+ forensics_dedup: override.forensics_dedup ?? base.forensics_dedup,
289
345
  };
290
346
  }
291
347
 
@@ -101,11 +101,19 @@ Explain your findings:
101
101
  - **Code snippet** — the problematic code and what it should do instead
102
102
  - **Recovery** — what the user can do right now to get unstuck
103
103
 
104
+ {{dedupSection}}
105
+
104
106
  Then **offer GitHub issue creation**: "Would you like me to create a GitHub issue for this on gsd-build/gsd-2?"
105
107
 
106
- If yes, create using `gh issue create` with this format:
108
+ **CRITICAL: The `github_issues` tool ONLY targets the current user's repository — it has no `repo` parameter. You MUST use `gh issue create --repo gsd-build/gsd-2` via the `bash` tool to file on the correct repo. Do NOT use the `github_issues` tool for this.**
107
109
 
108
- ```
110
+ If yes, create using the `bash` tool:
111
+
112
+ ```bash
113
+ gh issue create --repo gsd-build/gsd-2 \
114
+ --title "..." \
115
+ --label "bug" --label "auto-generated" \
116
+ --body "$(cat <<'EOF'
109
117
  ## Problem
110
118
  [1-2 sentence summary]
111
119
 
@@ -128,11 +136,10 @@ If yes, create using `gh issue create` with this format:
128
136
 
129
137
  ---
130
138
  *Auto-generated by `/gsd forensics`*
139
+ EOF
140
+ )"
131
141
  ```
132
142
 
133
- **Repository:** gsd-build/gsd-2
134
- **Labels:** bug, auto-generated
135
-
136
143
  ### Redaction Rules (CRITICAL)
137
144
 
138
145
  Before creating the issue, you MUST:
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { createHash } from "node:crypto";
10
10
  import { execFileSync } from "node:child_process";
11
- import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
11
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
13
  import { basename, dirname, join, resolve } from "node:path";
14
14
 
@@ -271,15 +271,54 @@ export function externalProjectsRoot(): string {
271
271
  return join(base, "projects");
272
272
  }
273
273
 
274
+ // ─── Numbered Variant Cleanup ────────────────────────────────────────────────
275
+
276
+ /**
277
+ * macOS collision pattern: `.gsd 2`, `.gsd 3`, `.gsd 4`, etc.
278
+ *
279
+ * When `symlinkSync` (or Finder) tries to create `.gsd` but a real directory
280
+ * already exists at that path, macOS APFS silently renames the new entry to
281
+ * `.gsd 2`, then `.gsd 3`, and so on. These numbered variants confuse GSD
282
+ * because the canonical `.gsd` path no longer resolves to the external state
283
+ * directory, making tracked planning files appear deleted.
284
+ *
285
+ * This helper scans the project root for entries matching `.gsd <digits>` and
286
+ * removes them. It is called early in `ensureGsdSymlink()` so that the
287
+ * canonical `.gsd` path is always the one in use.
288
+ */
289
+ const GSD_NUMBERED_VARIANT_RE = /^\.gsd \d+$/;
290
+
291
+ export function cleanNumberedGsdVariants(projectPath: string): string[] {
292
+ const removed: string[] = [];
293
+ try {
294
+ const entries = readdirSync(projectPath);
295
+ for (const entry of entries) {
296
+ if (GSD_NUMBERED_VARIANT_RE.test(entry)) {
297
+ const fullPath = join(projectPath, entry);
298
+ try {
299
+ rmSync(fullPath, { recursive: true, force: true });
300
+ removed.push(entry);
301
+ } catch {
302
+ // Best-effort: if removal fails (e.g. permissions), continue with next
303
+ }
304
+ }
305
+ }
306
+ } catch {
307
+ // Non-fatal: readdir failure should not block symlink creation
308
+ }
309
+ return removed;
310
+ }
311
+
274
312
  // ─── Symlink Management ─────────────────────────────────────────────────────
275
313
 
276
314
  /**
277
315
  * Ensure the `<project>/.gsd` symlink points to the external state directory.
278
316
  *
279
- * 1. mkdir -p the external dir
280
- * 2. If `<project>/.gsd` doesn't exist → create symlink
281
- * 3. If `<project>/.gsd` is already the correct symlink → no-op
282
- * 4. If `<project>/.gsd` is a real directoryreturn as-is (migration handles later)
317
+ * 1. Clean up any macOS numbered collision variants (`.gsd 2`, `.gsd 3`, etc.)
318
+ * 2. mkdir -p the external dir
319
+ * 3. If `<project>/.gsd` doesn't exist create symlink
320
+ * 4. If `<project>/.gsd` is already the correct symlink no-op
321
+ * 5. If `<project>/.gsd` is a real directory → return as-is (migration handles later)
283
322
  *
284
323
  * Returns the resolved external path.
285
324
  */
@@ -297,6 +336,10 @@ export function ensureGsdSymlink(projectPath: string): string {
297
336
  return localGsd;
298
337
  }
299
338
 
339
+ // Clean up macOS numbered collision variants (.gsd 2, .gsd 3, etc.) before
340
+ // any existence checks — otherwise they accumulate and confuse state (#2205).
341
+ cleanNumberedGsdVariants(projectPath);
342
+
300
343
  // Ensure external directory exists
301
344
  mkdirSync(externalPath, { recursive: true });
302
345
 
@@ -23,6 +23,8 @@ import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./comm
23
23
 
24
24
  export type ServiceTierSetting = "priority" | "flex" | undefined;
25
25
 
26
+ const SERVICE_TIER_SCOPE_NOTE = "Only affects gpt-5.4 models, regardless of provider.";
27
+
26
28
  // ─── Gating ──────────────────────────────────────────────────────────────────
27
29
 
28
30
  /**
@@ -51,7 +53,7 @@ export function formatServiceTierStatus(tier: ServiceTierSetting): string {
51
53
  " /gsd fast flex Set to flex (0.5x cost, slower)",
52
54
  " /gsd fast off Disable service tier",
53
55
  "",
54
- "Only affects gpt-5.4 models.",
56
+ SERVICE_TIER_SCOPE_NOTE,
55
57
  ].join("\n");
56
58
  }
57
59
 
@@ -64,10 +66,18 @@ export function formatServiceTierStatus(tier: ServiceTierSetting): string {
64
66
  " /gsd fast flex Set to flex (0.5x cost, slower)",
65
67
  " /gsd fast off Disable service tier",
66
68
  "",
67
- "Only affects gpt-5.4 models.",
69
+ SERVICE_TIER_SCOPE_NOTE,
68
70
  ].join("\n");
69
71
  }
70
72
 
73
+ export function formatServiceTierFooterStatus(
74
+ tier: ServiceTierSetting,
75
+ modelId: string | undefined,
76
+ ): string | undefined {
77
+ if (!tier || !modelId || !supportsServiceTier(modelId)) return undefined;
78
+ return tier === "priority" ? "fast: ⚡ priority" : "fast: 💰 flex";
79
+ }
80
+
71
81
  // ─── Icon Resolution ─────────────────────────────────────────────────────────
72
82
 
73
83
  /**
@@ -148,19 +158,22 @@ export async function handleFast(args: string, ctx: ExtensionCommandContext): Pr
148
158
 
149
159
  if (trimmed === "on") {
150
160
  await writeGlobalServiceTier(ctx, "priority");
151
- ctx.ui.notify("Service tier set to priority (2x cost, faster responses). Only affects gpt-5.4 models.", "info");
161
+ ctx.ui.setStatus("gsd-fast", formatServiceTierFooterStatus("priority", ctx.model?.id));
162
+ ctx.ui.notify("Service tier set to priority (2x cost, faster responses). Only affects gpt-5.4 models, regardless of provider.", "info");
152
163
  return;
153
164
  }
154
165
 
155
166
  if (trimmed === "off") {
156
167
  await writeGlobalServiceTier(ctx, undefined);
168
+ ctx.ui.setStatus("gsd-fast", undefined);
157
169
  ctx.ui.notify("Service tier disabled.", "info");
158
170
  return;
159
171
  }
160
172
 
161
173
  if (trimmed === "flex") {
162
174
  await writeGlobalServiceTier(ctx, "flex");
163
- ctx.ui.notify("Service tier set to flex (0.5x cost, slower responses). Only affects gpt-5.4 models.", "info");
175
+ ctx.ui.setStatus("gsd-fast", formatServiceTierFooterStatus("flex", ctx.model?.id));
176
+ ctx.ui.notify("Service tier set to flex (0.5x cost, slower responses). Only affects gpt-5.4 models, regardless of provider.", "info");
164
177
  return;
165
178
  }
166
179
 
@@ -239,7 +239,7 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
239
239
  const elapsed = Date.now() - _lockAcquiredAt;
240
240
  if (elapsed < 1_800_000) {
241
241
  process.stderr.write(
242
- `[gsd] Lock heartbeat mismatch after ${Math.round(elapsed / 1000)}s — event loop stall, continuing.\n`,
242
+ `[gsd] Lock heartbeat caught up after ${Math.round(elapsed / 1000)}s — long LLM call, no action needed.\n`,
243
243
  );
244
244
  return; // Suppress false positive
245
245
  }
@@ -299,7 +299,7 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
299
299
  const elapsed = Date.now() - _lockAcquiredAt;
300
300
  if (elapsed < 1_800_000) {
301
301
  process.stderr.write(
302
- `[gsd] Lock heartbeat mismatch after ${Math.round(elapsed / 1000)}s — event loop stall, continuing.\n`,
302
+ `[gsd] Lock heartbeat caught up after ${Math.round(elapsed / 1000)}s — long LLM call, no action needed.\n`,
303
303
  );
304
304
  return;
305
305
  }
@@ -4,7 +4,7 @@
4
4
  * - activity-log-save.test.ts (caching, dedup, collision recovery)
5
5
  */
6
6
 
7
- import test from "node:test";
7
+ import { describe, test, beforeEach, afterEach } from "node:test";
8
8
  import assert from "node:assert/strict";
9
9
  import { existsSync, mkdtempSync, mkdirSync, readdirSync, realpathSync, rmSync, utimesSync, writeFileSync, readFileSync } from "node:fs";
10
10
  import { join, dirname } from "node:path";
@@ -48,9 +48,12 @@ function createCtx(entries: unknown[]) {
48
48
 
49
49
  // ── Pruning ──────────────────────────────────────────────────────────────────
50
50
 
51
- test("pruneActivityLogs deletes old files, keeps recent and highest-seq", () => {
52
- const dir = createTmpDir();
53
- try {
51
+ describe("pruneActivityLogs", () => {
52
+ let dir: string;
53
+ beforeEach(() => { dir = createTmpDir(); });
54
+ afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
55
+
56
+ test("deletes old files, keeps recent and highest-seq", () => {
54
57
  const f001 = writeActivityFile(dir, "001", "execute-task-M001-S01-T01");
55
58
  writeActivityFile(dir, "002", "execute-task-M001-S01-T02");
56
59
  writeActivityFile(dir, "003", "execute-task-M001-S01-T03");
@@ -61,14 +64,9 @@ test("pruneActivityLogs deletes old files, keeps recent and highest-seq", () =>
61
64
  assert.ok(!remaining.includes("001-execute-task-M001-S01-T01.jsonl"));
62
65
  assert.ok(remaining.includes("002-execute-task-M001-S01-T02.jsonl"));
63
66
  assert.ok(remaining.includes("003-execute-task-M001-S01-T03.jsonl"));
64
- } finally {
65
- rmSync(dir, { recursive: true, force: true });
66
- }
67
- });
67
+ });
68
68
 
69
- test("pruneActivityLogs preserves highest-seq even when all files are old", () => {
70
- const dir = createTmpDir();
71
- try {
69
+ test("preserves highest-seq even when all files are old", () => {
72
70
  const f001 = writeActivityFile(dir, "001", "t1");
73
71
  const f002 = writeActivityFile(dir, "002", "t2");
74
72
  const f003 = writeActivityFile(dir, "003", "t3");
@@ -78,14 +76,9 @@ test("pruneActivityLogs preserves highest-seq even when all files are old", () =
78
76
  const remaining = listFiles(dir);
79
77
  assert.equal(remaining.length, 1);
80
78
  assert.ok(remaining[0].startsWith("003-"));
81
- } finally {
82
- rmSync(dir, { recursive: true, force: true });
83
- }
84
- });
79
+ });
85
80
 
86
- test("pruneActivityLogs with retentionDays=0 keeps only highest-seq", () => {
87
- const dir = createTmpDir();
88
- try {
81
+ test("with retentionDays=0 keeps only highest-seq", () => {
89
82
  writeActivityFile(dir, "001", "t1");
90
83
  writeActivityFile(dir, "002", "t2");
91
84
  writeActivityFile(dir, "003", "t3");
@@ -94,51 +87,31 @@ test("pruneActivityLogs with retentionDays=0 keeps only highest-seq", () => {
94
87
  const remaining = listFiles(dir);
95
88
  assert.equal(remaining.length, 1);
96
89
  assert.ok(remaining[0].startsWith("003-"));
97
- } finally {
98
- rmSync(dir, { recursive: true, force: true });
99
- }
100
- });
90
+ });
101
91
 
102
- test("pruneActivityLogs no-op when all files are recent", () => {
103
- const dir = createTmpDir();
104
- try {
92
+ test("no-op when all files are recent", () => {
105
93
  writeActivityFile(dir, "001", "t1");
106
94
  writeActivityFile(dir, "002", "t2");
107
95
  writeActivityFile(dir, "003", "t3");
108
96
 
109
97
  pruneActivityLogs(dir, 30);
110
98
  assert.equal(listFiles(dir).length, 3);
111
- } finally {
112
- rmSync(dir, { recursive: true, force: true });
113
- }
114
- });
99
+ });
115
100
 
116
- test("pruneActivityLogs handles empty directory", () => {
117
- const dir = createTmpDir();
118
- try {
101
+ test("handles empty directory", () => {
119
102
  assert.doesNotThrow(() => pruneActivityLogs(dir, 30));
120
103
  assert.equal(readdirSync(dir).length, 0);
121
- } finally {
122
- rmSync(dir, { recursive: true, force: true });
123
- }
124
- });
104
+ });
125
105
 
126
- test("pruneActivityLogs preserves single old file (it is highest-seq)", () => {
127
- const dir = createTmpDir();
128
- try {
106
+ test("preserves single old file (it is highest-seq)", () => {
129
107
  const f = writeActivityFile(dir, "001", "t1");
130
108
  backdateFile(f, 100);
131
109
 
132
110
  pruneActivityLogs(dir, 30);
133
111
  assert.equal(listFiles(dir).length, 1);
134
- } finally {
135
- rmSync(dir, { recursive: true, force: true });
136
- }
137
- });
112
+ });
138
113
 
139
- test("pruneActivityLogs ignores non-matching filenames", () => {
140
- const dir = createTmpDir();
141
- try {
114
+ test("ignores non-matching filenames", () => {
142
115
  const f001 = writeActivityFile(dir, "001", "t1");
143
116
  writeFileSync(join(dir, "notes.txt"), "some notes\n", "utf-8");
144
117
  backdateFile(f001, 40);
@@ -148,16 +121,17 @@ test("pruneActivityLogs ignores non-matching filenames", () => {
148
121
  assert.ok(remaining.includes("notes.txt"));
149
122
  // 001 is the only seq file, so it's highest-seq and survives
150
123
  assert.ok(remaining.includes("001-t1.jsonl"));
151
- } finally {
152
- rmSync(dir, { recursive: true, force: true });
153
- }
124
+ });
154
125
  });
155
126
 
156
127
  // ── Save: caching, dedup, collision recovery ─────────────────────────────────
157
128
 
158
- test("saveActivityLog caches sequence instead of rescanning", () => {
159
- const baseDir = createTmpDir();
160
- try {
129
+ describe("saveActivityLog", () => {
130
+ let baseDir: string;
131
+ beforeEach(() => { baseDir = createTmpDir(); });
132
+ afterEach(() => { rmSync(baseDir, { recursive: true, force: true }); });
133
+
134
+ test("caches sequence instead of rescanning", () => {
161
135
  saveActivityLog(createCtx([{ kind: "first", n: 1 }]) as any, baseDir, "execute-task", "M001/S01/T01");
162
136
  writeFileSync(join(activityDir(baseDir), "999-external.jsonl"), '{"x":1}\n', "utf-8");
163
137
  saveActivityLog(createCtx([{ kind: "second", n: 2 }]) as any, baseDir, "execute-task", "M001/S01/T02");
@@ -166,14 +140,9 @@ test("saveActivityLog caches sequence instead of rescanning", () => {
166
140
  assert.ok(files.includes("001-execute-task-M001-S01-T01.jsonl"));
167
141
  assert.ok(files.includes("002-execute-task-M001-S01-T02.jsonl"));
168
142
  assert.ok(!files.some(f => f.startsWith("1000-")));
169
- } finally {
170
- rmSync(baseDir, { recursive: true, force: true });
171
- }
172
- });
143
+ });
173
144
 
174
- test("saveActivityLog deduplicates identical snapshots for same unit", () => {
175
- const baseDir = createTmpDir();
176
- try {
145
+ test("deduplicates identical snapshots for same unit", () => {
177
146
  const ctx = createCtx([{ role: "assistant", content: "same" }]);
178
147
  saveActivityLog(ctx as any, baseDir, "plan-slice", "M002/S01");
179
148
  saveActivityLog(ctx as any, baseDir, "plan-slice", "M002/S01");
@@ -184,14 +153,9 @@ test("saveActivityLog deduplicates identical snapshots for same unit", () => {
184
153
  saveActivityLog(createCtx([{ role: "assistant", content: "changed" }]) as any, baseDir, "plan-slice", "M002/S01");
185
154
  files = listFiles(activityDir(baseDir));
186
155
  assert.equal(files.length, 2);
187
- } finally {
188
- rmSync(baseDir, { recursive: true, force: true });
189
- }
190
- });
156
+ });
191
157
 
192
- test("saveActivityLog recovers on sequence collision", () => {
193
- const baseDir = createTmpDir();
194
- try {
158
+ test("recovers on sequence collision", () => {
195
159
  saveActivityLog(createCtx([{ turn: 1 }]) as any, baseDir, "execute-task", "M003/S02/T01");
196
160
  writeFileSync(join(activityDir(baseDir), "002-execute-task-M003-S02-T02.jsonl"), '{"collision":true}\n', "utf-8");
197
161
  saveActivityLog(createCtx([{ turn: 2 }]) as any, baseDir, "execute-task", "M003/S02/T02");
@@ -199,9 +163,7 @@ test("saveActivityLog recovers on sequence collision", () => {
199
163
  const files = listFiles(activityDir(baseDir));
200
164
  assert.ok(files.includes("002-execute-task-M003-S02-T02.jsonl"));
201
165
  assert.ok(files.includes("003-execute-task-M003-S02-T02.jsonl"));
202
- } finally {
203
- rmSync(baseDir, { recursive: true, force: true });
204
- }
166
+ });
205
167
  });
206
168
 
207
169
  // ── Prompt text assertion ────────────────────────────────────────────────────