gsd-pi 2.77.0-dev.58d3d4d6c → 2.77.0-dev.cfd69e714

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 (429) hide show
  1. package/README.md +1 -1
  2. package/dist/claude-cli-check.js +5 -1
  3. package/dist/headless.js +49 -4
  4. package/dist/resource-loader.d.ts +40 -0
  5. package/dist/resource-loader.js +32 -13
  6. package/dist/resources/extensions/browser-tools/capture.js +9 -0
  7. package/dist/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +8 -59
  8. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +36 -24
  9. package/dist/resources/extensions/browser-tools/tests/capture-sharp-optional.test.cjs +69 -71
  10. package/dist/resources/extensions/browser-tools/tools/forms.js +5 -1
  11. package/dist/resources/extensions/browser-tools/tools/intent.js +5 -1
  12. package/dist/resources/extensions/claude-code-cli/readiness.js +5 -1
  13. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +481 -17
  14. package/dist/resources/extensions/gsd/auto/loop.js +43 -0
  15. package/dist/resources/extensions/gsd/auto/phases.js +15 -21
  16. package/dist/resources/extensions/gsd/auto/session.js +0 -2
  17. package/dist/resources/extensions/gsd/auto-dispatch.js +102 -24
  18. package/dist/resources/extensions/gsd/auto-model-selection.js +124 -4
  19. package/dist/resources/extensions/gsd/auto-post-unit.js +71 -64
  20. package/dist/resources/extensions/gsd/auto-prompts.js +329 -102
  21. package/dist/resources/extensions/gsd/auto-recovery.js +195 -23
  22. package/dist/resources/extensions/gsd/auto-start.js +34 -24
  23. package/dist/resources/extensions/gsd/auto-tool-tracking.js +47 -7
  24. package/dist/resources/extensions/gsd/auto-worktree.js +122 -26
  25. package/dist/resources/extensions/gsd/auto.js +31 -20
  26. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +9 -1
  27. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +209 -0
  28. package/dist/resources/extensions/gsd/bootstrap/provider-error-resume.js +3 -6
  29. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -3
  30. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +127 -9
  31. package/dist/resources/extensions/gsd/component-loader.js +447 -0
  32. package/dist/resources/extensions/gsd/component-types.js +69 -0
  33. package/dist/resources/extensions/gsd/detection.js +49 -1
  34. package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  35. package/dist/resources/extensions/gsd/gate-registry.js +2 -2
  36. package/dist/resources/extensions/gsd/git-constants.js +28 -1
  37. package/dist/resources/extensions/gsd/git-self-heal.js +27 -0
  38. package/dist/resources/extensions/gsd/git-service.js +126 -2
  39. package/dist/resources/extensions/gsd/gsd-db.js +6 -3
  40. package/dist/resources/extensions/gsd/guided-flow.js +17 -5
  41. package/dist/resources/extensions/gsd/memory-extractor.js +7 -1
  42. package/dist/resources/extensions/gsd/milestone-scope-classifier.js +299 -0
  43. package/dist/resources/extensions/gsd/model-cost-table.js +3 -0
  44. package/dist/resources/extensions/gsd/model-router.js +6 -0
  45. package/dist/resources/extensions/gsd/native-git-bridge.js +34 -4
  46. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +6 -2
  47. package/dist/resources/extensions/gsd/prompts/plan-slice.md +15 -2
  48. package/dist/resources/extensions/gsd/safety/git-checkpoint.js +11 -0
  49. package/dist/resources/extensions/gsd/service-tier.js +5 -2
  50. package/dist/resources/extensions/gsd/session-lock.js +19 -10
  51. package/dist/resources/extensions/gsd/skill-manifest.js +168 -0
  52. package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +278 -8
  53. package/dist/resources/extensions/gsd/state.js +44 -33
  54. package/dist/resources/extensions/gsd/sync-lock.js +98 -42
  55. package/dist/resources/extensions/gsd/unit-context-composer.js +147 -0
  56. package/dist/resources/extensions/gsd/unit-context-manifest.js +370 -0
  57. package/dist/resources/extensions/gsd/uok/gate-runner.js +53 -5
  58. package/dist/resources/extensions/gsd/workflow-mcp.js +6 -0
  59. package/dist/resources/extensions/gsd/worktree-manager.js +34 -8
  60. package/dist/resources/extensions/mcp-client/index.js +3 -1
  61. package/dist/resources/extensions/ollama/index.js +5 -1
  62. package/dist/resources/extensions/remote-questions/manager.js +11 -5
  63. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  64. package/dist/web/standalone/.next/BUILD_ID +1 -1
  65. package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
  66. package/dist/web/standalone/.next/build-manifest.json +2 -2
  67. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  68. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/index.html +1 -1
  86. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
  93. package/dist/web/standalone/.next/server/chunks/1926.js +1 -1
  94. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  95. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  97. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  98. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  99. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  100. package/package.json +2 -3
  101. package/packages/daemon/src/logger.ts +4 -3
  102. package/packages/mcp-server/dist/server.d.ts +24 -0
  103. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  104. package/packages/mcp-server/dist/server.js +88 -87
  105. package/packages/mcp-server/dist/server.js.map +1 -1
  106. package/packages/mcp-server/src/mcp-server.test.ts +25 -3
  107. package/packages/mcp-server/src/readers/graph.test.ts +87 -15
  108. package/packages/mcp-server/src/secure-env-collect.test.ts +232 -237
  109. package/packages/mcp-server/src/server.ts +131 -105
  110. package/packages/mcp-server/src/workflow-tools.test.ts +80 -39
  111. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  112. package/packages/native/package.json +1 -1
  113. package/packages/native/src/__tests__/_test-coverage-guard.test.mjs +98 -0
  114. package/packages/native/src/__tests__/module-compat.test.mjs +59 -27
  115. package/packages/native/src/__tests__/ps.test.mjs +14 -8
  116. package/packages/native/src/__tests__/stream-process.test.mjs +23 -2
  117. package/packages/native/src/__tests__/truncate.test.mjs +17 -2
  118. package/packages/pi-agent-core/src/agent-loop.test.ts +5 -15
  119. package/packages/pi-agent-core/src/agent.test.ts +96 -102
  120. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  121. package/packages/pi-ai/dist/models/capability-patches.d.ts.map +1 -1
  122. package/packages/pi-ai/dist/models/capability-patches.js +9 -2
  123. package/packages/pi-ai/dist/models/capability-patches.js.map +1 -1
  124. package/packages/pi-ai/dist/models/generated/index.d.ts +34 -0
  125. package/packages/pi-ai/dist/models/generated/index.d.ts.map +1 -1
  126. package/packages/pi-ai/dist/models/generated/openai-codex.d.ts +17 -0
  127. package/packages/pi-ai/dist/models/generated/openai-codex.d.ts.map +1 -1
  128. package/packages/pi-ai/dist/models/generated/openai-codex.js +17 -0
  129. package/packages/pi-ai/dist/models/generated/openai-codex.js.map +1 -1
  130. package/packages/pi-ai/dist/models/generated/openai.d.ts +17 -0
  131. package/packages/pi-ai/dist/models/generated/openai.d.ts.map +1 -1
  132. package/packages/pi-ai/dist/models/generated/openai.js +17 -0
  133. package/packages/pi-ai/dist/models/generated/openai.js.map +1 -1
  134. package/packages/pi-ai/dist/models.generated.test.js +43 -70
  135. package/packages/pi-ai/dist/models.generated.test.js.map +1 -1
  136. package/packages/pi-ai/dist/models.test.js +36 -11
  137. package/packages/pi-ai/dist/models.test.js.map +1 -1
  138. package/packages/pi-ai/scripts/generate-models.ts +44 -0
  139. package/packages/pi-ai/src/models/capability-patches.ts +10 -2
  140. package/packages/pi-ai/src/models/generated/openai-codex.ts +17 -0
  141. package/packages/pi-ai/src/models/generated/openai.ts +17 -0
  142. package/packages/pi-ai/src/models.generated.test.ts +46 -73
  143. package/packages/pi-ai/src/models.test.ts +48 -11
  144. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  145. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +96 -32
  146. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +75 -12
  148. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -1
  149. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +99 -31
  150. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
  151. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +5 -0
  152. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  153. package/packages/pi-coding-agent/dist/core/extensions/loader.js +61 -0
  154. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +30 -4
  156. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +17 -0
  158. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/resource-loader-cache-reset.test.js +76 -18
  160. package/packages/pi-coding-agent/dist/core/resource-loader-cache-reset.test.js.map +1 -1
  161. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  162. package/packages/pi-coding-agent/dist/core/retry-handler.js +2 -6
  163. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  164. package/packages/pi-coding-agent/dist/core/retry-handler.test.js +5 -1
  165. package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
  166. package/packages/pi-coding-agent/dist/core/retryable-error-regex.d.ts +18 -0
  167. package/packages/pi-coding-agent/dist/core/retryable-error-regex.d.ts.map +1 -0
  168. package/packages/pi-coding-agent/dist/core/retryable-error-regex.js +18 -0
  169. package/packages/pi-coding-agent/dist/core/retryable-error-regex.js.map +1 -0
  170. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +20 -0
  171. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  172. package/packages/pi-coding-agent/dist/core/system-prompt.js +16 -2
  173. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/index.d.ts +1 -0
  175. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  176. package/packages/pi-coding-agent/dist/index.js +1 -0
  177. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  178. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +20 -13
  179. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -1
  180. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  181. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +18 -3
  182. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  183. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.js +125 -0
  184. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.js.map +1 -1
  185. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +2 -0
  186. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  187. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  188. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
  189. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  190. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +105 -13
  191. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  192. package/packages/pi-coding-agent/dist/tests/system-prompt-skill-filter.test.d.ts +2 -0
  193. package/packages/pi-coding-agent/dist/tests/system-prompt-skill-filter.test.d.ts.map +1 -0
  194. package/packages/pi-coding-agent/dist/tests/system-prompt-skill-filter.test.js +130 -0
  195. package/packages/pi-coding-agent/dist/tests/system-prompt-skill-filter.test.js.map +1 -0
  196. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +113 -37
  197. package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +89 -17
  198. package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +112 -43
  199. package/packages/pi-coding-agent/src/core/extensions/loader.ts +58 -0
  200. package/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +35 -4
  201. package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +20 -0
  202. package/packages/pi-coding-agent/src/core/resource-loader-cache-reset.test.ts +93 -28
  203. package/packages/pi-coding-agent/src/core/retry-handler.test.ts +5 -1
  204. package/packages/pi-coding-agent/src/core/retry-handler.ts +2 -8
  205. package/packages/pi-coding-agent/src/core/retryable-error-regex.ts +18 -0
  206. package/packages/pi-coding-agent/src/core/system-prompt.ts +35 -1
  207. package/packages/pi-coding-agent/src/index.ts +1 -0
  208. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +26 -20
  209. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.test.ts +146 -1
  210. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +20 -3
  211. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +2 -0
  212. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +119 -13
  213. package/packages/pi-coding-agent/src/tests/system-prompt-skill-filter.test.ts +157 -0
  214. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  215. package/packages/pi-tui/dist/__tests__/autocomplete.test.js +18 -8
  216. package/packages/pi-tui/dist/__tests__/autocomplete.test.js.map +1 -1
  217. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js +128 -17
  218. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js.map +1 -1
  219. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js +36 -12
  220. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js.map +1 -1
  221. package/packages/pi-tui/dist/__tests__/tui.test.js +18 -30
  222. package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
  223. package/packages/pi-tui/dist/components/__tests__/input.test.js +10 -3
  224. package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
  225. package/packages/pi-tui/dist/components/__tests__/loader.test.js +53 -9
  226. package/packages/pi-tui/dist/components/__tests__/loader.test.js.map +1 -1
  227. package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js +6 -2
  228. package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js.map +1 -1
  229. package/packages/pi-tui/dist/components/editor.d.ts +14 -0
  230. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  231. package/packages/pi-tui/dist/components/editor.js +19 -0
  232. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  233. package/packages/pi-tui/dist/components/image.test.js +6 -5
  234. package/packages/pi-tui/dist/components/image.test.js.map +1 -1
  235. package/packages/pi-tui/dist/editor-component.d.ts +2 -0
  236. package/packages/pi-tui/dist/editor-component.d.ts.map +1 -1
  237. package/packages/pi-tui/dist/editor-component.js.map +1 -1
  238. package/packages/pi-tui/src/__tests__/autocomplete.test.ts +24 -8
  239. package/packages/pi-tui/src/__tests__/overlay-layout.test.ts +140 -17
  240. package/packages/pi-tui/src/__tests__/stdin-buffer.test.ts +41 -12
  241. package/packages/pi-tui/src/__tests__/tui.test.ts +18 -37
  242. package/packages/pi-tui/src/components/__tests__/input.test.ts +19 -3
  243. package/packages/pi-tui/src/components/__tests__/loader.test.ts +112 -35
  244. package/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts +9 -2
  245. package/packages/pi-tui/src/components/editor.ts +22 -0
  246. package/packages/pi-tui/src/components/image.test.ts +10 -5
  247. package/packages/pi-tui/src/editor-component.ts +3 -0
  248. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  249. package/packages/rpc-client/dist/rpc-client.test.js +101 -51
  250. package/packages/rpc-client/dist/rpc-client.test.js.map +1 -1
  251. package/packages/rpc-client/src/rpc-client.test.ts +109 -52
  252. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
  253. package/scripts/install.js +15 -1
  254. package/src/resources/extensions/browser-tools/capture.ts +12 -0
  255. package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +8 -59
  256. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +36 -24
  257. package/src/resources/extensions/browser-tools/tests/capture-sharp-optional.test.cjs +69 -71
  258. package/src/resources/extensions/browser-tools/tools/forms.ts +5 -1
  259. package/src/resources/extensions/browser-tools/tools/intent.ts +5 -1
  260. package/src/resources/extensions/claude-code-cli/readiness.ts +5 -1
  261. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +518 -19
  262. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +919 -75
  263. package/src/resources/extensions/github-sync/tests/cli.test.ts +76 -7
  264. package/src/resources/extensions/github-sync/tests/templates.test.ts +33 -1
  265. package/src/resources/extensions/gsd/auto/loop.ts +47 -0
  266. package/src/resources/extensions/gsd/auto/phases.ts +16 -20
  267. package/src/resources/extensions/gsd/auto/session.ts +0 -2
  268. package/src/resources/extensions/gsd/auto-dispatch.ts +113 -24
  269. package/src/resources/extensions/gsd/auto-model-selection.ts +131 -4
  270. package/src/resources/extensions/gsd/auto-post-unit.ts +82 -73
  271. package/src/resources/extensions/gsd/auto-prompts.ts +330 -90
  272. package/src/resources/extensions/gsd/auto-recovery.ts +225 -24
  273. package/src/resources/extensions/gsd/auto-start.ts +54 -6
  274. package/src/resources/extensions/gsd/auto-tool-tracking.ts +51 -7
  275. package/src/resources/extensions/gsd/auto-worktree.ts +130 -26
  276. package/src/resources/extensions/gsd/auto.ts +43 -22
  277. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +9 -1
  278. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +221 -0
  279. package/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +3 -7
  280. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +7 -3
  281. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +158 -9
  282. package/src/resources/extensions/gsd/component-loader.ts +598 -0
  283. package/src/resources/extensions/gsd/component-types.ts +362 -0
  284. package/src/resources/extensions/gsd/detection.ts +58 -1
  285. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  286. package/src/resources/extensions/gsd/gate-registry.ts +2 -2
  287. package/src/resources/extensions/gsd/git-constants.ts +30 -1
  288. package/src/resources/extensions/gsd/git-self-heal.ts +31 -0
  289. package/src/resources/extensions/gsd/git-service.ts +133 -2
  290. package/src/resources/extensions/gsd/gsd-db.ts +6 -3
  291. package/src/resources/extensions/gsd/guided-flow.ts +20 -5
  292. package/src/resources/extensions/gsd/memory-extractor.ts +11 -3
  293. package/src/resources/extensions/gsd/milestone-scope-classifier.ts +366 -0
  294. package/src/resources/extensions/gsd/model-cost-table.ts +3 -0
  295. package/src/resources/extensions/gsd/model-router.ts +6 -0
  296. package/src/resources/extensions/gsd/native-git-bridge.ts +34 -4
  297. package/src/resources/extensions/gsd/prompts/complete-milestone.md +6 -2
  298. package/src/resources/extensions/gsd/prompts/plan-slice.md +15 -2
  299. package/src/resources/extensions/gsd/safety/git-checkpoint.ts +15 -0
  300. package/src/resources/extensions/gsd/service-tier.ts +5 -2
  301. package/src/resources/extensions/gsd/session-lock.ts +20 -10
  302. package/src/resources/extensions/gsd/skill-manifest.ts +175 -0
  303. package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +309 -8
  304. package/src/resources/extensions/gsd/state.ts +49 -44
  305. package/src/resources/extensions/gsd/sync-lock.ts +97 -39
  306. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +270 -0
  307. package/src/resources/extensions/gsd/tests/auto-deterministic-error-classification-4973.test.ts +341 -0
  308. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +264 -0
  309. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +94 -289
  310. package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +742 -0
  311. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +78 -0
  312. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +61 -0
  313. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +93 -0
  314. package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +8 -197
  315. package/src/resources/extensions/gsd/tests/auto-start-needs-discussion.test.ts +15 -58
  316. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +17 -21
  317. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +263 -0
  318. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +25 -0
  319. package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +192 -0
  320. package/src/resources/extensions/gsd/tests/complete-task.test.ts +16 -8
  321. package/src/resources/extensions/gsd/tests/component-loader.test.ts +589 -0
  322. package/src/resources/extensions/gsd/tests/component-types.test.ts +127 -0
  323. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +50 -1
  324. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -3
  325. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +40 -0
  326. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +91 -3
  327. package/src/resources/extensions/gsd/tests/derive-state.test.ts +4 -3
  328. package/src/resources/extensions/gsd/tests/dispatcher-stuck-planning.test.ts +3 -2
  329. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +139 -129
  330. package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +9 -105
  331. package/src/resources/extensions/gsd/tests/gate-state-canonicalization.test.ts +102 -0
  332. package/src/resources/extensions/gsd/tests/gate-storage.test.ts +1 -1
  333. package/src/resources/extensions/gsd/tests/hook-key-parsing.test.ts +4 -55
  334. package/src/resources/extensions/gsd/tests/integration/all-milestones-complete-merge.test.ts +7 -57
  335. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +20 -0
  336. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +18 -2
  337. package/src/resources/extensions/gsd/tests/integration/queue-completed-milestone-perf.test.ts +10 -4
  338. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +144 -7
  339. package/src/resources/extensions/gsd/tests/integration/state-machine-live-validation.test.ts +4 -0
  340. package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +2 -16
  341. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +6 -9
  342. package/src/resources/extensions/gsd/tests/mcp-client-security.test.ts +8 -37
  343. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +5 -15
  344. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +227 -62
  345. package/src/resources/extensions/gsd/tests/milestone-scope-classifier.test.ts +187 -0
  346. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +9 -1
  347. package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
  348. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +6 -49
  349. package/src/resources/extensions/gsd/tests/notification-widget.test.ts +6 -3
  350. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +273 -133
  351. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +301 -0
  352. package/src/resources/extensions/gsd/tests/pre-execution-pause-wiring.test.ts +32 -1
  353. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +23 -24
  354. package/src/resources/extensions/gsd/tests/queue-auto-guard.test.ts +32 -0
  355. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +75 -2
  356. package/src/resources/extensions/gsd/tests/reassess-default-optin.test.ts +132 -0
  357. package/src/resources/extensions/gsd/tests/recovery-attempts-reset.test.ts +8 -40
  358. package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +136 -256
  359. package/src/resources/extensions/gsd/tests/research-milestone-composer.test.ts +114 -0
  360. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +148 -0
  361. package/src/resources/extensions/gsd/tests/service-tier.test.ts +4 -0
  362. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +29 -0
  363. package/src/resources/extensions/gsd/tests/silent-catch-diagnostics.test.ts +55 -95
  364. package/src/resources/extensions/gsd/tests/single-writer-v3-tool-surface.test.ts +158 -0
  365. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +120 -1
  366. package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +112 -0
  367. package/src/resources/extensions/gsd/tests/slice-parallel-orchestrator.test.ts +164 -1
  368. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -5
  369. package/src/resources/extensions/gsd/tests/structured-data-formatter.test.ts +11 -92
  370. package/src/resources/extensions/gsd/tests/survivor-branch-complete.test.ts +102 -101
  371. package/src/resources/extensions/gsd/tests/sync-lock.test.ts +31 -0
  372. package/src/resources/extensions/gsd/tests/test-helpers.test.ts +12 -61
  373. package/src/resources/extensions/gsd/tests/test-helpers.ts +21 -8
  374. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +61 -1
  375. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +8 -1
  376. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +355 -0
  377. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +258 -0
  378. package/src/resources/extensions/gsd/tests/uok-gate-runner.test.ts +75 -0
  379. package/src/resources/extensions/gsd/tests/uok-gitops-wiring.test.ts +49 -26
  380. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +1 -0
  381. package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +144 -81
  382. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +20 -54
  383. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +342 -277
  384. package/src/resources/extensions/gsd/tests/worker-model-override.test.ts +37 -29
  385. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +226 -266
  386. package/src/resources/extensions/gsd/tests/worktree-health-monorepo.test.ts +103 -67
  387. package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +92 -90
  388. package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +238 -59
  389. package/src/resources/extensions/gsd/tests/worktree-sync-overwrite-loop.test.ts +113 -161
  390. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +262 -0
  391. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +186 -0
  392. package/src/resources/extensions/gsd/tests/write-gate.test.ts +7 -5
  393. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +80 -96
  394. package/src/resources/extensions/gsd/types.ts +3 -3
  395. package/src/resources/extensions/gsd/unit-context-composer.ts +218 -0
  396. package/src/resources/extensions/gsd/unit-context-manifest.ts +574 -0
  397. package/src/resources/extensions/gsd/uok/gate-runner.ts +65 -5
  398. package/src/resources/extensions/gsd/workflow-mcp.ts +6 -0
  399. package/src/resources/extensions/gsd/worktree-manager.ts +55 -7
  400. package/src/resources/extensions/mcp-client/index.ts +3 -1
  401. package/src/resources/extensions/mcp-client/tests/server-name-spaces.test.ts +70 -36
  402. package/src/resources/extensions/ollama/index.ts +5 -1
  403. package/src/resources/extensions/ollama/ollama-auth-mode.test.ts +123 -15
  404. package/src/resources/extensions/ollama/ollama-status-indicator.test.ts +206 -19
  405. package/src/resources/extensions/remote-questions/manager.ts +36 -4
  406. package/src/resources/extensions/remote-questions/tests/command-polling.test.ts +200 -190
  407. package/src/resources/extensions/shared/tests/interview-preview.test.ts +11 -3
  408. package/src/resources/extensions/voice/tests/linux-ready.test.ts +129 -113
  409. package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.d.ts +0 -2
  410. package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.d.ts.map +0 -1
  411. package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.js +0 -289
  412. package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.js.map +0 -1
  413. package/packages/pi-ai/src/utils/oauth/oauth-providers.test.ts +0 -363
  414. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +0 -144
  415. package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +0 -157
  416. package/src/resources/extensions/gsd/tests/dashboard-model-label-ordering.test.ts +0 -107
  417. package/src/resources/extensions/gsd/tests/find-missing-summaries-closed.test.ts +0 -48
  418. package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +0 -159
  419. package/src/resources/extensions/gsd/tests/forensics-db-completion.test.ts +0 -96
  420. package/src/resources/extensions/gsd/tests/forensics-dedup.test.ts +0 -79
  421. package/src/resources/extensions/gsd/tests/forensics-hook-key-parse.test.ts +0 -75
  422. package/src/resources/extensions/gsd/tests/forensics-journal.test.ts +0 -162
  423. package/src/resources/extensions/gsd/tests/forensics-worktree-telemetry.test.ts +0 -145
  424. package/src/resources/extensions/gsd/tests/gitignore-bg-shell.test.ts +0 -38
  425. package/src/resources/extensions/gsd/tests/gsd-no-project-error.test.ts +0 -73
  426. package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +0 -130
  427. package/src/resources/extensions/gsd/tests/import-done-milestones.test.ts +0 -43
  428. /package/dist/web/standalone/.next/static/{Cev5xrAYA3ZGTRLyjR2fX → SvCJDZPQW104bR1KnBQg1}/_buildManifest.js +0 -0
  429. /package/dist/web/standalone/.next/static/{Cev5xrAYA3ZGTRLyjR2fX → SvCJDZPQW104bR1KnBQg1}/_ssgManifest.js +0 -0
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=system-prompt-skill-filter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"system-prompt-skill-filter.test.d.ts","sourceRoot":"","sources":["../../src/tests/system-prompt-skill-filter.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,130 @@
1
+ // @gsd/pi-coding-agent + system-prompt-skill-filter.test — coverage for the
2
+ // optional `skillFilter` option added to buildSystemPrompt (RFC #4779). The
3
+ // filter lets consumers narrow the <available_skills> catalog rendered into
4
+ // the cached system prompt without touching skill loading or invocation.
5
+ import test from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { buildSystemPrompt } from "../core/system-prompt.js";
8
+ function makeSkill(name, description = `description for ${name}`) {
9
+ return {
10
+ name,
11
+ description,
12
+ filePath: `/tmp/${name}/SKILL.md`,
13
+ baseDir: `/tmp/${name}`,
14
+ source: "project",
15
+ disableModelInvocation: false,
16
+ };
17
+ }
18
+ function extractAvailableSkills(prompt) {
19
+ const start = prompt.indexOf("<available_skills>");
20
+ const end = prompt.indexOf("</available_skills>");
21
+ if (start === -1 || end === -1)
22
+ return "";
23
+ return prompt.slice(start, end + "</available_skills>".length);
24
+ }
25
+ // ─── Default branch (no customPrompt) ──────────────────────────────────────
26
+ test("buildSystemPrompt: skillFilter omits filtered-out skills from <available_skills>", () => {
27
+ const skills = [makeSkill("alpha"), makeSkill("beta"), makeSkill("gamma")];
28
+ const prompt = buildSystemPrompt({
29
+ skills,
30
+ selectedTools: ["read", "Skill"],
31
+ skillFilter: skill => skill.name !== "beta",
32
+ });
33
+ const section = extractAvailableSkills(prompt);
34
+ assert.ok(section.length > 0, "catalog section should render");
35
+ assert.match(section, /<name>alpha<\/name>/);
36
+ assert.match(section, /<name>gamma<\/name>/);
37
+ assert.doesNotMatch(section, /<name>beta<\/name>/);
38
+ });
39
+ test("buildSystemPrompt: skillFilter omitted preserves pre-filter behavior (all skills render)", () => {
40
+ const skills = [makeSkill("alpha"), makeSkill("beta")];
41
+ const prompt = buildSystemPrompt({
42
+ skills,
43
+ selectedTools: ["read", "Skill"],
44
+ });
45
+ const section = extractAvailableSkills(prompt);
46
+ assert.match(section, /<name>alpha<\/name>/);
47
+ assert.match(section, /<name>beta<\/name>/);
48
+ });
49
+ test("buildSystemPrompt: skillFilter that rejects every skill suppresses the <available_skills> block", () => {
50
+ const skills = [makeSkill("alpha"), makeSkill("beta")];
51
+ const prompt = buildSystemPrompt({
52
+ skills,
53
+ selectedTools: ["read", "Skill"],
54
+ skillFilter: () => false,
55
+ });
56
+ // With zero visible skills, formatSkillsForPrompt returns an empty string,
57
+ // so the opening tag should not appear anywhere.
58
+ assert.ok(!prompt.includes("<available_skills>"));
59
+ });
60
+ // ─── Custom-prompt branch ──────────────────────────────────────────────────
61
+ test("buildSystemPrompt (customPrompt): skillFilter applies to the catalog appended onto a custom prompt", () => {
62
+ const skills = [makeSkill("alpha"), makeSkill("beta"), makeSkill("gamma")];
63
+ const prompt = buildSystemPrompt({
64
+ customPrompt: "CUSTOM BASE",
65
+ skills,
66
+ selectedTools: ["read", "Skill"],
67
+ skillFilter: skill => skill.name === "alpha",
68
+ });
69
+ const section = extractAvailableSkills(prompt);
70
+ assert.match(section, /<name>alpha<\/name>/);
71
+ assert.doesNotMatch(section, /<name>beta<\/name>/);
72
+ assert.doesNotMatch(section, /<name>gamma<\/name>/);
73
+ });
74
+ // ─── Interaction with disableModelInvocation ──────────────────────────────
75
+ test("buildSystemPrompt: skillFilter composes with disableModelInvocation (both must pass)", () => {
76
+ // A skill already hidden from the catalog by disableModelInvocation must
77
+ // remain hidden even if skillFilter would otherwise admit it. The filter
78
+ // narrows, it does not override the existing invisibility contract.
79
+ const skills = [
80
+ { ...makeSkill("visible"), disableModelInvocation: false },
81
+ { ...makeSkill("hidden"), disableModelInvocation: true },
82
+ ];
83
+ const prompt = buildSystemPrompt({
84
+ skills,
85
+ selectedTools: ["read", "Skill"],
86
+ skillFilter: () => true,
87
+ });
88
+ const section = extractAvailableSkills(prompt);
89
+ assert.match(section, /<name>visible<\/name>/);
90
+ assert.doesNotMatch(section, /<name>hidden<\/name>/);
91
+ });
92
+ // ─── Pass-through of non-filtered fields ──────────────────────────────────
93
+ test("buildSystemPrompt: skillFilter does not affect context files or cwd rendering", () => {
94
+ const skills = [makeSkill("alpha")];
95
+ const prompt = buildSystemPrompt({
96
+ skills,
97
+ cwd: "/tmp/example",
98
+ contextFiles: [{ path: "CLAUDE.md", content: "project instructions" }],
99
+ selectedTools: ["read", "Skill"],
100
+ skillFilter: () => false,
101
+ });
102
+ assert.ok(prompt.includes("/tmp/example"), "cwd should still render");
103
+ assert.ok(prompt.includes("project instructions"), "context files should still render");
104
+ assert.ok(!prompt.includes("<available_skills>"), "no skill catalog when filter rejects all");
105
+ });
106
+ // ─── Exception safety ─────────────────────────────────────────────────────
107
+ test("buildSystemPrompt: skillFilter that throws falls back to unfiltered list and does not propagate", (t) => {
108
+ // A buggy consumer predicate must not bubble out of buildSystemPrompt.
109
+ // If it did, _rebuildSystemPrompt could unwind mid-setTools() and leave
110
+ // the session with updated tools but a stale system prompt.
111
+ const skills = [makeSkill("alpha"), makeSkill("beta")];
112
+ // Suppress the console.warn the fallback emits so test output stays clean.
113
+ const originalWarn = console.warn;
114
+ const warnings = [];
115
+ console.warn = (...args) => { warnings.push(args.join(" ")); };
116
+ t.after(() => { console.warn = originalWarn; });
117
+ let prompt = "";
118
+ assert.doesNotThrow(() => {
119
+ prompt = buildSystemPrompt({
120
+ skills,
121
+ selectedTools: ["read", "Skill"],
122
+ skillFilter: () => { throw new Error("consumer bug"); },
123
+ });
124
+ });
125
+ const section = extractAvailableSkills(prompt);
126
+ assert.match(section, /<name>alpha<\/name>/, "alpha should render (fallback to unfiltered)");
127
+ assert.match(section, /<name>beta<\/name>/, "beta should render (fallback to unfiltered)");
128
+ assert.ok(warnings.some(w => w.includes("skillFilter threw") && w.includes("consumer bug")), "fallback should emit an identifying warning");
129
+ });
130
+ //# sourceMappingURL=system-prompt-skill-filter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"system-prompt-skill-filter.test.js","sourceRoot":"","sources":["../../src/tests/system-prompt-skill-filter.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @gsd/pi-coding-agent + system-prompt-skill-filter.test — coverage for the\n// optional `skillFilter` option added to buildSystemPrompt (RFC #4779). The\n// filter lets consumers narrow the <available_skills> catalog rendered into\n// the cached system prompt without touching skill loading or invocation.\n\nimport test from \"node:test\";\nimport assert from \"node:assert/strict\";\n\nimport { buildSystemPrompt } from \"../core/system-prompt.js\";\nimport type { Skill } from \"../core/skills.js\";\n\nfunction makeSkill(name: string, description = `description for ${name}`): Skill {\n\treturn {\n\t\tname,\n\t\tdescription,\n\t\tfilePath: `/tmp/${name}/SKILL.md`,\n\t\tbaseDir: `/tmp/${name}`,\n\t\tsource: \"project\",\n\t\tdisableModelInvocation: false,\n\t};\n}\n\nfunction extractAvailableSkills(prompt: string): string {\n\tconst start = prompt.indexOf(\"<available_skills>\");\n\tconst end = prompt.indexOf(\"</available_skills>\");\n\tif (start === -1 || end === -1) return \"\";\n\treturn prompt.slice(start, end + \"</available_skills>\".length);\n}\n\n// ─── Default branch (no customPrompt) ──────────────────────────────────────\n\ntest(\"buildSystemPrompt: skillFilter omits filtered-out skills from <available_skills>\", () => {\n\tconst skills = [makeSkill(\"alpha\"), makeSkill(\"beta\"), makeSkill(\"gamma\")];\n\tconst prompt = buildSystemPrompt({\n\t\tskills,\n\t\tselectedTools: [\"read\", \"Skill\"],\n\t\tskillFilter: skill => skill.name !== \"beta\",\n\t});\n\n\tconst section = extractAvailableSkills(prompt);\n\tassert.ok(section.length > 0, \"catalog section should render\");\n\tassert.match(section, /<name>alpha<\\/name>/);\n\tassert.match(section, /<name>gamma<\\/name>/);\n\tassert.doesNotMatch(section, /<name>beta<\\/name>/);\n});\n\ntest(\"buildSystemPrompt: skillFilter omitted preserves pre-filter behavior (all skills render)\", () => {\n\tconst skills = [makeSkill(\"alpha\"), makeSkill(\"beta\")];\n\tconst prompt = buildSystemPrompt({\n\t\tskills,\n\t\tselectedTools: [\"read\", \"Skill\"],\n\t});\n\n\tconst section = extractAvailableSkills(prompt);\n\tassert.match(section, /<name>alpha<\\/name>/);\n\tassert.match(section, /<name>beta<\\/name>/);\n});\n\ntest(\"buildSystemPrompt: skillFilter that rejects every skill suppresses the <available_skills> block\", () => {\n\tconst skills = [makeSkill(\"alpha\"), makeSkill(\"beta\")];\n\tconst prompt = buildSystemPrompt({\n\t\tskills,\n\t\tselectedTools: [\"read\", \"Skill\"],\n\t\tskillFilter: () => false,\n\t});\n\n\t// With zero visible skills, formatSkillsForPrompt returns an empty string,\n\t// so the opening tag should not appear anywhere.\n\tassert.ok(!prompt.includes(\"<available_skills>\"));\n});\n\n// ─── Custom-prompt branch ──────────────────────────────────────────────────\n\ntest(\"buildSystemPrompt (customPrompt): skillFilter applies to the catalog appended onto a custom prompt\", () => {\n\tconst skills = [makeSkill(\"alpha\"), makeSkill(\"beta\"), makeSkill(\"gamma\")];\n\tconst prompt = buildSystemPrompt({\n\t\tcustomPrompt: \"CUSTOM BASE\",\n\t\tskills,\n\t\tselectedTools: [\"read\", \"Skill\"],\n\t\tskillFilter: skill => skill.name === \"alpha\",\n\t});\n\n\tconst section = extractAvailableSkills(prompt);\n\tassert.match(section, /<name>alpha<\\/name>/);\n\tassert.doesNotMatch(section, /<name>beta<\\/name>/);\n\tassert.doesNotMatch(section, /<name>gamma<\\/name>/);\n});\n\n// ─── Interaction with disableModelInvocation ──────────────────────────────\n\ntest(\"buildSystemPrompt: skillFilter composes with disableModelInvocation (both must pass)\", () => {\n\t// A skill already hidden from the catalog by disableModelInvocation must\n\t// remain hidden even if skillFilter would otherwise admit it. The filter\n\t// narrows, it does not override the existing invisibility contract.\n\tconst skills: Skill[] = [\n\t\t{ ...makeSkill(\"visible\"), disableModelInvocation: false },\n\t\t{ ...makeSkill(\"hidden\"), disableModelInvocation: true },\n\t];\n\tconst prompt = buildSystemPrompt({\n\t\tskills,\n\t\tselectedTools: [\"read\", \"Skill\"],\n\t\tskillFilter: () => true,\n\t});\n\n\tconst section = extractAvailableSkills(prompt);\n\tassert.match(section, /<name>visible<\\/name>/);\n\tassert.doesNotMatch(section, /<name>hidden<\\/name>/);\n});\n\n// ─── Pass-through of non-filtered fields ──────────────────────────────────\n\ntest(\"buildSystemPrompt: skillFilter does not affect context files or cwd rendering\", () => {\n\tconst skills = [makeSkill(\"alpha\")];\n\tconst prompt = buildSystemPrompt({\n\t\tskills,\n\t\tcwd: \"/tmp/example\",\n\t\tcontextFiles: [{ path: \"CLAUDE.md\", content: \"project instructions\" }],\n\t\tselectedTools: [\"read\", \"Skill\"],\n\t\tskillFilter: () => false,\n\t});\n\n\tassert.ok(prompt.includes(\"/tmp/example\"), \"cwd should still render\");\n\tassert.ok(prompt.includes(\"project instructions\"), \"context files should still render\");\n\tassert.ok(!prompt.includes(\"<available_skills>\"), \"no skill catalog when filter rejects all\");\n});\n\n// ─── Exception safety ─────────────────────────────────────────────────────\n\ntest(\"buildSystemPrompt: skillFilter that throws falls back to unfiltered list and does not propagate\", (t) => {\n\t// A buggy consumer predicate must not bubble out of buildSystemPrompt.\n\t// If it did, _rebuildSystemPrompt could unwind mid-setTools() and leave\n\t// the session with updated tools but a stale system prompt.\n\tconst skills = [makeSkill(\"alpha\"), makeSkill(\"beta\")];\n\n\t// Suppress the console.warn the fallback emits so test output stays clean.\n\tconst originalWarn = console.warn;\n\tconst warnings: string[] = [];\n\tconsole.warn = (...args: unknown[]) => { warnings.push(args.join(\" \")); };\n\tt.after(() => { console.warn = originalWarn; });\n\n\tlet prompt = \"\";\n\tassert.doesNotThrow(() => {\n\t\tprompt = buildSystemPrompt({\n\t\t\tskills,\n\t\t\tselectedTools: [\"read\", \"Skill\"],\n\t\t\tskillFilter: () => { throw new Error(\"consumer bug\"); },\n\t\t});\n\t});\n\n\tconst section = extractAvailableSkills(prompt);\n\tassert.match(section, /<name>alpha<\\/name>/, \"alpha should render (fallback to unfiltered)\");\n\tassert.match(section, /<name>beta<\\/name>/, \"beta should render (fallback to unfiltered)\");\n\tassert.ok(\n\t\twarnings.some(w => w.includes(\"skillFilter threw\") && w.includes(\"consumer bug\")),\n\t\t\"fallback should emit an identifying warning\",\n\t);\n});\n"]}
@@ -1,57 +1,133 @@
1
- import test, { describe } from "node:test";
1
+ // Regression test for #4243 abort() must be called BEFORE
2
+ // _disconnectFromAgent() inside newSession() and switchSession() so that
3
+ // message_end/agent_end events (and the #4216 finalization code) fire
4
+ // before we unsubscribe from the event bus.
5
+ //
6
+ // Verified behaviourally: we construct a real AgentSession, wrap `abort`
7
+ // and `_disconnectFromAgent` with call-order recording, trigger each
8
+ // session-transition method, and assert the observed call order.
9
+
2
10
  import assert from "node:assert/strict";
3
- import { readFileSync } from "node:fs";
11
+ import { mkdtempSync, rmSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
4
13
  import { join } from "node:path";
14
+ import { afterEach, beforeEach, describe, it } from "node:test";
15
+
16
+ import { Agent } from "@gsd/pi-agent-core";
17
+ import { AgentSession } from "./agent-session.js";
18
+ import { AuthStorage } from "./auth-storage.js";
19
+ import { ModelRegistry } from "./model-registry.js";
20
+ import { DefaultResourceLoader } from "./resource-loader.js";
21
+ import { SessionManager } from "./session-manager.js";
22
+ import { SettingsManager } from "./settings-manager.js";
5
23
 
6
- const source = readFileSync(
7
- join(process.cwd(), "packages/pi-coding-agent/src/core/agent-session.ts"),
8
- "utf-8",
9
- );
24
+ let testDir: string;
10
25
 
11
- describe("#4243 abort() must be called before _disconnectFromAgent()", () => {
12
- test("newSession() calls abort() before _disconnectFromAgent()", () => {
13
- // Find the newSession method body where the fix was applied
14
- const newSessionStart = source.indexOf("async newSession(options?:");
15
- assert.ok(newSessionStart >= 0, "should find newSession method");
26
+ async function createSession(opts: { persistSessions?: boolean } = {}): Promise<AgentSession> {
27
+ const agentDir = join(testDir, "agent-home");
28
+ const authStorage = AuthStorage.inMemory({});
29
+ const modelRegistry = new ModelRegistry(authStorage, join(agentDir, "models.json"));
30
+ const settingsManager = SettingsManager.inMemory();
31
+ const resourceLoader = new DefaultResourceLoader({
32
+ cwd: testDir,
33
+ agentDir,
34
+ settingsManager,
35
+ noExtensions: true,
36
+ noPromptTemplates: true,
37
+ noThemes: true,
38
+ });
39
+ await resourceLoader.reload();
16
40
 
17
- // Get a window that includes the abort/disconnect section.
18
- // Use 2000 chars to accommodate guard checks inserted between the two calls.
19
- const window = source.slice(newSessionStart, newSessionStart + 2000);
41
+ // switchSession() needs a sessionFile; in-memory manager returns undefined.
42
+ // Use file-backed manager when the test needs to resume.
43
+ const sessionManager = opts.persistSessions
44
+ ? SessionManager.create(testDir, join(testDir, "sessions"))
45
+ : SessionManager.inMemory(testDir);
46
+
47
+ return new AgentSession({
48
+ agent: new Agent(),
49
+ sessionManager,
50
+ settingsManager,
51
+ cwd: testDir,
52
+ resourceLoader,
53
+ modelRegistry,
54
+ });
55
+ }
20
56
 
21
- // Find the abort and _disconnectFromAgent calls
22
- const abortIdx = window.indexOf("await this.abort();");
23
- const disconnectIdx = window.indexOf("this._disconnectFromAgent();");
57
+ /**
58
+ * Wrap two methods on the same object so their call order is recorded.
59
+ * Returns the recording array — assertions use index lookups.
60
+ */
61
+ function recordCallOrder<O extends object>(
62
+ target: O,
63
+ methods: Array<keyof O>,
64
+ ): string[] {
65
+ const order: string[] = [];
66
+ for (const method of methods) {
67
+ const name = String(method);
68
+ const original = (target as any)[name] as (...args: unknown[]) => unknown;
69
+ if (typeof original !== "function") {
70
+ throw new Error(`recordCallOrder: ${name} is not a function on target`);
71
+ }
72
+ (target as any)[name] = function (this: O, ...args: unknown[]) {
73
+ order.push(name);
74
+ return original.apply(this, args);
75
+ };
76
+ }
77
+ return order;
78
+ }
79
+
80
+ describe("#4243 — abort() must run before _disconnectFromAgent()", () => {
81
+ beforeEach(() => {
82
+ testDir = mkdtempSync(join(tmpdir(), "agent-session-abort-"));
83
+ });
84
+
85
+ afterEach(() => {
86
+ rmSync(testDir, { recursive: true, force: true });
87
+ });
24
88
 
25
- assert.ok(abortIdx >= 0, "newSession should call await this.abort()");
26
- assert.ok(disconnectIdx >= 0, "newSession should call this._disconnectFromAgent()");
89
+ it("newSession() invokes abort() before _disconnectFromAgent()", async () => {
90
+ const session = await createSession();
91
+ const order = recordCallOrder(session as any, ["abort", "_disconnectFromAgent"]);
92
+
93
+ const ok = await session.newSession();
94
+ assert.equal(ok, true);
95
+
96
+ const abortIdx = order.indexOf("abort");
97
+ const disconnectIdx = order.indexOf("_disconnectFromAgent");
98
+ assert.ok(abortIdx >= 0, `newSession should call abort(); order=${order.join(",")}`);
99
+ assert.ok(
100
+ disconnectIdx >= 0,
101
+ `newSession should call _disconnectFromAgent(); order=${order.join(",")}`,
102
+ );
27
103
  assert.ok(
28
104
  abortIdx < disconnectIdx,
29
- "abort() must be called BEFORE _disconnectFromAgent() so that message_end/agent_end events fire before unsubscribing from the event bus",
105
+ `abort() must run before _disconnectFromAgent(); order=${order.join(",")}`,
30
106
  );
31
107
  });
32
108
 
33
- test("newSession() references #4243 in the abort/disconnect comment", () => {
34
- const idx = source.indexOf("#4243");
35
- assert.ok(idx >= 0, "source should reference issue #4243 for the abort-order fix");
36
- });
37
-
38
- test("switchSession() calls abort() before _disconnectFromAgent()", () => {
39
- // Find the switchSession method body
40
- const switchStart = source.indexOf("async switchSession(sessionPath:");
41
- assert.ok(switchStart >= 0, "should find switchSession method");
109
+ it("switchSession() invokes abort() before _disconnectFromAgent()", async () => {
110
+ const session = await createSession({ persistSessions: true });
111
+ // Seed a session file to switch to (switchSession reads from the session manager).
112
+ await session.newSession();
113
+ const sessionFile = session.sessionFile;
114
+ assert.ok(typeof sessionFile === "string" && sessionFile.length > 0, "need a session file to switch to");
42
115
 
43
- // Get a window that includes the abort/disconnect section
44
- const window = source.slice(switchStart, switchStart + 800);
116
+ const order = recordCallOrder(session as any, ["abort", "_disconnectFromAgent"]);
45
117
 
46
- // Find the abort and _disconnectFromAgent calls
47
- const abortIdx = window.indexOf("await this.abort();");
48
- const disconnectIdx = window.indexOf("this._disconnectFromAgent();");
118
+ const ok = await session.switchSession(sessionFile);
119
+ assert.equal(ok, true);
49
120
 
50
- assert.ok(abortIdx >= 0, "switchSession should call await this.abort()");
51
- assert.ok(disconnectIdx >= 0, "switchSession should call this._disconnectFromAgent()");
121
+ const abortIdx = order.indexOf("abort");
122
+ const disconnectIdx = order.indexOf("_disconnectFromAgent");
123
+ assert.ok(abortIdx >= 0, `switchSession should call abort(); order=${order.join(",")}`);
124
+ assert.ok(
125
+ disconnectIdx >= 0,
126
+ `switchSession should call _disconnectFromAgent(); order=${order.join(",")}`,
127
+ );
52
128
  assert.ok(
53
129
  abortIdx < disconnectIdx,
54
- "abort() must be called BEFORE _disconnectFromAgent() in switchSession so that events fire before unsubscribing",
130
+ `abort() must run before _disconnectFromAgent() in switchSession; order=${order.join(",")}`,
55
131
  );
56
132
  });
57
133
  });
@@ -1,21 +1,93 @@
1
- import test from "node:test";
1
+ // Regression test: explicit model switches must cancel any in-flight retry
2
+ // BEFORE applying the new model. Otherwise stale provider backoff errors
3
+ // from the previous model can continue to land after the switch.
4
+ //
5
+ // Verified behaviourally: construct a real AgentSession, wrap
6
+ // `_retryHandler.abortRetry` and `agent.setModel` to record call order,
7
+ // invoke setModel(), and assert the observed order.
8
+
2
9
  import assert from "node:assert/strict";
3
- import { readFileSync } from "node:fs";
10
+ import { mkdtempSync, rmSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
4
12
  import { join } from "node:path";
13
+ import { afterEach, beforeEach, describe, it } from "node:test";
14
+
15
+ import { Agent } from "@gsd/pi-agent-core";
16
+ import { getModel } from "@gsd/pi-ai";
17
+ import { AgentSession } from "./agent-session.js";
18
+ import { AuthStorage } from "./auth-storage.js";
19
+ import { ModelRegistry } from "./model-registry.js";
20
+ import { DefaultResourceLoader } from "./resource-loader.js";
21
+ import { SessionManager } from "./session-manager.js";
22
+ import { SettingsManager } from "./settings-manager.js";
23
+
24
+ let testDir: string;
25
+
26
+ async function createSession(): Promise<AgentSession> {
27
+ const agentDir = join(testDir, "agent-home");
28
+ const authStorage = AuthStorage.inMemory({});
29
+ // Seed a runtime anthropic API key so modelRegistry.isProviderRequestReady()
30
+ // returns true and setModel() doesn't throw on missing credentials.
31
+ authStorage.setRuntimeApiKey("anthropic", "sk-test-not-used");
32
+ const modelRegistry = new ModelRegistry(authStorage, join(agentDir, "models.json"));
33
+ const settingsManager = SettingsManager.inMemory();
34
+ const resourceLoader = new DefaultResourceLoader({
35
+ cwd: testDir,
36
+ agentDir,
37
+ settingsManager,
38
+ noExtensions: true,
39
+ noPromptTemplates: true,
40
+ noThemes: true,
41
+ });
42
+ await resourceLoader.reload();
43
+
44
+ return new AgentSession({
45
+ agent: new Agent(),
46
+ sessionManager: SessionManager.inMemory(testDir),
47
+ settingsManager,
48
+ cwd: testDir,
49
+ resourceLoader,
50
+ modelRegistry,
51
+ });
52
+ }
53
+
54
+ describe("AgentSession — explicit model switch cancels retry before applying new model", () => {
55
+ beforeEach(() => {
56
+ testDir = mkdtempSync(join(tmpdir(), "agent-session-model-switch-"));
57
+ });
58
+
59
+ afterEach(() => {
60
+ rmSync(testDir, { recursive: true, force: true });
61
+ });
62
+
63
+ it("setModel() calls _retryHandler.abortRetry() before agent.setModel()", async () => {
64
+ const session = await createSession();
65
+
66
+ const order: string[] = [];
67
+ const retryHandler = (session as any)._retryHandler;
68
+ const originalAbortRetry = retryHandler.abortRetry.bind(retryHandler);
69
+ retryHandler.abortRetry = () => {
70
+ order.push("abortRetry");
71
+ return originalAbortRetry();
72
+ };
73
+
74
+ const agent = (session as any).agent;
75
+ const originalSetModel = agent.setModel.bind(agent);
76
+ agent.setModel = (model: unknown) => {
77
+ order.push("setModel");
78
+ return originalSetModel(model);
79
+ };
80
+
81
+ const newModel = getModel("anthropic", "claude-3-5-sonnet-20241022");
82
+ await session.setModel(newModel, { persist: false });
5
83
 
6
- const source = readFileSync(join(process.cwd(), "packages/pi-coding-agent/src/core/agent-session.ts"), "utf-8");
7
-
8
- test("agent-session: explicit model switches cancel retry before applying new model", () => {
9
- const start = source.indexOf("private async _applyModelChange(");
10
- assert.ok(start >= 0, "missing _applyModelChange");
11
- const window = source.slice(start, start + 900);
12
- const abortIdx = window.indexOf("this._retryHandler.abortRetry();");
13
- const setModelIdx = window.indexOf("this.agent.setModel(model);");
14
-
15
- assert.ok(abortIdx >= 0, "_applyModelChange should cancel any in-flight retry");
16
- assert.ok(setModelIdx >= 0, "_applyModelChange should set the new model");
17
- assert.ok(
18
- abortIdx < setModelIdx,
19
- "retry cancellation must happen before applying the new model to prevent stale provider retries",
20
- );
84
+ const abortIdx = order.indexOf("abortRetry");
85
+ const setIdx = order.indexOf("setModel");
86
+ assert.ok(abortIdx >= 0, `setModel should cancel in-flight retry; order=${order.join(",")}`);
87
+ assert.ok(setIdx >= 0, `setModel should call agent.setModel; order=${order.join(",")}`);
88
+ assert.ok(
89
+ abortIdx < setIdx,
90
+ `retry cancellation must happen before applying the new model; order=${order.join(",")}`,
91
+ );
92
+ });
21
93
  });
@@ -1,64 +1,133 @@
1
- // GSD-2 — Regression tests for #3616: tool list persistence across newSession() calls
2
- // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
1
+ // Regression test for #3616: newSession() must restore the full tool set
2
+ // when cwd is unchanged.
3
+ //
4
+ // The bug: extensions may narrow the active tool list via setActiveTools()
5
+ // during a session. Without a refresh in the else branch of newSession(),
6
+ // the narrowed set persists into the next session — breaking auto-mode
7
+ // subagent sessions that expect a full tool palette.
8
+ //
9
+ // Verified behaviourally: construct an AgentSession, wrap _refreshToolRegistry
10
+ // to record its args, call newSession() with cwd unchanged, and assert that
11
+ // a refresh was requested with includeAllExtensionTools: true.
3
12
 
4
- import test, { describe } from "node:test";
5
13
  import assert from "node:assert/strict";
6
- import { readFileSync } from "node:fs";
14
+ import { mkdtempSync, rmSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
7
16
  import { join } from "node:path";
17
+ import { afterEach, beforeEach, describe, it } from "node:test";
8
18
 
9
- const source = readFileSync(
10
- join(process.cwd(), "packages/pi-coding-agent/src/core/agent-session.ts"),
11
- "utf-8",
12
- );
19
+ import { Agent } from "@gsd/pi-agent-core";
20
+ import { AgentSession } from "./agent-session.js";
21
+ import { AuthStorage } from "./auth-storage.js";
22
+ import { ModelRegistry } from "./model-registry.js";
23
+ import { DefaultResourceLoader } from "./resource-loader.js";
24
+ import { SessionManager } from "./session-manager.js";
25
+ import { SettingsManager } from "./settings-manager.js";
13
26
 
14
- describe("#3616 newSession() must restore full tool set", () => {
15
- test("newSession() calls _refreshToolRegistry with includeAllExtensionTools when cwd is unchanged", () => {
16
- // Find the newSession method
17
- const newSessionStart = source.indexOf("async newSession(options?:");
18
- assert.ok(newSessionStart >= 0, "should find newSession method");
27
+ let testDir: string;
19
28
 
20
- // Get the method body (up to the next top-level method)
21
- const methodBody = source.slice(newSessionStart, newSessionStart + 3000);
29
+ async function createSession(): Promise<AgentSession> {
30
+ const agentDir = join(testDir, "agent-home");
31
+ const authStorage = AuthStorage.inMemory({});
32
+ const modelRegistry = new ModelRegistry(authStorage, join(agentDir, "models.json"));
33
+ const settingsManager = SettingsManager.inMemory();
34
+ const resourceLoader = new DefaultResourceLoader({
35
+ cwd: testDir,
36
+ agentDir,
37
+ settingsManager,
38
+ noExtensions: true,
39
+ noPromptTemplates: true,
40
+ noThemes: true,
41
+ });
42
+ await resourceLoader.reload();
22
43
 
23
- // Verify the cwd-changed branch rebuilds tools
24
- assert.ok(
25
- methodBody.includes("if (this._cwd !== previousCwd)"),
26
- "should have cwd-change guard",
27
- );
44
+ return new AgentSession({
45
+ agent: new Agent(),
46
+ sessionManager: SessionManager.inMemory(testDir),
47
+ settingsManager,
48
+ cwd: testDir,
49
+ resourceLoader,
50
+ modelRegistry,
51
+ });
52
+ }
53
+
54
+ describe("#3616 — newSession() restores narrowed tool set when cwd unchanged", () => {
55
+ beforeEach(() => {
56
+ testDir = mkdtempSync(join(tmpdir(), "agent-session-tool-refresh-"));
57
+ });
58
+
59
+ afterEach(() => {
60
+ rmSync(testDir, { recursive: true, force: true });
61
+ });
28
62
 
29
- // Verify the else branch exists and refreshes tools with includeAllExtensionTools
30
- const elseIdx = methodBody.indexOf("} else {");
31
- assert.ok(elseIdx >= 0, "should have else branch for cwd-unchanged case");
63
+ it("calls _refreshToolRegistry with includeAllExtensionTools: true when cwd unchanged", async () => {
64
+ const session = await createSession();
65
+ // Pin _cwd so newSession()'s `process.cwd()` branch takes the
66
+ // cwd-unchanged path. The production code compares `this._cwd !==
67
+ // previousCwd`; we force equality by setting _cwd to current cwd.
68
+ (session as any)._cwd = process.cwd();
69
+
70
+ const refreshCalls: Array<{ includeAllExtensionTools?: boolean }> = [];
71
+ const originalRefresh = (session as any)._refreshToolRegistry.bind(session);
72
+ (session as any)._refreshToolRegistry = (options?: { includeAllExtensionTools?: boolean }) => {
73
+ refreshCalls.push(options ?? {});
74
+ return originalRefresh(options);
75
+ };
76
+
77
+ const ok = await session.newSession();
78
+ assert.equal(ok, true);
32
79
 
33
- const elseBranch = methodBody.slice(elseIdx, elseIdx + 800);
34
80
  assert.ok(
35
- elseBranch.includes("_refreshToolRegistry"),
36
- "else branch should call _refreshToolRegistry",
81
+ refreshCalls.length > 0,
82
+ "newSession() should invoke _refreshToolRegistry in the cwd-unchanged branch",
37
83
  );
38
84
  assert.ok(
39
- elseBranch.includes("includeAllExtensionTools: true"),
40
- "else branch should pass includeAllExtensionTools: true to restore narrowed tools",
85
+ refreshCalls.some((o) => o.includeAllExtensionTools === true),
86
+ `at least one _refreshToolRegistry call must pass includeAllExtensionTools: true; observed=${JSON.stringify(refreshCalls)}`,
41
87
  );
42
88
  });
43
89
 
44
- test("newSession() references #3616 in the else-branch comment", () => {
45
- const idx = source.indexOf("#3616");
46
- assert.ok(idx >= 0, "source should reference issue #3616 for the tool restore fix");
90
+ it("agent.reset() does not clear _state.tools (tools persist across reset)", () => {
91
+ // Structural invariant protecting #3616: if reset() starts clearing
92
+ // tools, newSession()'s refresh becomes the only defense against loss.
93
+ // Assertion is behavioural — seed tools, call reset(), observe survival.
94
+ const agent = new Agent();
95
+ const tool = {
96
+ name: "test_tool",
97
+ description: "x",
98
+ schema: { type: "object", properties: {}, additionalProperties: false } as any,
99
+ execute: async () => ({ content: [] }),
100
+ };
101
+ (agent as any)._state.tools = [tool];
102
+ agent.reset();
103
+ assert.deepEqual(
104
+ (agent as any)._state.tools,
105
+ [tool],
106
+ "Agent.reset() must preserve _state.tools",
107
+ );
47
108
  });
48
109
 
49
- test("agent.reset() does not clear _state.tools (tools persist across reset)", () => {
50
- // This is a structural invariant — if reset() starts clearing tools,
51
- // the newSession() refresh becomes the only defense against tool loss.
52
- const agentSource = readFileSync(
53
- join(process.cwd(), "packages/pi-agent-core/src/agent.ts"),
54
- "utf-8",
55
- );
56
- const resetStart = agentSource.indexOf("reset()");
57
- assert.ok(resetStart >= 0, "should find reset() method");
58
- const resetBody = agentSource.slice(resetStart, resetStart + 400);
110
+ it("takes the cwd-changed branch (rebuilds runtime) when cwd differs", async () => {
111
+ const session = await createSession();
112
+ // Force the cwd-changed branch: set _cwd to something that won't equal process.cwd().
113
+ (session as any)._cwd = join(testDir, "some", "other", "cwd");
114
+
115
+ let buildRuntimeCalled = false;
116
+ let buildRuntimeIncludedAll = false;
117
+ const originalBuild = (session as any)._buildRuntime.bind(session);
118
+ (session as any)._buildRuntime = (options?: { includeAllExtensionTools?: boolean }) => {
119
+ buildRuntimeCalled = true;
120
+ if (options?.includeAllExtensionTools === true) buildRuntimeIncludedAll = true;
121
+ return originalBuild(options);
122
+ };
123
+
124
+ const ok = await session.newSession();
125
+ assert.equal(ok, true);
126
+
127
+ assert.ok(buildRuntimeCalled, "cwd-changed branch must rebuild the tool runtime");
59
128
  assert.ok(
60
- !resetBody.includes("tools"),
61
- "reset() should NOT touch _state.tools tools are managed by agent-session",
129
+ buildRuntimeIncludedAll,
130
+ "cwd-changed branch must rebuild with includeAllExtensionTools: true",
62
131
  );
63
132
  });
64
133
  });