gsd-pi 2.77.0-dev.eaa4973bc → 2.78.0-dev.aeeb2ca00

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 (545) hide show
  1. package/README.md +53 -17
  2. package/dist/claude-cli-check.js +46 -10
  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 +72 -16
  13. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +481 -17
  14. package/dist/resources/extensions/github-sync/templates.js +103 -0
  15. package/dist/resources/extensions/google-search/index.js +3 -2
  16. package/dist/resources/extensions/gsd/auto/loop.js +124 -2
  17. package/dist/resources/extensions/gsd/auto/phases.js +57 -39
  18. package/dist/resources/extensions/gsd/auto/session.js +6 -2
  19. package/dist/resources/extensions/gsd/auto-dispatch.js +142 -29
  20. package/dist/resources/extensions/gsd/auto-model-selection.js +124 -4
  21. package/dist/resources/extensions/gsd/auto-post-unit.js +150 -64
  22. package/dist/resources/extensions/gsd/auto-prompts.js +372 -104
  23. package/dist/resources/extensions/gsd/auto-recovery.js +197 -48
  24. package/dist/resources/extensions/gsd/auto-start.js +107 -29
  25. package/dist/resources/extensions/gsd/auto-tool-tracking.js +47 -7
  26. package/dist/resources/extensions/gsd/auto-worktree.js +122 -26
  27. package/dist/resources/extensions/gsd/auto.js +76 -21
  28. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +19 -1
  29. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +209 -0
  30. package/dist/resources/extensions/gsd/bootstrap/provider-error-resume.js +3 -6
  31. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -3
  32. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +127 -9
  33. package/dist/resources/extensions/gsd/component-loader.js +447 -0
  34. package/dist/resources/extensions/gsd/component-types.js +69 -0
  35. package/dist/resources/extensions/gsd/context-store.js +23 -7
  36. package/dist/resources/extensions/gsd/detection.js +49 -1
  37. package/dist/resources/extensions/gsd/dispatch-guard.js +2 -17
  38. package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  39. package/dist/resources/extensions/gsd/forensics.js +106 -0
  40. package/dist/resources/extensions/gsd/gate-registry.js +2 -2
  41. package/dist/resources/extensions/gsd/git-constants.js +28 -1
  42. package/dist/resources/extensions/gsd/git-self-heal.js +27 -0
  43. package/dist/resources/extensions/gsd/git-service.js +126 -2
  44. package/dist/resources/extensions/gsd/gsd-db.js +6 -3
  45. package/dist/resources/extensions/gsd/guided-flow.js +39 -13
  46. package/dist/resources/extensions/gsd/memory-extractor.js +7 -1
  47. package/dist/resources/extensions/gsd/milestone-scope-classifier.js +299 -0
  48. package/dist/resources/extensions/gsd/milestone-summary-classifier.js +37 -0
  49. package/dist/resources/extensions/gsd/model-cost-table.js +3 -0
  50. package/dist/resources/extensions/gsd/model-router.js +6 -0
  51. package/dist/resources/extensions/gsd/native-git-bridge.js +34 -4
  52. package/dist/resources/extensions/gsd/preferences-validation.js +23 -0
  53. package/dist/resources/extensions/gsd/prompt-cache-optimizer.js +4 -0
  54. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +6 -2
  55. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +23 -4
  56. package/dist/resources/extensions/gsd/prompts/doctor-heal.md +5 -4
  57. package/dist/resources/extensions/gsd/prompts/plan-slice.md +15 -2
  58. package/dist/resources/extensions/gsd/safety/git-checkpoint.js +11 -0
  59. package/dist/resources/extensions/gsd/service-tier.js +5 -2
  60. package/dist/resources/extensions/gsd/session-lock.js +19 -10
  61. package/dist/resources/extensions/gsd/skill-manifest.js +168 -0
  62. package/dist/resources/extensions/gsd/slice-cadence.js +238 -0
  63. package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +278 -8
  64. package/dist/resources/extensions/gsd/state-transition-matrix.js +118 -0
  65. package/dist/resources/extensions/gsd/state.js +69 -58
  66. package/dist/resources/extensions/gsd/sync-lock.js +98 -42
  67. package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -2
  68. package/dist/resources/extensions/gsd/unit-context-composer.js +147 -0
  69. package/dist/resources/extensions/gsd/unit-context-manifest.js +370 -0
  70. package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +33 -0
  71. package/dist/resources/extensions/gsd/uok/execution-graph.js +10 -0
  72. package/dist/resources/extensions/gsd/uok/gate-runner.js +53 -5
  73. package/dist/resources/extensions/gsd/uok/gitops.js +2 -1
  74. package/dist/resources/extensions/gsd/uok/loop-adapter.js +37 -10
  75. package/dist/resources/extensions/gsd/uok/parity-report.js +58 -0
  76. package/dist/resources/extensions/gsd/uok/plan-v2.js +10 -4
  77. package/dist/resources/extensions/gsd/uok/writer.js +82 -0
  78. package/dist/resources/extensions/gsd/workflow-mcp.js +6 -0
  79. package/dist/resources/extensions/gsd/worktree-manager.js +85 -8
  80. package/dist/resources/extensions/gsd/worktree-resolver.js +86 -7
  81. package/dist/resources/extensions/gsd/worktree-telemetry.js +198 -0
  82. package/dist/resources/extensions/mcp-client/index.js +3 -1
  83. package/dist/resources/extensions/ollama/index.js +5 -1
  84. package/dist/resources/extensions/remote-questions/manager.js +11 -5
  85. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  86. package/dist/web/standalone/.next/BUILD_ID +1 -1
  87. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  88. package/dist/web/standalone/.next/build-manifest.json +2 -2
  89. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  90. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  91. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  99. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  100. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  101. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  102. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  103. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  104. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  105. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  106. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/index.html +1 -1
  108. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  109. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  110. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  111. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  112. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  113. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  114. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  115. package/dist/web/standalone/.next/server/chunks/1926.js +1 -1
  116. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  117. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  119. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  120. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  121. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  122. package/package.json +2 -3
  123. package/packages/daemon/package.json +2 -2
  124. package/packages/daemon/src/logger.ts +4 -3
  125. package/packages/mcp-server/dist/server.d.ts +24 -0
  126. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  127. package/packages/mcp-server/dist/server.js +88 -87
  128. package/packages/mcp-server/dist/server.js.map +1 -1
  129. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  130. package/packages/mcp-server/dist/workflow-tools.js +15 -6
  131. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  132. package/packages/mcp-server/package.json +2 -2
  133. package/packages/mcp-server/src/mcp-server.test.ts +25 -3
  134. package/packages/mcp-server/src/readers/graph.test.ts +87 -15
  135. package/packages/mcp-server/src/secure-env-collect.test.ts +232 -237
  136. package/packages/mcp-server/src/server.ts +131 -105
  137. package/packages/mcp-server/src/workflow-tools.test.ts +85 -0
  138. package/packages/mcp-server/src/workflow-tools.ts +19 -6
  139. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  140. package/packages/native/package.json +2 -2
  141. package/packages/native/src/__tests__/_test-coverage-guard.test.mjs +98 -0
  142. package/packages/native/src/__tests__/module-compat.test.mjs +59 -27
  143. package/packages/native/src/__tests__/ps.test.mjs +14 -8
  144. package/packages/native/src/__tests__/stream-process.test.mjs +23 -2
  145. package/packages/native/src/__tests__/truncate.test.mjs +17 -2
  146. package/packages/pi-agent-core/package.json +1 -1
  147. package/packages/pi-agent-core/src/agent-loop.test.ts +5 -15
  148. package/packages/pi-agent-core/src/agent.test.ts +96 -102
  149. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  150. package/packages/pi-ai/dist/models/capability-patches.d.ts.map +1 -1
  151. package/packages/pi-ai/dist/models/capability-patches.js +9 -2
  152. package/packages/pi-ai/dist/models/capability-patches.js.map +1 -1
  153. package/packages/pi-ai/dist/models/generated/index.d.ts +34 -0
  154. package/packages/pi-ai/dist/models/generated/index.d.ts.map +1 -1
  155. package/packages/pi-ai/dist/models/generated/openai-codex.d.ts +17 -0
  156. package/packages/pi-ai/dist/models/generated/openai-codex.d.ts.map +1 -1
  157. package/packages/pi-ai/dist/models/generated/openai-codex.js +17 -0
  158. package/packages/pi-ai/dist/models/generated/openai-codex.js.map +1 -1
  159. package/packages/pi-ai/dist/models/generated/openai.d.ts +17 -0
  160. package/packages/pi-ai/dist/models/generated/openai.d.ts.map +1 -1
  161. package/packages/pi-ai/dist/models/generated/openai.js +17 -0
  162. package/packages/pi-ai/dist/models/generated/openai.js.map +1 -1
  163. package/packages/pi-ai/dist/models.generated.test.js +43 -70
  164. package/packages/pi-ai/dist/models.generated.test.js.map +1 -1
  165. package/packages/pi-ai/dist/models.test.js +36 -11
  166. package/packages/pi-ai/dist/models.test.js.map +1 -1
  167. package/packages/pi-ai/package.json +1 -1
  168. package/packages/pi-ai/scripts/generate-models.ts +44 -0
  169. package/packages/pi-ai/src/models/capability-patches.ts +10 -2
  170. package/packages/pi-ai/src/models/generated/openai-codex.ts +17 -0
  171. package/packages/pi-ai/src/models/generated/openai.ts +17 -0
  172. package/packages/pi-ai/src/models.generated.test.ts +46 -73
  173. package/packages/pi-ai/src/models.test.ts +48 -11
  174. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  175. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +96 -32
  176. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  177. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +75 -12
  178. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -1
  179. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +99 -31
  180. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
  181. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +5 -0
  182. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  183. package/packages/pi-coding-agent/dist/core/extensions/loader.js +61 -0
  184. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  185. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +30 -4
  186. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -1
  187. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +17 -0
  188. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -1
  189. package/packages/pi-coding-agent/dist/core/resource-loader-cache-reset.test.js +76 -18
  190. package/packages/pi-coding-agent/dist/core/resource-loader-cache-reset.test.js.map +1 -1
  191. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  192. package/packages/pi-coding-agent/dist/core/retry-handler.js +2 -6
  193. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  194. package/packages/pi-coding-agent/dist/core/retry-handler.test.js +5 -1
  195. package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
  196. package/packages/pi-coding-agent/dist/core/retryable-error-regex.d.ts +18 -0
  197. package/packages/pi-coding-agent/dist/core/retryable-error-regex.d.ts.map +1 -0
  198. package/packages/pi-coding-agent/dist/core/retryable-error-regex.js +18 -0
  199. package/packages/pi-coding-agent/dist/core/retryable-error-regex.js.map +1 -0
  200. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +20 -0
  201. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  202. package/packages/pi-coding-agent/dist/core/system-prompt.js +16 -2
  203. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  204. package/packages/pi-coding-agent/dist/index.d.ts +1 -0
  205. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  206. package/packages/pi-coding-agent/dist/index.js +1 -0
  207. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  208. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +36 -5
  209. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  210. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +20 -13
  211. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -1
  212. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  213. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +30 -12
  214. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  215. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  216. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +18 -3
  217. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  218. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.js +125 -0
  219. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.js.map +1 -1
  220. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +2 -0
  221. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  222. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  223. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
  224. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  225. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +105 -13
  226. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  227. package/packages/pi-coding-agent/dist/tests/system-prompt-skill-filter.test.d.ts +2 -0
  228. package/packages/pi-coding-agent/dist/tests/system-prompt-skill-filter.test.d.ts.map +1 -0
  229. package/packages/pi-coding-agent/dist/tests/system-prompt-skill-filter.test.js +130 -0
  230. package/packages/pi-coding-agent/dist/tests/system-prompt-skill-filter.test.js.map +1 -0
  231. package/packages/pi-coding-agent/package.json +1 -1
  232. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +113 -37
  233. package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +89 -17
  234. package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +112 -43
  235. package/packages/pi-coding-agent/src/core/extensions/loader.ts +58 -0
  236. package/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +35 -4
  237. package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +20 -0
  238. package/packages/pi-coding-agent/src/core/resource-loader-cache-reset.test.ts +93 -28
  239. package/packages/pi-coding-agent/src/core/retry-handler.test.ts +5 -1
  240. package/packages/pi-coding-agent/src/core/retry-handler.ts +2 -8
  241. package/packages/pi-coding-agent/src/core/retryable-error-regex.ts +18 -0
  242. package/packages/pi-coding-agent/src/core/system-prompt.ts +35 -1
  243. package/packages/pi-coding-agent/src/index.ts +1 -0
  244. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +49 -3
  245. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +26 -20
  246. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +48 -9
  247. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.test.ts +146 -1
  248. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +20 -3
  249. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +2 -0
  250. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +119 -13
  251. package/packages/pi-coding-agent/src/tests/system-prompt-skill-filter.test.ts +157 -0
  252. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  253. package/packages/pi-tui/dist/__tests__/autocomplete.test.js +18 -8
  254. package/packages/pi-tui/dist/__tests__/autocomplete.test.js.map +1 -1
  255. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js +128 -17
  256. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js.map +1 -1
  257. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js +37 -11
  258. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js.map +1 -1
  259. package/packages/pi-tui/dist/__tests__/tui.test.js +18 -30
  260. package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
  261. package/packages/pi-tui/dist/components/__tests__/input.test.js +10 -3
  262. package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
  263. package/packages/pi-tui/dist/components/__tests__/loader.test.js +53 -9
  264. package/packages/pi-tui/dist/components/__tests__/loader.test.js.map +1 -1
  265. package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js +6 -2
  266. package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js.map +1 -1
  267. package/packages/pi-tui/dist/components/editor.d.ts +14 -0
  268. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  269. package/packages/pi-tui/dist/components/editor.js +19 -0
  270. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  271. package/packages/pi-tui/dist/components/image.test.js +6 -5
  272. package/packages/pi-tui/dist/components/image.test.js.map +1 -1
  273. package/packages/pi-tui/dist/editor-component.d.ts +2 -0
  274. package/packages/pi-tui/dist/editor-component.d.ts.map +1 -1
  275. package/packages/pi-tui/dist/editor-component.js.map +1 -1
  276. package/packages/pi-tui/package.json +1 -1
  277. package/packages/pi-tui/src/__tests__/autocomplete.test.ts +24 -8
  278. package/packages/pi-tui/src/__tests__/overlay-layout.test.ts +140 -17
  279. package/packages/pi-tui/src/__tests__/stdin-buffer.test.ts +42 -11
  280. package/packages/pi-tui/src/__tests__/tui.test.ts +18 -37
  281. package/packages/pi-tui/src/components/__tests__/input.test.ts +19 -3
  282. package/packages/pi-tui/src/components/__tests__/loader.test.ts +112 -35
  283. package/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts +9 -2
  284. package/packages/pi-tui/src/components/editor.ts +22 -0
  285. package/packages/pi-tui/src/components/image.test.ts +10 -5
  286. package/packages/pi-tui/src/editor-component.ts +3 -0
  287. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  288. package/packages/rpc-client/dist/rpc-client.test.js +101 -51
  289. package/packages/rpc-client/dist/rpc-client.test.js.map +1 -1
  290. package/packages/rpc-client/package.json +1 -1
  291. package/packages/rpc-client/src/rpc-client.test.ts +109 -52
  292. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
  293. package/pkg/package.json +1 -1
  294. package/scripts/install.js +15 -1
  295. package/src/resources/extensions/browser-tools/capture.ts +12 -0
  296. package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +8 -59
  297. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +36 -24
  298. package/src/resources/extensions/browser-tools/tests/capture-sharp-optional.test.cjs +69 -71
  299. package/src/resources/extensions/browser-tools/tools/forms.ts +5 -1
  300. package/src/resources/extensions/browser-tools/tools/intent.ts +5 -1
  301. package/src/resources/extensions/claude-code-cli/readiness.ts +75 -16
  302. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +518 -19
  303. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +919 -75
  304. package/src/resources/extensions/github-sync/templates.ts +151 -0
  305. package/src/resources/extensions/github-sync/tests/cli.test.ts +76 -7
  306. package/src/resources/extensions/github-sync/tests/templates.test.ts +92 -1
  307. package/src/resources/extensions/google-search/index.ts +3 -2
  308. package/src/resources/extensions/gsd/auto/loop.ts +142 -2
  309. package/src/resources/extensions/gsd/auto/phases.ts +62 -38
  310. package/src/resources/extensions/gsd/auto/session.ts +7 -2
  311. package/src/resources/extensions/gsd/auto-dispatch.ts +156 -29
  312. package/src/resources/extensions/gsd/auto-model-selection.ts +131 -4
  313. package/src/resources/extensions/gsd/auto-post-unit.ts +163 -73
  314. package/src/resources/extensions/gsd/auto-prompts.ts +385 -93
  315. package/src/resources/extensions/gsd/auto-recovery.ts +230 -51
  316. package/src/resources/extensions/gsd/auto-start.ts +127 -9
  317. package/src/resources/extensions/gsd/auto-tool-tracking.ts +51 -7
  318. package/src/resources/extensions/gsd/auto-worktree.ts +130 -26
  319. package/src/resources/extensions/gsd/auto.ts +90 -23
  320. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +20 -1
  321. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +221 -0
  322. package/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +3 -7
  323. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +7 -3
  324. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +158 -9
  325. package/src/resources/extensions/gsd/component-loader.ts +598 -0
  326. package/src/resources/extensions/gsd/component-types.ts +362 -0
  327. package/src/resources/extensions/gsd/context-store.ts +25 -8
  328. package/src/resources/extensions/gsd/detection.ts +58 -1
  329. package/src/resources/extensions/gsd/dispatch-guard.ts +2 -20
  330. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  331. package/src/resources/extensions/gsd/forensics.ts +118 -1
  332. package/src/resources/extensions/gsd/gate-registry.ts +2 -2
  333. package/src/resources/extensions/gsd/git-constants.ts +30 -1
  334. package/src/resources/extensions/gsd/git-self-heal.ts +31 -0
  335. package/src/resources/extensions/gsd/git-service.ts +149 -2
  336. package/src/resources/extensions/gsd/gsd-db.ts +6 -3
  337. package/src/resources/extensions/gsd/guided-flow.ts +57 -14
  338. package/src/resources/extensions/gsd/journal.ts +11 -1
  339. package/src/resources/extensions/gsd/memory-extractor.ts +11 -3
  340. package/src/resources/extensions/gsd/milestone-scope-classifier.ts +366 -0
  341. package/src/resources/extensions/gsd/milestone-summary-classifier.ts +42 -0
  342. package/src/resources/extensions/gsd/model-cost-table.ts +3 -0
  343. package/src/resources/extensions/gsd/model-router.ts +6 -0
  344. package/src/resources/extensions/gsd/native-git-bridge.ts +34 -4
  345. package/src/resources/extensions/gsd/preferences-validation.ts +21 -0
  346. package/src/resources/extensions/gsd/prompt-cache-optimizer.ts +4 -0
  347. package/src/resources/extensions/gsd/prompts/complete-milestone.md +6 -2
  348. package/src/resources/extensions/gsd/prompts/discuss-headless.md +23 -4
  349. package/src/resources/extensions/gsd/prompts/doctor-heal.md +5 -4
  350. package/src/resources/extensions/gsd/prompts/plan-slice.md +15 -2
  351. package/src/resources/extensions/gsd/safety/git-checkpoint.ts +15 -0
  352. package/src/resources/extensions/gsd/service-tier.ts +5 -2
  353. package/src/resources/extensions/gsd/session-lock.ts +20 -10
  354. package/src/resources/extensions/gsd/skill-manifest.ts +175 -0
  355. package/src/resources/extensions/gsd/slice-cadence.ts +299 -0
  356. package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +309 -8
  357. package/src/resources/extensions/gsd/state-transition-matrix.ts +152 -0
  358. package/src/resources/extensions/gsd/state.ts +76 -66
  359. package/src/resources/extensions/gsd/sync-lock.ts +97 -39
  360. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +270 -0
  361. package/src/resources/extensions/gsd/tests/artifacts-table-preserved-on-cache-invalidate.test.ts +2 -1
  362. package/src/resources/extensions/gsd/tests/auto-deterministic-error-classification-4973.test.ts +341 -0
  363. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +264 -0
  364. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +133 -292
  365. package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +742 -0
  366. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +78 -0
  367. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +61 -0
  368. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +93 -0
  369. package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +4 -1
  370. package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +8 -194
  371. package/src/resources/extensions/gsd/tests/auto-start-clean-runtime-db-gated.test.ts +3 -2
  372. package/src/resources/extensions/gsd/tests/auto-start-cold-db-bootstrap.test.ts +2 -2
  373. package/src/resources/extensions/gsd/tests/auto-start-needs-discussion.test.ts +15 -58
  374. package/src/resources/extensions/gsd/tests/auto-start-worktree-db-path.test.ts +2 -2
  375. package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +3 -2
  376. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +3 -2
  377. package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -1
  378. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +17 -21
  379. package/src/resources/extensions/gsd/tests/canonical-milestone-root.test.ts +108 -0
  380. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +263 -0
  381. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +25 -0
  382. package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +192 -0
  383. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +2 -1
  384. package/src/resources/extensions/gsd/tests/complete-task.test.ts +16 -8
  385. package/src/resources/extensions/gsd/tests/component-loader.test.ts +589 -0
  386. package/src/resources/extensions/gsd/tests/component-types.test.ts +127 -0
  387. package/src/resources/extensions/gsd/tests/context-store.test.ts +79 -0
  388. package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +2 -1
  389. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +50 -1
  390. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +159 -0
  391. package/src/resources/extensions/gsd/tests/db-access-guardrails.test.ts +1 -0
  392. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -3
  393. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +40 -0
  394. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +91 -3
  395. package/src/resources/extensions/gsd/tests/derive-state.test.ts +4 -4
  396. package/src/resources/extensions/gsd/tests/discuss-slice-structured-questions.test.ts +2 -1
  397. package/src/resources/extensions/gsd/tests/discuss-tool-scope-leak.test.ts +2 -1
  398. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +5 -0
  399. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +25 -0
  400. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +14 -0
  401. package/src/resources/extensions/gsd/tests/dispatcher-stuck-planning.test.ts +3 -2
  402. package/src/resources/extensions/gsd/tests/double-merge-guard.test.ts +4 -3
  403. package/src/resources/extensions/gsd/tests/empty-content-abort-loop.test.ts +4 -3
  404. package/src/resources/extensions/gsd/tests/execution-entry-missing-context-4671.test.ts +173 -0
  405. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +139 -129
  406. package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +8 -104
  407. package/src/resources/extensions/gsd/tests/gate-state-canonicalization.test.ts +102 -0
  408. package/src/resources/extensions/gsd/tests/gate-storage.test.ts +1 -1
  409. package/src/resources/extensions/gsd/tests/google-search-stub.test.ts +14 -4
  410. package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +117 -0
  411. package/src/resources/extensions/gsd/tests/hook-key-parsing.test.ts +4 -55
  412. package/src/resources/extensions/gsd/tests/integration/all-milestones-complete-merge.test.ts +7 -56
  413. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +20 -0
  414. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +18 -2
  415. package/src/resources/extensions/gsd/tests/integration/queue-completed-milestone-perf.test.ts +10 -4
  416. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +144 -7
  417. package/src/resources/extensions/gsd/tests/integration/state-machine-live-validation.test.ts +4 -0
  418. package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +2 -16
  419. package/src/resources/extensions/gsd/tests/interactive-routing-bypass.test.ts +9 -3
  420. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +6 -9
  421. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +64 -0
  422. package/src/resources/extensions/gsd/tests/knowledge.test.ts +93 -1
  423. package/src/resources/extensions/gsd/tests/mcp-client-security.test.ts +8 -37
  424. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +5 -15
  425. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +227 -55
  426. package/src/resources/extensions/gsd/tests/milestone-scope-classifier.test.ts +187 -0
  427. package/src/resources/extensions/gsd/tests/milestone-summary-classifier.test.ts +30 -0
  428. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +9 -1
  429. package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
  430. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +6 -48
  431. package/src/resources/extensions/gsd/tests/notification-widget.test.ts +6 -3
  432. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +59 -2
  433. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +273 -130
  434. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +301 -0
  435. package/src/resources/extensions/gsd/tests/pre-execution-pause-wiring.test.ts +32 -1
  436. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +2 -1
  437. package/src/resources/extensions/gsd/tests/prompt-cache-optimizer.test.ts +12 -0
  438. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +15 -4
  439. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +23 -24
  440. package/src/resources/extensions/gsd/tests/queue-auto-guard.test.ts +32 -0
  441. package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +3 -2
  442. package/src/resources/extensions/gsd/tests/queued-discuss-fast-path.test.ts +4 -5
  443. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +75 -2
  444. package/src/resources/extensions/gsd/tests/reassess-default-optin.test.ts +132 -0
  445. package/src/resources/extensions/gsd/tests/recovery-attempts-reset.test.ts +8 -40
  446. package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +136 -256
  447. package/src/resources/extensions/gsd/tests/research-milestone-composer.test.ts +114 -0
  448. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +6 -3
  449. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +148 -0
  450. package/src/resources/extensions/gsd/tests/service-tier.test.ts +4 -0
  451. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +29 -0
  452. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +3 -2
  453. package/src/resources/extensions/gsd/tests/silent-catch-diagnostics.test.ts +55 -95
  454. package/src/resources/extensions/gsd/tests/single-writer-v3-tool-surface.test.ts +158 -0
  455. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +120 -1
  456. package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +112 -0
  457. package/src/resources/extensions/gsd/tests/slice-cadence.test.ts +242 -0
  458. package/src/resources/extensions/gsd/tests/slice-context-injection.test.ts +3 -2
  459. package/src/resources/extensions/gsd/tests/slice-parallel-orchestrator.test.ts +164 -1
  460. package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +2 -1
  461. package/src/resources/extensions/gsd/tests/stale-dirlistcache-4648.test.ts +112 -0
  462. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +29 -5
  463. package/src/resources/extensions/gsd/tests/state-transition-matrix.test.ts +44 -0
  464. package/src/resources/extensions/gsd/tests/stop-auto-race-null-unit.test.ts +3 -3
  465. package/src/resources/extensions/gsd/tests/structured-data-formatter.test.ts +11 -92
  466. package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +7 -6
  467. package/src/resources/extensions/gsd/tests/survivor-branch-complete.test.ts +102 -101
  468. package/src/resources/extensions/gsd/tests/sync-lock.test.ts +31 -0
  469. package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +4 -3
  470. package/src/resources/extensions/gsd/tests/test-helpers.test.ts +98 -0
  471. package/src/resources/extensions/gsd/tests/test-helpers.ts +153 -0
  472. package/src/resources/extensions/gsd/tests/token-profile.test.ts +8 -1
  473. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +61 -1
  474. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +8 -1
  475. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +355 -0
  476. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +258 -0
  477. package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +51 -0
  478. package/src/resources/extensions/gsd/tests/uok-execution-graph.test.ts +16 -0
  479. package/src/resources/extensions/gsd/tests/uok-gate-runner.test.ts +75 -0
  480. package/src/resources/extensions/gsd/tests/uok-gitops-wiring.test.ts +49 -26
  481. package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +65 -0
  482. package/src/resources/extensions/gsd/tests/uok-parity-report.test.ts +42 -0
  483. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +19 -2
  484. package/src/resources/extensions/gsd/tests/uok-writer.test.ts +75 -0
  485. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +12 -0
  486. package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +144 -80
  487. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +20 -54
  488. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +342 -277
  489. package/src/resources/extensions/gsd/tests/worker-model-override.test.ts +37 -29
  490. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +226 -266
  491. package/src/resources/extensions/gsd/tests/worktree-health-monorepo.test.ts +103 -67
  492. package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +92 -90
  493. package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +238 -59
  494. package/src/resources/extensions/gsd/tests/worktree-sync-overwrite-loop.test.ts +113 -161
  495. package/src/resources/extensions/gsd/tests/worktree-telemetry.test.ts +210 -0
  496. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +262 -0
  497. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +186 -0
  498. package/src/resources/extensions/gsd/tests/write-gate.test.ts +7 -5
  499. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +80 -96
  500. package/src/resources/extensions/gsd/tools/validate-milestone.ts +8 -2
  501. package/src/resources/extensions/gsd/types.ts +3 -3
  502. package/src/resources/extensions/gsd/unit-context-composer.ts +218 -0
  503. package/src/resources/extensions/gsd/unit-context-manifest.ts +574 -0
  504. package/src/resources/extensions/gsd/uok/contracts.ts +65 -0
  505. package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +56 -0
  506. package/src/resources/extensions/gsd/uok/execution-graph.ts +22 -0
  507. package/src/resources/extensions/gsd/uok/gate-runner.ts +65 -5
  508. package/src/resources/extensions/gsd/uok/gitops.ts +6 -1
  509. package/src/resources/extensions/gsd/uok/loop-adapter.ts +45 -10
  510. package/src/resources/extensions/gsd/uok/parity-report.ts +84 -0
  511. package/src/resources/extensions/gsd/uok/plan-v2.ts +13 -5
  512. package/src/resources/extensions/gsd/uok/writer.ts +113 -0
  513. package/src/resources/extensions/gsd/workflow-mcp.ts +6 -0
  514. package/src/resources/extensions/gsd/worktree-manager.ts +108 -7
  515. package/src/resources/extensions/gsd/worktree-resolver.ts +96 -9
  516. package/src/resources/extensions/gsd/worktree-telemetry.ts +322 -0
  517. package/src/resources/extensions/mcp-client/index.ts +3 -1
  518. package/src/resources/extensions/mcp-client/tests/server-name-spaces.test.ts +70 -36
  519. package/src/resources/extensions/ollama/index.ts +5 -1
  520. package/src/resources/extensions/ollama/ollama-auth-mode.test.ts +123 -15
  521. package/src/resources/extensions/ollama/ollama-status-indicator.test.ts +206 -19
  522. package/src/resources/extensions/remote-questions/manager.ts +36 -4
  523. package/src/resources/extensions/remote-questions/tests/command-polling.test.ts +200 -190
  524. package/src/resources/extensions/shared/tests/interview-preview.test.ts +11 -3
  525. package/src/resources/extensions/voice/tests/linux-ready.test.ts +129 -113
  526. package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.d.ts +0 -2
  527. package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.d.ts.map +0 -1
  528. package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.js +0 -289
  529. package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.js.map +0 -1
  530. package/packages/pi-ai/src/utils/oauth/oauth-providers.test.ts +0 -363
  531. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +0 -143
  532. package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +0 -157
  533. package/src/resources/extensions/gsd/tests/dashboard-model-label-ordering.test.ts +0 -107
  534. package/src/resources/extensions/gsd/tests/find-missing-summaries-closed.test.ts +0 -48
  535. package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +0 -159
  536. package/src/resources/extensions/gsd/tests/forensics-db-completion.test.ts +0 -96
  537. package/src/resources/extensions/gsd/tests/forensics-dedup.test.ts +0 -79
  538. package/src/resources/extensions/gsd/tests/forensics-hook-key-parse.test.ts +0 -74
  539. package/src/resources/extensions/gsd/tests/forensics-journal.test.ts +0 -162
  540. package/src/resources/extensions/gsd/tests/gitignore-bg-shell.test.ts +0 -38
  541. package/src/resources/extensions/gsd/tests/gsd-no-project-error.test.ts +0 -73
  542. package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +0 -125
  543. package/src/resources/extensions/gsd/tests/import-done-milestones.test.ts +0 -42
  544. /package/dist/web/standalone/.next/static/{5wbu35_C2_MQ3Jj1lEVDx → cAJH99yNS1UPbeSEiNRrV}/_buildManifest.js +0 -0
  545. /package/dist/web/standalone/.next/static/{5wbu35_C2_MQ3Jj1lEVDx → cAJH99yNS1UPbeSEiNRrV}/_ssgManifest.js +0 -0
@@ -13,6 +13,10 @@ import {
13
13
  buildPromptFromContext,
14
14
  buildSdkQueryPrompt,
15
15
  buildSdkOptions,
16
+ createClaudeCodeCanUseToolHandler,
17
+ buildBashPermissionPattern,
18
+ buildBashPermissionPatternOptions,
19
+ bashCommandMatchesSavedRules,
16
20
  createClaudeCodeElicitationHandler,
17
21
  extractImageBlocksFromContext,
18
22
  extractToolResultsFromSdkUserMessage,
@@ -20,11 +24,58 @@ import {
20
24
  parseAskUserQuestionsElicitation,
21
25
  parseTextInputElicitation,
22
26
  parseClaudeLookupOutput,
27
+ resolveBundledClaudeCliPath,
28
+ normalizeClaudePathForSdk,
23
29
  roundResultToElicitationContent,
24
30
  } from "../stream-adapter.ts";
25
31
  import type { AssistantMessage, Context, Message } from "@gsd/pi-ai";
26
32
  import type { SDKUserMessage } from "../sdk-types.ts";
27
33
 
34
+ // ---------------------------------------------------------------------------
35
+ // Env helpers — `GSD_WORKFLOW_MCP_*` save/restore
36
+ //
37
+ // The naive pattern `process.env.X = prev.X` breaks when `prev.X` is
38
+ // undefined: Node coerces the assignment to the literal string
39
+ // "undefined", which then pollutes subsequent tests that read the var
40
+ // and assume it's absent. Issue #4808 documents the resulting bleed.
41
+ //
42
+ // `setWorkflowMcpEnv` returns a `restore()` closure that either
43
+ // re-assigns the previous string value OR `delete`s the key when the
44
+ // original was absent. Call in a try/finally; restore in the finally.
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const WORKFLOW_MCP_ENV_KEYS = [
48
+ "GSD_WORKFLOW_MCP_COMMAND",
49
+ "GSD_WORKFLOW_MCP_NAME",
50
+ "GSD_WORKFLOW_MCP_ARGS",
51
+ "GSD_WORKFLOW_MCP_ENV",
52
+ "GSD_WORKFLOW_MCP_CWD",
53
+ ] as const;
54
+
55
+ type WorkflowMcpEnvKey = (typeof WORKFLOW_MCP_ENV_KEYS)[number];
56
+
57
+ function setWorkflowMcpEnv(
58
+ values: Partial<Record<WorkflowMcpEnvKey, string>>,
59
+ ): () => void {
60
+ const prev: Partial<Record<WorkflowMcpEnvKey, string | undefined>> = {};
61
+ for (const key of WORKFLOW_MCP_ENV_KEYS) {
62
+ prev[key] = process.env[key];
63
+ }
64
+ for (const [key, value] of Object.entries(values)) {
65
+ process.env[key] = value;
66
+ }
67
+ return function restore() {
68
+ for (const key of WORKFLOW_MCP_ENV_KEYS) {
69
+ const previous = prev[key];
70
+ if (previous === undefined) {
71
+ delete process.env[key];
72
+ } else {
73
+ process.env[key] = previous;
74
+ }
75
+ }
76
+ };
77
+ }
78
+
28
79
  // ---------------------------------------------------------------------------
29
80
  // Existing tests — exhausted stream fallback (#2575)
30
81
  // ---------------------------------------------------------------------------
@@ -737,19 +788,14 @@ describe("stream-adapter — session persistence (#2859)", () => {
737
788
  });
738
789
 
739
790
  test("buildSdkOptions includes workflow MCP server config when env is set", () => {
740
- const prev = {
741
- GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
742
- GSD_WORKFLOW_MCP_NAME: process.env.GSD_WORKFLOW_MCP_NAME,
743
- GSD_WORKFLOW_MCP_ARGS: process.env.GSD_WORKFLOW_MCP_ARGS,
744
- GSD_WORKFLOW_MCP_ENV: process.env.GSD_WORKFLOW_MCP_ENV,
745
- GSD_WORKFLOW_MCP_CWD: process.env.GSD_WORKFLOW_MCP_CWD,
746
- };
791
+ const restore = setWorkflowMcpEnv({
792
+ GSD_WORKFLOW_MCP_COMMAND: "node",
793
+ GSD_WORKFLOW_MCP_NAME: "gsd-workflow",
794
+ GSD_WORKFLOW_MCP_ARGS: JSON.stringify(["packages/mcp-server/dist/cli.js"]),
795
+ GSD_WORKFLOW_MCP_ENV: JSON.stringify({ GSD_CLI_PATH: "/tmp/gsd" }),
796
+ GSD_WORKFLOW_MCP_CWD: "/tmp/project",
797
+ });
747
798
  try {
748
- process.env.GSD_WORKFLOW_MCP_COMMAND = "node";
749
- process.env.GSD_WORKFLOW_MCP_NAME = "gsd-workflow";
750
- process.env.GSD_WORKFLOW_MCP_ARGS = JSON.stringify(["packages/mcp-server/dist/cli.js"]);
751
- process.env.GSD_WORKFLOW_MCP_ENV = JSON.stringify({ GSD_CLI_PATH: "/tmp/gsd" });
752
- process.env.GSD_WORKFLOW_MCP_CWD = "/tmp/project";
753
799
 
754
800
  const options = buildSdkOptions("claude-sonnet-4-20250514", "test");
755
801
  const mcpServers = options.mcpServers as Record<string, any>;
@@ -776,28 +822,19 @@ describe("stream-adapter — session persistence (#2859)", () => {
776
822
  "mcp__gsd-workflow__*",
777
823
  ]);
778
824
  } finally {
779
- process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
780
- process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
781
- process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
782
- process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
783
- process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
825
+ restore();
784
826
  }
785
827
  });
786
828
 
787
829
  test("buildSdkOptions auto-approves every tool for custom workflow MCP server names", () => {
788
- const prev = {
789
- GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
790
- GSD_WORKFLOW_MCP_NAME: process.env.GSD_WORKFLOW_MCP_NAME,
791
- GSD_WORKFLOW_MCP_ARGS: process.env.GSD_WORKFLOW_MCP_ARGS,
792
- GSD_WORKFLOW_MCP_ENV: process.env.GSD_WORKFLOW_MCP_ENV,
793
- GSD_WORKFLOW_MCP_CWD: process.env.GSD_WORKFLOW_MCP_CWD,
794
- };
830
+ const restore = setWorkflowMcpEnv({
831
+ GSD_WORKFLOW_MCP_COMMAND: "node",
832
+ GSD_WORKFLOW_MCP_NAME: "custom-workflow",
833
+ GSD_WORKFLOW_MCP_ARGS: JSON.stringify(["packages/mcp-server/dist/cli.js"]),
834
+ GSD_WORKFLOW_MCP_ENV: JSON.stringify({ GSD_CLI_PATH: "/tmp/gsd" }),
835
+ GSD_WORKFLOW_MCP_CWD: "/tmp/project",
836
+ });
795
837
  try {
796
- process.env.GSD_WORKFLOW_MCP_COMMAND = "node";
797
- process.env.GSD_WORKFLOW_MCP_NAME = "custom-workflow";
798
- process.env.GSD_WORKFLOW_MCP_ARGS = JSON.stringify(["packages/mcp-server/dist/cli.js"]);
799
- process.env.GSD_WORKFLOW_MCP_ENV = JSON.stringify({ GSD_CLI_PATH: "/tmp/gsd" });
800
- process.env.GSD_WORKFLOW_MCP_CWD = "/tmp/project";
801
838
 
802
839
  const options = buildSdkOptions("claude-sonnet-4-20250514", "test");
803
840
  const mcpServers = options.mcpServers as Record<string, any>;
@@ -817,22 +854,16 @@ describe("stream-adapter — session persistence (#2859)", () => {
817
854
  "mcp__custom-workflow__*",
818
855
  ]);
819
856
  } finally {
820
- process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
821
- process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
822
- process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
823
- process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
824
- process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
857
+ restore();
825
858
  }
826
859
  });
827
860
 
828
861
  test("buildSdkOptions auto-discovers bundled MCP server even without env hints", () => {
829
- const prev = {
830
- GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
831
- GSD_WORKFLOW_MCP_NAME: process.env.GSD_WORKFLOW_MCP_NAME,
832
- GSD_WORKFLOW_MCP_ARGS: process.env.GSD_WORKFLOW_MCP_ARGS,
833
- GSD_WORKFLOW_MCP_ENV: process.env.GSD_WORKFLOW_MCP_ENV,
834
- GSD_WORKFLOW_MCP_CWD: process.env.GSD_WORKFLOW_MCP_CWD,
835
- };
862
+ // Use setWorkflowMcpEnv with no values to save current state;
863
+ // restore() in finally will put it back correctly (including
864
+ // deleting any keys that started as undefined — the #4808 bug
865
+ // the naive `process.env.X = prev.X` pattern introduced).
866
+ const restore = setWorkflowMcpEnv({});
836
867
  try {
837
868
  delete process.env.GSD_WORKFLOW_MCP_COMMAND;
838
869
  delete process.env.GSD_WORKFLOW_MCP_NAME;
@@ -857,23 +888,15 @@ describe("stream-adapter — session persistence (#2859)", () => {
857
888
  }
858
889
  rmSync(emptyDir, { recursive: true, force: true });
859
890
  } finally {
860
- process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
861
- process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
862
- process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
863
- process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
864
- process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
891
+ restore();
865
892
  }
866
893
  });
867
894
 
868
895
  test("buildSdkOptions auto-detects local workflow MCP dist CLI when present", () => {
869
- const prev = {
870
- GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
871
- GSD_WORKFLOW_MCP_NAME: process.env.GSD_WORKFLOW_MCP_NAME,
872
- GSD_WORKFLOW_MCP_ARGS: process.env.GSD_WORKFLOW_MCP_ARGS,
873
- GSD_WORKFLOW_MCP_ENV: process.env.GSD_WORKFLOW_MCP_ENV,
874
- GSD_WORKFLOW_MCP_CWD: process.env.GSD_WORKFLOW_MCP_CWD,
875
- GSD_CLI_PATH: process.env.GSD_CLI_PATH,
876
- };
896
+ // GSD_CLI_PATH isn't in WORKFLOW_MCP_ENV_KEYS, so save+restore it
897
+ // manually around setWorkflowMcpEnv which handles the MCP keys.
898
+ const prevCliPath = process.env.GSD_CLI_PATH;
899
+ const restore = setWorkflowMcpEnv({});
877
900
  const originalCwd = process.cwd();
878
901
  const repoDir = mkdtempSync(join(tmpdir(), "claude-mcp-detect-"));
879
902
  try {
@@ -904,23 +927,18 @@ describe("stream-adapter — session persistence (#2859)", () => {
904
927
  } finally {
905
928
  process.chdir(originalCwd);
906
929
  rmSync(repoDir, { recursive: true, force: true });
907
- process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
908
- process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
909
- process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
910
- process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
911
- process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
912
- process.env.GSD_CLI_PATH = prev.GSD_CLI_PATH;
930
+ restore();
931
+ // GSD_CLI_PATH isn't in setWorkflowMcpEnv's scope — restore it here.
932
+ if (prevCliPath === undefined) {
933
+ delete process.env.GSD_CLI_PATH;
934
+ } else {
935
+ process.env.GSD_CLI_PATH = prevCliPath;
936
+ }
913
937
  }
914
938
  });
915
939
 
916
940
  test("buildSdkOptions preserves runtime callbacks such as onElicitation", () => {
917
- const prev = {
918
- GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
919
- GSD_WORKFLOW_MCP_NAME: process.env.GSD_WORKFLOW_MCP_NAME,
920
- GSD_WORKFLOW_MCP_ARGS: process.env.GSD_WORKFLOW_MCP_ARGS,
921
- GSD_WORKFLOW_MCP_ENV: process.env.GSD_WORKFLOW_MCP_ENV,
922
- GSD_WORKFLOW_MCP_CWD: process.env.GSD_WORKFLOW_MCP_CWD,
923
- };
941
+ const restore = setWorkflowMcpEnv({});
924
942
  const onElicitation = async () => ({ action: "decline" as const });
925
943
  try {
926
944
  delete process.env.GSD_WORKFLOW_MCP_COMMAND;
@@ -931,11 +949,7 @@ describe("stream-adapter — session persistence (#2859)", () => {
931
949
  const options = buildSdkOptions("claude-sonnet-4-20250514", "test", undefined, { onElicitation });
932
950
  assert.equal(options.onElicitation, onElicitation);
933
951
  } finally {
934
- process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
935
- process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
936
- process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
937
- process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
938
- process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
952
+ restore();
939
953
  }
940
954
  });
941
955
  });
@@ -1363,8 +1377,838 @@ describe("stream-adapter — Windows Claude path lookup (#3770)", () => {
1363
1377
  assert.equal(getClaudeLookupCommand("linux"), "which claude");
1364
1378
  });
1365
1379
 
1366
- test("parseClaudeLookupOutput keeps the first native path from multi-line lookup output", () => {
1367
- const output = "C:\\Users\\Binoy\\.local\\bin\\claude.exe\r\nC:\\Program Files\\Claude\\claude.exe\r\n";
1368
- assert.equal(parseClaudeLookupOutput(output), "C:\\Users\\Binoy\\.local\\bin\\claude.exe");
1380
+ test("parseClaudeLookupOutput prefers .exe on win32 when where output includes shims", () => {
1381
+ const output = [
1382
+ "C:\\Users\\djeff\\AppData\\Roaming\\npm\\claude",
1383
+ "C:\\Users\\djeff\\AppData\\Roaming\\npm\\claude.cmd",
1384
+ "C:\\Program Files\\Claude\\claude.exe",
1385
+ ].join("\r\n");
1386
+ assert.equal(parseClaudeLookupOutput(output, "win32"), "C:\\Program Files\\Claude\\claude.exe");
1387
+ });
1388
+
1389
+ test("parseClaudeLookupOutput keeps first line on non-win32 platforms", () => {
1390
+ const output = "/usr/local/bin/claude\n/opt/homebrew/bin/claude\n";
1391
+ assert.equal(parseClaudeLookupOutput(output, "darwin"), "/usr/local/bin/claude");
1392
+ });
1393
+
1394
+ test("normalizeClaudePathForSdk swaps Windows shim paths to bundled cli.js", () => {
1395
+ const shimPath = "C:\\Users\\djeff\\AppData\\Roaming\\npm\\claude";
1396
+ const bundled = "C:\\repo\\node_modules\\@anthropic-ai\\claude-agent-sdk\\cli.js";
1397
+ assert.equal(normalizeClaudePathForSdk(shimPath, "win32", bundled), bundled);
1398
+ assert.equal(normalizeClaudePathForSdk("C:\\Program Files\\Claude\\claude.exe", "win32", bundled), "C:\\Program Files\\Claude\\claude.exe");
1399
+ });
1400
+
1401
+ test("resolveBundledClaudeCliPath returns a .js path when SDK package is present", () => {
1402
+ const resolved = resolveBundledClaudeCliPath();
1403
+ assert.ok(resolved, "expected sdk cli.js to be resolvable in test workspace");
1404
+ assert.match(resolved!, /[\\/]@anthropic-ai[\\/]claude-agent-sdk[\\/]cli\.js$/);
1405
+ });
1406
+ });
1407
+
1408
+ // ---------------------------------------------------------------------------
1409
+ // canUseTool handler (#4383)
1410
+ // ---------------------------------------------------------------------------
1411
+
1412
+ describe("stream-adapter — canUseTool handler", () => {
1413
+ function makeOptions(overrides: Partial<{ signal: AbortSignal; suggestions: Array<Record<string, unknown>>; title: string; description: string; toolUseID: string }> = {}) {
1414
+ return {
1415
+ signal: overrides.signal ?? new AbortController().signal,
1416
+ toolUseID: overrides.toolUseID ?? "toolu_test123",
1417
+ ...(overrides.title !== undefined ? { title: overrides.title } : {}),
1418
+ ...(overrides.description !== undefined ? { description: overrides.description } : {}),
1419
+ ...(overrides.suggestions !== undefined ? { suggestions: overrides.suggestions } : {}),
1420
+ };
1421
+ }
1422
+
1423
+ // Point process.cwd() at an empty temp dir so the real repo's
1424
+ // .claude/settings.local.json (which may already contain rules like
1425
+ // "Bash(gh pr list:*)") does not short-circuit the permission flow.
1426
+ // Returns a cleanup function that restores cwd and removes the temp dir.
1427
+ // biome-ignore lint/suspicious/noExplicitAny: test-only monkey-patch
1428
+ function withIsolatedCwd(): () => void {
1429
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-canusetool-")));
1430
+ const orig = process.cwd;
1431
+ process.cwd = () => dir;
1432
+ return () => {
1433
+ process.cwd = orig;
1434
+ rmSync(dir, { recursive: true, force: true });
1435
+ };
1436
+ }
1437
+
1438
+ test("returns undefined when no UI context is provided", () => {
1439
+ const handler = createClaudeCodeCanUseToolHandler(undefined);
1440
+ assert.equal(handler, undefined);
1441
+ });
1442
+
1443
+ test("shows select dialog with Allow/Always Allow/Deny and returns allow", async () => {
1444
+ let selectPrompt = "";
1445
+ let selectOptions: string[] = [];
1446
+ const ui = {
1447
+ select: async (prompt: string, options: string[]) => {
1448
+ selectPrompt = prompt;
1449
+ selectOptions = options;
1450
+ return "Allow";
1451
+ },
1452
+ };
1453
+
1454
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1455
+ assert.ok(handler);
1456
+
1457
+ const input = { command: "ls -la" };
1458
+ const result = await handler!("Bash", input, makeOptions({
1459
+ title: "Claude wants to run: ls -la",
1460
+ description: "List directory contents",
1461
+ }));
1462
+
1463
+ assert.equal(result.behavior, "allow");
1464
+ assert.deepEqual((result as any).updatedInput, input);
1465
+ assert.equal((result as any).toolUseID, "toolu_test123");
1466
+ // Allow (one-time) should NOT include updatedPermissions
1467
+ assert.equal((result as any).updatedPermissions, undefined);
1468
+ assert.deepEqual(selectOptions, ["Allow", "Always Allow", "Deny"]);
1469
+ // Prompt includes title and input summary
1470
+ assert.ok(selectPrompt.includes("Claude wants to run: ls -la"));
1471
+ assert.ok(selectPrompt.includes("ls -la"));
1472
+ });
1473
+
1474
+ test("returns deny when user selects Deny", async () => {
1475
+ const ui = {
1476
+ select: async () => "Deny",
1477
+ };
1478
+
1479
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1480
+ const result = await handler!("Bash", { command: "rm -rf /" }, makeOptions());
1481
+
1482
+ assert.equal(result.behavior, "deny");
1483
+ assert.equal((result as any).message, "User denied");
1484
+ assert.equal((result as any).toolUseID, "toolu_test123");
1485
+ });
1486
+
1487
+ test("returns deny when user dismisses dialog (undefined)", async () => {
1488
+ const ui = {
1489
+ select: async () => undefined,
1490
+ };
1491
+
1492
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1493
+ const result = await handler!("Bash", { command: "echo hi" }, makeOptions());
1494
+
1495
+ assert.equal(result.behavior, "deny");
1496
+ assert.equal((result as any).message, "User denied");
1497
+ });
1498
+
1499
+ test("Always Allow for Bash patches SDK suggestions with smart ruleContent", async () => {
1500
+ const notified: string[] = [];
1501
+ const ui = { select: async (_p: string, opts: string[]) => opts.find((o) => o.startsWith("Always Allow"))!, notify: (msg: string) => notified.push(msg) };
1502
+ const suggestions = [{
1503
+ type: "addRules",
1504
+ rules: [{ toolName: "Bash", ruleContent: "ls -la /tmp" }],
1505
+ behavior: "allow",
1506
+ destination: "localSettings",
1507
+ }];
1508
+
1509
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1510
+ const result = await handler!("Bash", { command: "ls -la /tmp" }, makeOptions({ suggestions }));
1511
+
1512
+ assert.equal(result.behavior, "allow");
1513
+ // Should patch ruleContent with our smart pattern, preserving SDK structure
1514
+ assert.deepEqual((result as any).updatedPermissions, [{
1515
+ type: "addRules",
1516
+ rules: [{ toolName: "Bash", ruleContent: "ls:*" }],
1517
+ behavior: "allow",
1518
+ destination: "localSettings",
1519
+ }]);
1520
+ assert.equal(notified.length, 1);
1521
+ assert.ok(notified[0].includes("Saved:") && notified[0].includes("Bash(ls:*)"));
1522
+ });
1523
+
1524
+ test("Always Allow for Bash with subcommand-sensitive CLI captures verb", async () => {
1525
+ const cleanup = withIsolatedCwd();
1526
+ try {
1527
+ const notified: string[] = [];
1528
+ // First select call: pick "Always Allow ..."; second call (level
1529
+ // picker): pick the "git push" granularity explicitly.
1530
+ let selectCall = 0;
1531
+ const ui = {
1532
+ select: async (_p: string, opts: string[]) => {
1533
+ selectCall++;
1534
+ if (selectCall === 1) return opts.find((o) => o.startsWith("Always Allow"))!;
1535
+ return "Bash(git push:*)";
1536
+ },
1537
+ notify: (msg: string) => notified.push(msg),
1538
+ };
1539
+ const suggestions = [{
1540
+ type: "addRules",
1541
+ rules: [{ toolName: "Bash", ruleContent: "git push origin main" }],
1542
+ behavior: "allow",
1543
+ destination: "localSettings",
1544
+ }];
1545
+
1546
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1547
+ const result = await handler!("Bash", { command: "git push origin main" }, makeOptions({ suggestions }));
1548
+
1549
+ assert.equal(result.behavior, "allow");
1550
+ assert.deepEqual((result as any).updatedPermissions, [{
1551
+ type: "addRules",
1552
+ rules: [{ toolName: "Bash", ruleContent: "git push:*" }],
1553
+ behavior: "allow",
1554
+ destination: "localSettings",
1555
+ }]);
1556
+ assert.ok(notified[0].includes("Saved:") && notified[0].includes("Bash(git push:*)"));
1557
+ } finally {
1558
+ cleanup();
1559
+ }
1560
+ });
1561
+
1562
+ test("Always Allow for Bash without suggestions builds proper PermissionUpdate", async () => {
1563
+ const cleanup = withIsolatedCwd();
1564
+ try {
1565
+ const notified: string[] = [];
1566
+ let selectCall = 0;
1567
+ const ui = {
1568
+ select: async (_p: string, opts: string[]) => {
1569
+ selectCall++;
1570
+ if (selectCall === 1) return opts.find((o) => o.startsWith("Always Allow"))!;
1571
+ return "Bash(gh pr list:*)";
1572
+ },
1573
+ notify: (msg: string) => notified.push(msg),
1574
+ };
1575
+
1576
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1577
+ const result = await handler!("Bash", { command: "gh pr list" }, makeOptions());
1578
+
1579
+ assert.equal(result.behavior, "allow");
1580
+ // No SDK suggestions → builds PermissionUpdate from scratch
1581
+ assert.deepEqual((result as any).updatedPermissions, [{
1582
+ type: "addRules",
1583
+ rules: [{ toolName: "Bash", ruleContent: "gh pr list:*" }],
1584
+ behavior: "allow",
1585
+ destination: "localSettings",
1586
+ }]);
1587
+ assert.ok(notified[0].includes("Saved:") && notified[0].includes("Bash(gh pr list:*)"));
1588
+ } finally {
1589
+ cleanup();
1590
+ }
1591
+ });
1592
+
1593
+ test("Always Allow for non-Bash tools passes SDK suggestions through", async () => {
1594
+ const notified: string[] = [];
1595
+ const ui = { select: async (_p: string, opts: string[]) => opts.find((o) => o.startsWith("Always Allow"))!, notify: (msg: string) => notified.push(msg) };
1596
+ const suggestions = [{
1597
+ type: "addRules",
1598
+ rules: [{ toolName: "Write" }],
1599
+ behavior: "allow",
1600
+ destination: "localSettings",
1601
+ }];
1602
+
1603
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1604
+ const result = await handler!("Write", { file_path: "/tmp/test.txt" }, makeOptions({ suggestions }));
1605
+
1606
+ assert.equal(result.behavior, "allow");
1607
+ assert.deepEqual((result as any).updatedPermissions, suggestions);
1608
+ // Non-Bash tools don't emit a post-selection notification (only Bash runs the level picker)
1609
+ assert.equal(notified.length, 0);
1610
+ });
1611
+
1612
+ test("Always Allow for non-Bash without suggestions omits updatedPermissions", async () => {
1613
+ const notified: string[] = [];
1614
+ const ui = { select: async (_p: string, opts: string[]) => opts.find((o) => o.startsWith("Always Allow"))!, notify: (msg: string) => notified.push(msg) };
1615
+
1616
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1617
+ const result = await handler!("Write", { file_path: "/tmp/test.txt" }, makeOptions());
1618
+
1619
+ assert.equal(result.behavior, "allow");
1620
+ assert.equal((result as any).updatedPermissions, undefined);
1621
+ // No suggestions → no notification
1622
+ assert.equal(notified.length, 0);
1623
+ });
1624
+
1625
+ test("prompt includes command text for Bash tools", async () => {
1626
+ let selectPrompt = "";
1627
+ const ui = {
1628
+ select: async (prompt: string) => {
1629
+ selectPrompt = prompt;
1630
+ return "Allow";
1631
+ },
1632
+ };
1633
+
1634
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1635
+ await handler!("Bash", { command: "git status" }, makeOptions());
1636
+ assert.ok(selectPrompt.includes("git status"), `prompt should include command: ${selectPrompt}`);
1637
+ });
1638
+
1639
+ test("prompt includes file_path for file tools", async () => {
1640
+ let selectPrompt = "";
1641
+ const ui = {
1642
+ select: async (prompt: string) => {
1643
+ selectPrompt = prompt;
1644
+ return "Allow";
1645
+ },
1646
+ };
1647
+
1648
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1649
+ await handler!("Write", { file_path: "/tmp/test.txt", content: "hello" }, makeOptions());
1650
+ assert.ok(selectPrompt.includes("/tmp/test.txt"), `prompt should include file path: ${selectPrompt}`);
1651
+ });
1652
+
1653
+ test("uses title from options when available", async () => {
1654
+ let selectPrompt = "";
1655
+ const ui = {
1656
+ select: async (prompt: string) => {
1657
+ selectPrompt = prompt;
1658
+ return "Allow";
1659
+ },
1660
+ };
1661
+
1662
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1663
+ await handler!("WebFetch", {}, makeOptions({ title: "Claude wants to fetch: https://example.com" }));
1664
+ assert.ok(selectPrompt.includes("Claude wants to fetch: https://example.com"));
1665
+ });
1666
+
1667
+ test("falls back to default title when options.title is missing", async () => {
1668
+ let selectPrompt = "";
1669
+ const ui = {
1670
+ select: async (prompt: string) => {
1671
+ selectPrompt = prompt;
1672
+ return "Allow";
1673
+ },
1674
+ };
1675
+
1676
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1677
+ await handler!("WebFetch", { url: "https://example.com" }, makeOptions());
1678
+ assert.ok(selectPrompt.includes("Allow Claude Code to use: WebFetch?"));
1679
+ });
1680
+
1681
+ test("returns deny when signal is already aborted", async () => {
1682
+ const ui = {
1683
+ select: async () => { throw new Error("should not be called"); },
1684
+ };
1685
+
1686
+ const controller = new AbortController();
1687
+ controller.abort();
1688
+
1689
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1690
+ const result = await handler!("Bash", {}, makeOptions({ signal: controller.signal }));
1691
+
1692
+ assert.equal(result.behavior, "deny");
1693
+ assert.equal((result as any).message, "Aborted");
1694
+ });
1695
+
1696
+ test("returns deny when ui.select throws", async () => {
1697
+ const ui = {
1698
+ select: async () => { throw new Error("dialog crashed"); },
1699
+ };
1700
+
1701
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1702
+ const result = await handler!("Bash", {}, makeOptions());
1703
+
1704
+ assert.equal(result.behavior, "deny");
1705
+ assert.equal((result as any).message, "Aborted");
1706
+ });
1707
+
1708
+ test("buildSdkOptions passes canUseTool through extraOptions", () => {
1709
+ const canUseTool = async () => ({ behavior: "allow" as const, updatedInput: {}, toolUseID: "test" });
1710
+ const opts = buildSdkOptions("claude-sonnet-4-6", "test", undefined, { canUseTool });
1711
+ assert.equal(opts.canUseTool, canUseTool);
1712
+ });
1713
+
1714
+ test("Always Allow shows level picker and user broadens to base command", async () => {
1715
+ const cleanup = withIsolatedCwd();
1716
+ try {
1717
+ const prompts: string[] = [];
1718
+ const levelOpts: string[][] = [];
1719
+ let selectCall = 0;
1720
+ const ui = {
1721
+ select: async (prompt: string, opts: string[]) => {
1722
+ prompts.push(prompt);
1723
+ selectCall++;
1724
+ if (selectCall === 1) return opts.find((o) => o.startsWith("Always Allow"))!;
1725
+ levelOpts.push(opts);
1726
+ return "Bash(gh:*)";
1727
+ },
1728
+ notify: () => {},
1729
+ };
1730
+
1731
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1732
+ const result = await handler!("Bash", { command: "gh pr list" }, makeOptions());
1733
+
1734
+ assert.equal(result.behavior, "allow");
1735
+ assert.deepEqual((result as any).updatedPermissions, [{
1736
+ type: "addRules",
1737
+ rules: [{ toolName: "Bash", ruleContent: "gh:*" }],
1738
+ behavior: "allow",
1739
+ destination: "localSettings",
1740
+ }]);
1741
+ // Second dialog offered every granularity level
1742
+ assert.deepEqual(levelOpts[0], [
1743
+ "Bash(gh:*)",
1744
+ "Bash(gh pr:*)",
1745
+ "Bash(gh pr list:*)",
1746
+ ]);
1747
+ assert.ok(prompts[1].includes("Save permission at which level?"));
1748
+ } finally {
1749
+ cleanup();
1750
+ }
1751
+ });
1752
+
1753
+ test("Always Allow narrows to mid-level pattern when user picks Bash(gh pr:*)", async () => {
1754
+ const cleanup = withIsolatedCwd();
1755
+ try {
1756
+ let selectCall = 0;
1757
+ const ui = {
1758
+ select: async (_p: string, opts: string[]) => {
1759
+ selectCall++;
1760
+ if (selectCall === 1) return opts.find((o) => o.startsWith("Always Allow"))!;
1761
+ return "Bash(gh pr:*)";
1762
+ },
1763
+ notify: () => {},
1764
+ };
1765
+
1766
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1767
+ const result = await handler!("Bash", { command: "gh pr list --limit 5" }, makeOptions());
1768
+
1769
+ assert.equal(result.behavior, "allow");
1770
+ assert.deepEqual((result as any).updatedPermissions, [{
1771
+ type: "addRules",
1772
+ rules: [{ toolName: "Bash", ruleContent: "gh pr:*" }],
1773
+ behavior: "allow",
1774
+ destination: "localSettings",
1775
+ }]);
1776
+ } finally {
1777
+ cleanup();
1778
+ }
1779
+ });
1780
+
1781
+ test("Always Allow skips level picker when only one pattern is available", async () => {
1782
+ const cleanup = withIsolatedCwd();
1783
+ try {
1784
+ const prompts: string[] = [];
1785
+ const ui = {
1786
+ select: async (prompt: string, opts: string[]) => {
1787
+ prompts.push(prompt);
1788
+ return opts.find((o) => o.startsWith("Always Allow"))!;
1789
+ },
1790
+ notify: () => {},
1791
+ };
1792
+
1793
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1794
+ const result = await handler!("Bash", { command: "ls -la /tmp" }, makeOptions());
1795
+
1796
+ assert.equal(result.behavior, "allow");
1797
+ // "ls" has no subcommand tokens before the flag → single-option path
1798
+ assert.equal(prompts.length, 1, "should not show a second dialog");
1799
+ assert.deepEqual((result as any).updatedPermissions, [{
1800
+ type: "addRules",
1801
+ rules: [{ toolName: "Bash", ruleContent: "ls:*" }],
1802
+ behavior: "allow",
1803
+ destination: "localSettings",
1804
+ }]);
1805
+ } finally {
1806
+ cleanup();
1807
+ }
1808
+ });
1809
+
1810
+ test("Always Allow denies the tool when level picker is dismissed", async () => {
1811
+ const cleanup = withIsolatedCwd();
1812
+ try {
1813
+ const notified: string[] = [];
1814
+ let selectCall = 0;
1815
+ const ui = {
1816
+ select: async (_p: string, opts: string[]) => {
1817
+ selectCall++;
1818
+ if (selectCall === 1) return opts.find((o) => o.startsWith("Always Allow"))!;
1819
+ return undefined; // user dismissed level picker
1820
+ },
1821
+ notify: (msg: string) => notified.push(msg),
1822
+ };
1823
+
1824
+ const handler = createClaudeCodeCanUseToolHandler(ui as any);
1825
+ const result = await handler!("Bash", { command: "gh pr list" }, makeOptions());
1826
+
1827
+ // Dismissing the level picker cancels the tool use — a one-time allow
1828
+ // would leave the spawned agent running even though the user bailed.
1829
+ assert.equal(result.behavior, "deny");
1830
+ assert.equal((result as any).updatedPermissions, undefined);
1831
+ assert.equal(notified.length, 0, "no 'Saved:' notification when nothing was saved");
1832
+ } finally {
1833
+ cleanup();
1834
+ }
1835
+ });
1836
+ });
1837
+
1838
+ // ---------------------------------------------------------------------------
1839
+ // buildBashPermissionPattern — smart permission granularity
1840
+ // ---------------------------------------------------------------------------
1841
+
1842
+ describe("buildBashPermissionPattern", () => {
1843
+ test("simple command wildcards all args", () => {
1844
+ assert.equal(buildBashPermissionPattern("ping -n 4 localhost"), "Bash(ping:*)");
1845
+ assert.equal(buildBashPermissionPattern("echo hello world"), "Bash(echo:*)");
1846
+ assert.equal(buildBashPermissionPattern("ls -la /tmp"), "Bash(ls:*)");
1847
+ assert.equal(buildBashPermissionPattern("node server.js"), "Bash(node:*)");
1848
+ });
1849
+
1850
+ test("git captures one subcommand", () => {
1851
+ assert.equal(buildBashPermissionPattern("git push origin main"), "Bash(git push:*)");
1852
+ assert.equal(buildBashPermissionPattern("git log --oneline"), "Bash(git log:*)");
1853
+ assert.equal(buildBashPermissionPattern("git status"), "Bash(git status:*)");
1854
+ });
1855
+
1856
+ test("gh captures two subcommands", () => {
1857
+ assert.equal(buildBashPermissionPattern("gh pr list"), "Bash(gh pr list:*)");
1858
+ assert.equal(buildBashPermissionPattern("gh pr create --title foo"), "Bash(gh pr create:*)");
1859
+ assert.equal(buildBashPermissionPattern("gh issue view 123"), "Bash(gh issue view:*)");
1860
+ });
1861
+
1862
+ test("npm captures one subcommand", () => {
1863
+ assert.equal(buildBashPermissionPattern("npm install lodash"), "Bash(npm install:*)");
1864
+ assert.equal(buildBashPermissionPattern("npm publish"), "Bash(npm publish:*)");
1865
+ assert.equal(buildBashPermissionPattern("npm run test"), "Bash(npm run:*)");
1866
+ });
1867
+
1868
+ test("npx captures package name", () => {
1869
+ assert.equal(buildBashPermissionPattern("npx vitest run"), "Bash(npx vitest:*)");
1870
+ assert.equal(buildBashPermissionPattern("npx --version"), "Bash(npx --version:*)");
1871
+ });
1872
+
1873
+ test("docker captures one subcommand", () => {
1874
+ assert.equal(buildBashPermissionPattern("docker ps -a"), "Bash(docker ps:*)");
1875
+ assert.equal(buildBashPermissionPattern("docker rm container1"), "Bash(docker rm:*)");
1876
+ });
1877
+
1878
+ test("aws captures two subcommands", () => {
1879
+ assert.equal(buildBashPermissionPattern("aws s3 cp file.txt s3://bucket/"), "Bash(aws s3 cp:*)");
1880
+ assert.equal(buildBashPermissionPattern("aws ec2 describe-instances"), "Bash(aws ec2 describe-instances:*)");
1881
+ });
1882
+
1883
+ test("skips sudo wrapper", () => {
1884
+ assert.equal(buildBashPermissionPattern("sudo ping localhost"), "Bash(ping:*)");
1885
+ assert.equal(buildBashPermissionPattern("sudo git push"), "Bash(git push:*)");
1886
+ });
1887
+
1888
+ test("skips env wrapper and VAR=val assignments", () => {
1889
+ assert.equal(buildBashPermissionPattern("env NODE_ENV=prod node server.js"), "Bash(node:*)");
1890
+ assert.equal(buildBashPermissionPattern("NODE_ENV=prod node server.js"), "Bash(node:*)");
1891
+ assert.equal(buildBashPermissionPattern("FOO=bar BAZ=qux git push"), "Bash(git push:*)");
1892
+ });
1893
+
1894
+ test("strips path from executable", () => {
1895
+ assert.equal(buildBashPermissionPattern("/usr/bin/git push"), "Bash(git push:*)");
1896
+ assert.equal(buildBashPermissionPattern("C:\\Windows\\ping.exe localhost"), "Bash(ping:*)");
1897
+ });
1898
+
1899
+ test("empty or whitespace-only command", () => {
1900
+ assert.equal(buildBashPermissionPattern(""), "Bash(*)");
1901
+ assert.equal(buildBashPermissionPattern(" "), "Bash(*)");
1902
+ });
1903
+
1904
+ test("chained commands — extracts pattern from the meaningful segment", () => {
1905
+ assert.equal(buildBashPermissionPattern("cd /foo && gh pr list --limit 5"), "Bash(gh pr list:*)");
1906
+ assert.equal(buildBashPermissionPattern("cd C:/Users/djeff/repos/gsd-2 && gh pr list --limit 5"), "Bash(gh pr list:*)");
1907
+ assert.equal(buildBashPermissionPattern("cd /tmp && git push origin main"), "Bash(git push:*)");
1908
+ assert.equal(buildBashPermissionPattern("export FOO=1 && npm install lodash"), "Bash(npm install:*)");
1909
+ assert.equal(buildBashPermissionPattern("mkdir -p out; docker ps -a"), "Bash(docker ps:*)");
1910
+ assert.equal(buildBashPermissionPattern("echo start || ping localhost"), "Bash(ping:*)");
1911
+ });
1912
+
1913
+ test("skips trailing || true / || : error suppressors", () => {
1914
+ assert.equal(
1915
+ buildBashPermissionPattern("cd C:/Users/djeff/repos/gsd-2 && gh pr create --dry-run --title \"test\" --body \"test\" 2>&1 || true"),
1916
+ "Bash(gh pr create:*)",
1917
+ );
1918
+ assert.equal(buildBashPermissionPattern("gh pr list || true"), "Bash(gh pr list:*)");
1919
+ assert.equal(buildBashPermissionPattern("git push || :"), "Bash(git push:*)");
1920
+ assert.equal(buildBashPermissionPattern("cd /tmp && npm install || echo failed"), "Bash(npm install:*)");
1921
+ });
1922
+
1923
+ test("single command is unaffected by chain extraction", () => {
1924
+ assert.equal(buildBashPermissionPattern("gh pr list"), "Bash(gh pr list:*)");
1925
+ assert.equal(buildBashPermissionPattern("git push origin main"), "Bash(git push:*)");
1926
+ });
1927
+ });
1928
+
1929
+ // ---------------------------------------------------------------------------
1930
+ // buildBashPermissionPatternOptions — granularity level menu
1931
+ // ---------------------------------------------------------------------------
1932
+
1933
+ describe("buildBashPermissionPatternOptions", () => {
1934
+ test("offers every prefix from base to full subcommand chain", () => {
1935
+ assert.deepEqual(buildBashPermissionPatternOptions("gh pr list"), [
1936
+ "Bash(gh:*)",
1937
+ "Bash(gh pr:*)",
1938
+ "Bash(gh pr list:*)",
1939
+ ]);
1940
+ assert.deepEqual(buildBashPermissionPatternOptions("git push origin main"), [
1941
+ "Bash(git:*)",
1942
+ "Bash(git push:*)",
1943
+ "Bash(git push origin:*)",
1944
+ "Bash(git push origin main:*)",
1945
+ ]);
1946
+ });
1947
+
1948
+ test("stops at first flag — flags are args, not verbs", () => {
1949
+ assert.deepEqual(buildBashPermissionPatternOptions("gh pr create --title foo"), [
1950
+ "Bash(gh:*)",
1951
+ "Bash(gh pr:*)",
1952
+ "Bash(gh pr create:*)",
1953
+ ]);
1954
+ assert.deepEqual(buildBashPermissionPatternOptions("git log --oneline"), [
1955
+ "Bash(git:*)",
1956
+ "Bash(git log:*)",
1957
+ ]);
1958
+ });
1959
+
1960
+ test("single-option when there is no subcommand to choose from", () => {
1961
+ assert.deepEqual(buildBashPermissionPatternOptions("ls -la /tmp"), ["Bash(ls:*)"]);
1962
+ assert.deepEqual(buildBashPermissionPatternOptions("ping -n 4 localhost"), ["Bash(ping:*)"]);
1963
+ assert.deepEqual(buildBashPermissionPatternOptions("node"), ["Bash(node:*)"]);
1964
+ });
1965
+
1966
+ test("extracts meaningful segment from compound commands", () => {
1967
+ assert.deepEqual(buildBashPermissionPatternOptions("cd /foo && gh pr list"), [
1968
+ "Bash(gh:*)",
1969
+ "Bash(gh pr:*)",
1970
+ "Bash(gh pr list:*)",
1971
+ ]);
1972
+ assert.deepEqual(buildBashPermissionPatternOptions("gh pr create --dry-run || true"), [
1973
+ "Bash(gh:*)",
1974
+ "Bash(gh pr:*)",
1975
+ "Bash(gh pr create:*)",
1976
+ ]);
1977
+ });
1978
+
1979
+ test("caps at three subcommand tokens to keep the menu short", () => {
1980
+ const result = buildBashPermissionPatternOptions("foo bar baz qux quux corge");
1981
+ // base + 3 sub tokens = 4 patterns max
1982
+ assert.equal(result.length, 4);
1983
+ assert.deepEqual(result, [
1984
+ "Bash(foo:*)",
1985
+ "Bash(foo bar:*)",
1986
+ "Bash(foo bar baz:*)",
1987
+ "Bash(foo bar baz qux:*)",
1988
+ ]);
1989
+ });
1990
+
1991
+ test("skips sudo/env wrappers like the single-pattern variant", () => {
1992
+ assert.deepEqual(buildBashPermissionPatternOptions("sudo git push origin"), [
1993
+ "Bash(git:*)",
1994
+ "Bash(git push:*)",
1995
+ "Bash(git push origin:*)",
1996
+ ]);
1997
+ assert.deepEqual(buildBashPermissionPatternOptions("NODE_ENV=prod node server.js"), [
1998
+ "Bash(node:*)",
1999
+ "Bash(node server.js:*)",
2000
+ ]);
2001
+ });
2002
+
2003
+ test("empty command returns the catch-all pattern", () => {
2004
+ assert.deepEqual(buildBashPermissionPatternOptions(""), ["Bash(*)"]);
2005
+ assert.deepEqual(buildBashPermissionPatternOptions(" "), ["Bash(*)"]);
2006
+ });
2007
+ });
2008
+
2009
+ // ---------------------------------------------------------------------------
2010
+ // bashCommandMatchesSavedRules — compound command bypass for saved rules
2011
+ // ---------------------------------------------------------------------------
2012
+
2013
+ describe("bashCommandMatchesSavedRules — compound command bypass", () => {
2014
+ let tempDir: string;
2015
+ let originalCwd: string;
2016
+
2017
+ // Create a temp project directory with .claude/settings.local.json
2018
+ function setupSettings(allow: string[]): void {
2019
+ const claudeDir = join(tempDir, ".claude");
2020
+ mkdirSync(claudeDir, { recursive: true });
2021
+ writeFileSync(
2022
+ join(claudeDir, "settings.local.json"),
2023
+ JSON.stringify({ permissions: { allow } }),
2024
+ );
2025
+ }
2026
+
2027
+ // biome-ignore lint/suspicious/noExplicitAny: test-only monkey-patch
2028
+ let origCwd: any;
2029
+
2030
+ // Monkey-patch process.cwd() to point at our temp dir
2031
+ function setCwd(dir: string): void {
2032
+ origCwd = process.cwd;
2033
+ process.cwd = () => dir;
2034
+ }
2035
+ function restoreCwd(): void {
2036
+ if (origCwd) process.cwd = origCwd;
2037
+ }
2038
+
2039
+ test("matches cd-prefixed compound command against saved prefix rule", () => {
2040
+ tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
2041
+ try {
2042
+ setupSettings(["Bash(gh pr list:*)"]);
2043
+ setCwd(tempDir);
2044
+ assert.equal(
2045
+ bashCommandMatchesSavedRules("cd /some/path && gh pr list --limit 5"),
2046
+ true,
2047
+ );
2048
+ } finally {
2049
+ restoreCwd();
2050
+ rmSync(tempDir, { recursive: true, force: true });
2051
+ }
2052
+ });
2053
+
2054
+ test("matches cd-prefixed compound command with exact subcommand", () => {
2055
+ tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
2056
+ try {
2057
+ setupSettings(["Bash(gh pr list:*)"]);
2058
+ setCwd(tempDir);
2059
+ assert.equal(
2060
+ bashCommandMatchesSavedRules("cd C:/Users/foo/repos/bar && gh pr list"),
2061
+ true,
2062
+ );
2063
+ } finally {
2064
+ restoreCwd();
2065
+ rmSync(tempDir, { recursive: true, force: true });
2066
+ }
2067
+ });
2068
+
2069
+ test("rejects when leading segment is not cd", () => {
2070
+ tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
2071
+ try {
2072
+ setupSettings(["Bash(gh pr list:*)"]);
2073
+ setCwd(tempDir);
2074
+ // "rm -rf /tmp" is not a cd command — should not auto-approve
2075
+ assert.equal(
2076
+ bashCommandMatchesSavedRules("rm -rf /tmp && gh pr list"),
2077
+ false,
2078
+ );
2079
+ } finally {
2080
+ restoreCwd();
2081
+ rmSync(tempDir, { recursive: true, force: true });
2082
+ }
2083
+ });
2084
+
2085
+ test("rejects when meaningful segment does not match any rule", () => {
2086
+ tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
2087
+ try {
2088
+ setupSettings(["Bash(gh pr list:*)"]);
2089
+ setCwd(tempDir);
2090
+ assert.equal(
2091
+ bashCommandMatchesSavedRules("cd /path && gh issue create --title foo"),
2092
+ false,
2093
+ );
2094
+ } finally {
2095
+ restoreCwd();
2096
+ rmSync(tempDir, { recursive: true, force: true });
2097
+ }
2098
+ });
2099
+
2100
+ test("matches simple (non-compound) commands against on-disk rules", () => {
2101
+ tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
2102
+ try {
2103
+ setupSettings(["Bash(gh pr list:*)"]);
2104
+ setCwd(tempDir);
2105
+ // Simple commands must also be checked — the SDK's in-memory cache
2106
+ // may be stale if the rule was added mid-session via "Always Allow"
2107
+ assert.equal(bashCommandMatchesSavedRules("gh pr list --limit 5"), true);
2108
+ assert.equal(bashCommandMatchesSavedRules("gh pr list"), true);
2109
+ } finally {
2110
+ restoreCwd();
2111
+ rmSync(tempDir, { recursive: true, force: true });
2112
+ }
2113
+ });
2114
+
2115
+ test("returns false for simple commands with no matching rule", () => {
2116
+ tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
2117
+ try {
2118
+ setupSettings(["Bash(gh pr list:*)"]);
2119
+ setCwd(tempDir);
2120
+ assert.equal(bashCommandMatchesSavedRules("gh issue list --limit 5"), false);
2121
+ } finally {
2122
+ restoreCwd();
2123
+ rmSync(tempDir, { recursive: true, force: true });
2124
+ }
2125
+ });
2126
+
2127
+ test("returns false when no settings file exists", () => {
2128
+ tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
2129
+ try {
2130
+ // No .claude/settings.local.json created
2131
+ setCwd(tempDir);
2132
+ assert.equal(
2133
+ bashCommandMatchesSavedRules("cd /path && gh pr list"),
2134
+ false,
2135
+ );
2136
+ } finally {
2137
+ restoreCwd();
2138
+ rmSync(tempDir, { recursive: true, force: true });
2139
+ }
2140
+ });
2141
+
2142
+ test("matches exact rule (non-prefix)", () => {
2143
+ tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
2144
+ try {
2145
+ setupSettings(["Bash(ping -n 4 localhost)"]);
2146
+ setCwd(tempDir);
2147
+ assert.equal(
2148
+ bashCommandMatchesSavedRules("cd /path && ping -n 4 localhost"),
2149
+ true,
2150
+ );
2151
+ } finally {
2152
+ restoreCwd();
2153
+ rmSync(tempDir, { recursive: true, force: true });
2154
+ }
2155
+ });
2156
+
2157
+ test("handles multiple cd segments before the meaningful command", () => {
2158
+ tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
2159
+ try {
2160
+ setupSettings(["Bash(npm install:*)"]);
2161
+ setCwd(tempDir);
2162
+ assert.equal(
2163
+ bashCommandMatchesSavedRules("cd /home && cd project && npm install lodash"),
2164
+ true,
2165
+ );
2166
+ } finally {
2167
+ restoreCwd();
2168
+ rmSync(tempDir, { recursive: true, force: true });
2169
+ }
2170
+ });
2171
+
2172
+ test("matches compound command with trailing || true suppressor", () => {
2173
+ tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
2174
+ try {
2175
+ setupSettings(["Bash(gh pr create:*)"]);
2176
+ setCwd(tempDir);
2177
+ assert.equal(
2178
+ bashCommandMatchesSavedRules('cd C:/Users/djeff/repos/gsd-2 && gh pr create --dry-run --title "test" --body "test" 2>&1 || true'),
2179
+ true,
2180
+ );
2181
+ assert.equal(
2182
+ bashCommandMatchesSavedRules("gh pr create --dry-run || true"),
2183
+ true,
2184
+ );
2185
+ assert.equal(
2186
+ bashCommandMatchesSavedRules("cd /tmp && git push || :"),
2187
+ false, // rule is for gh pr create, not git push
2188
+ );
2189
+ } finally {
2190
+ restoreCwd();
2191
+ rmSync(tempDir, { recursive: true, force: true });
2192
+ }
2193
+ });
2194
+
2195
+ test("reads rules from settings.json as well as settings.local.json", () => {
2196
+ tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
2197
+ try {
2198
+ const claudeDir = join(tempDir, ".claude");
2199
+ mkdirSync(claudeDir, { recursive: true });
2200
+ writeFileSync(
2201
+ join(claudeDir, "settings.json"),
2202
+ JSON.stringify({ permissions: { allow: ["Bash(git push:*)"] } }),
2203
+ );
2204
+ setCwd(tempDir);
2205
+ assert.equal(
2206
+ bashCommandMatchesSavedRules("cd /repo && git push origin main"),
2207
+ true,
2208
+ );
2209
+ } finally {
2210
+ restoreCwd();
2211
+ rmSync(tempDir, { recursive: true, force: true });
2212
+ }
1369
2213
  });
1370
2214
  });