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
package/README.md CHANGED
@@ -288,7 +288,7 @@ Auto mode is a state machine driven by files on disk. It reads `.gsd/STATE.md`,
288
288
 
289
289
  5. **Provider error recovery** — Transient provider errors (rate limits, 500/503 server errors, overloaded) auto-resume after a delay. Permanent errors (auth, billing) pause for manual review. The model fallback chain retries transient network errors before switching models.
290
290
 
291
- 6. **Stuck detection** — A sliding-window detector identifies repeated dispatch patterns (including multi-unit cycles). On detection, it retries once with a deep diagnostic. If it fails again, auto mode stops with the exact file it expected.
291
+ 6. **Stuck and artifact detection** — A sliding-window detector identifies repeated dispatch patterns (including multi-unit cycles). Missing expected artifacts use a separate bounded path: GSD retries artifact verification up to 3 times with failure context, then pauses auto mode with the missing artifact error instead of looping indefinitely.
292
292
 
293
293
  7. **Timeout supervision** — Soft timeout warns the LLM to wrap up. Idle watchdog detects stalls. Hard timeout pauses auto mode. Recovery steering nudges the LLM to finish durable output before giving up.
294
294
 
@@ -28,7 +28,11 @@ function execClaudeCheck(args) {
28
28
  let lastError;
29
29
  for (const command of CLAUDE_COMMAND_CANDIDATES) {
30
30
  try {
31
- return execFileSync(command, args, { timeout: 5_000, stdio: 'pipe' });
31
+ return execFileSync(command, args, {
32
+ timeout: 5_000,
33
+ stdio: 'pipe',
34
+ shell: process.platform === 'win32',
35
+ });
32
36
  }
33
37
  catch (error) {
34
38
  lastError = error;
package/dist/headless.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * 10 — blocked (command reported a blocker)
12
12
  * 11 — cancelled (SIGINT/SIGTERM received)
13
13
  */
14
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
14
+ import { existsSync, mkdirSync, writeFileSync, writeSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
16
  import { resolve } from 'node:path';
17
17
  import { RpcClient, SessionManager } from '@gsd/pi-coding-agent';
@@ -250,6 +250,30 @@ async function runHeadlessOnce(options, restartCount) {
250
250
  const result = await handleQuery(process.cwd());
251
251
  return { exitCode: result.exitCode, interrupted: false };
252
252
  }
253
+ // Doctor: read-only health check, no RPC child needed (#4904 live-regression).
254
+ // The interactive `/gsd doctor` command lives in the GSD extension; this CLI
255
+ // path lets non-interactive callers (CI, recovery scripts, the live-regression
256
+ // suite) get the same diagnostic without a TTY.
257
+ if (options.command === 'doctor') {
258
+ const wantsJson = options.json || options.commandArgs.includes('--json');
259
+ const { runGSDDoctor } = await import('./resources/extensions/gsd/doctor.js');
260
+ const { formatDoctorReport, formatDoctorReportJson } = await import('./resources/extensions/gsd/doctor-format.js');
261
+ let exitCode = 1;
262
+ try {
263
+ const report = await runGSDDoctor(process.cwd());
264
+ const out = wantsJson ? formatDoctorReportJson(report) : formatDoctorReport(report);
265
+ process.stdout.write(`${out}\n`);
266
+ exitCode = report.ok ? 0 : 1;
267
+ }
268
+ catch (err) {
269
+ const msg = err instanceof Error ? err.message : String(err);
270
+ process.stderr.write(`[headless] doctor failed: ${msg}\n`);
271
+ exitCode = 1;
272
+ }
273
+ // Bypass the auto-restart loop in runHeadless — doctor is a one-shot
274
+ // diagnostic; exit 1 means "issues detected", not "crashed".
275
+ process.exit(exitCode);
276
+ }
253
277
  // Resolve CLI path for the child process
254
278
  const cliPath = process.env.GSD_BIN_PATH || process.argv[1];
255
279
  if (!cliPath) {
@@ -616,7 +640,15 @@ async function runHeadlessOnce(options, restartCount) {
616
640
  });
617
641
  // Signal handling
618
642
  const signalHandler = () => {
619
- process.stderr.write('\n[headless] Interrupted, stopping child process...\n');
643
+ // Use writeSync on fd 2 to guarantee the Interrupted marker reaches
644
+ // consumers before process.exit() truncates pending async writes.
645
+ try {
646
+ writeSync(2, '\n[headless] Interrupted, stopping child process...\n');
647
+ }
648
+ catch {
649
+ // Fallback to async write if fd 2 is somehow unavailable.
650
+ process.stderr.write('\n[headless] Interrupted, stopping child process...\n');
651
+ }
620
652
  interrupted = true;
621
653
  exitCode = EXIT_CANCELLED;
622
654
  // Kill child process — don't await, just fire and exit.
@@ -635,8 +667,21 @@ async function runHeadlessOnce(options, restartCount) {
635
667
  }
636
668
  process.exit(exitCode);
637
669
  };
638
- process.on('SIGINT', signalHandler);
639
- process.on('SIGTERM', signalHandler);
670
+ // Use prependListener so our handler runs before pi-coding-agent's
671
+ // LSP-client module-load SIGINT handler, which calls process.exit(0)
672
+ // and would otherwise short-circuit our exit-code-11 contract.
673
+ process.prependListener('SIGINT', signalHandler);
674
+ process.prependListener('SIGTERM', signalHandler);
675
+ // Emit a deterministic readiness marker so test harnesses can wait for
676
+ // the SIGINT handler to be live before sending a signal. writeSync on
677
+ // fd 2 avoids any pipe-buffering race between the marker and subsequent
678
+ // signal delivery.
679
+ try {
680
+ writeSync(2, '[headless] signal-handlers-ready\n');
681
+ }
682
+ catch {
683
+ process.stderr.write('[headless] signal-handlers-ready\n');
684
+ }
640
685
  // Start the RPC session
641
686
  try {
642
687
  await client.start();
@@ -2,7 +2,47 @@ import { DefaultResourceLoader } from '@gsd/pi-coding-agent';
2
2
  export { discoverExtensionEntryPaths } from './extension-discovery.js';
3
3
  export declare function getExtensionKey(entryPath: string, extensionsDir: string): string;
4
4
  export declare function readManagedResourceVersion(agentDir: string): string | null;
5
+ /**
6
+ * Computes a content fingerprint of a resources directory (defaults to the
7
+ * bundled resourcesDir).
8
+ *
9
+ * Walks all files under `rootDir` and hashes `${relativePath}:${sha256(contents)}`
10
+ * for each one. Using the file *contents* — not size — is what distinguishes
11
+ * this from the earlier implementation and closes #4787: a same-size edit
12
+ * (e.g. swapping one word for another word of the same byte length) produces
13
+ * a different file hash, bumps the aggregate fingerprint, and therefore
14
+ * triggers a full resync in `initResources`. The old path+size approach
15
+ * silently cached stale prompts across upgrades.
16
+ *
17
+ * Cost is ~1-2ms for a typical resources tree (~100 small .md files) —
18
+ * still negligible at startup. Files are streamed via `readFileSync` but
19
+ * bundled prompts are tiny so this is fine.
20
+ *
21
+ * Exported for unit tests and for callers that want to check a different
22
+ * directory (e.g. pre-install verification).
23
+ */
24
+ export declare function computeResourceFingerprint(rootDir?: string): string;
5
25
  export declare function getNewerManagedResourceVersion(agentDir: string, currentVersion: string): string | null;
26
+ /**
27
+ * Syncs a single bundled resource directory into the agent directory.
28
+ *
29
+ * 1. Makes the destination writable (handles Nix store read-only copies).
30
+ * 2. Removes destination subdirs that exist in source to clear stale files,
31
+ * while preserving user-created directories.
32
+ * 3. Copies source into destination.
33
+ * 4. Makes the result writable for the next upgrade cycle.
34
+ */
35
+ export declare function syncResourceDir(srcDir: string, destDir: string): void;
36
+ /** Check if any @gsd* scopes exist in internal but not in hoisted node_modules */
37
+ export declare function hasMissingWorkspaceScopes(hoisted: string, internal: string): boolean;
38
+ /**
39
+ * Create a real node_modules directory containing symlinks from both the
40
+ * hoisted root (external deps) and internal root (@gsd/* workspace packages).
41
+ * Used for pnpm global installs where @gsd/* isn't hoisted.
42
+ */
43
+ export declare function reconcileMergedNodeModules(agentNodeModules: string, hoisted: string, internal: string): void;
44
+ /** Build a cache fingerprint from packageRoot + sorted entry names of both directories */
45
+ export declare function mergedFingerprint(hoisted: string, internal: string): string;
6
46
  /**
7
47
  * Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
8
48
  *
@@ -101,17 +101,27 @@ function readManagedResourceManifest(agentDir) {
101
101
  }
102
102
  }
103
103
  /**
104
- * Computes a lightweight content fingerprint of the bundled resources directory.
104
+ * Computes a content fingerprint of a resources directory (defaults to the
105
+ * bundled resourcesDir).
105
106
  *
106
- * Walks all files under resourcesDir and hashes their relative paths + sizes.
107
- * This catches same-version content changes (npm link dev workflow, hotfixes
108
- * within a release) without the cost of reading every file's contents.
107
+ * Walks all files under `rootDir` and hashes `${relativePath}:${sha256(contents)}`
108
+ * for each one. Using the file *contents* not size — is what distinguishes
109
+ * this from the earlier implementation and closes #4787: a same-size edit
110
+ * (e.g. swapping one word for another word of the same byte length) produces
111
+ * a different file hash, bumps the aggregate fingerprint, and therefore
112
+ * triggers a full resync in `initResources`. The old path+size approach
113
+ * silently cached stale prompts across upgrades.
109
114
  *
110
- * ~1ms for a typical resources tree (~100 files) — just stat calls, no reads.
115
+ * Cost is ~1-2ms for a typical resources tree (~100 small .md files) —
116
+ * still negligible at startup. Files are streamed via `readFileSync` but
117
+ * bundled prompts are tiny so this is fine.
118
+ *
119
+ * Exported for unit tests and for callers that want to check a different
120
+ * directory (e.g. pre-install verification).
111
121
  */
112
- function computeResourceFingerprint() {
122
+ export function computeResourceFingerprint(rootDir = resourcesDir) {
113
123
  const entries = [];
114
- collectFileEntries(resourcesDir, resourcesDir, entries);
124
+ collectFileEntries(rootDir, rootDir, entries);
115
125
  entries.sort();
116
126
  return createHash('sha256').update(entries.join('\n')).digest('hex').slice(0, 16);
117
127
  }
@@ -125,8 +135,17 @@ function collectFileEntries(dir, root, out) {
125
135
  }
126
136
  else {
127
137
  const rel = relative(root, fullPath);
128
- const size = statSync(fullPath).size;
129
- out.push(`${rel}:${size}`);
138
+ // Hash the file contents — see function doc for #4787 rationale.
139
+ let contentHash;
140
+ try {
141
+ contentHash = createHash('sha256').update(readFileSync(fullPath)).digest('hex');
142
+ }
143
+ catch {
144
+ // Unreadable file — fall back to a stable marker so the entry still
145
+ // contributes to the aggregate hash and future reads will re-hash.
146
+ contentHash = 'unreadable';
147
+ }
148
+ out.push(`${rel}:${contentHash}`);
130
149
  }
131
150
  }
132
151
  }
@@ -188,7 +207,7 @@ function makeTreeWritable(dirPath) {
188
207
  * 3. Copies source into destination.
189
208
  * 4. Makes the result writable for the next upgrade cycle.
190
209
  */
191
- function syncResourceDir(srcDir, destDir) {
210
+ export function syncResourceDir(srcDir, destDir) {
192
211
  makeTreeWritable(destDir);
193
212
  if (existsSync(srcDir)) {
194
213
  pruneStaleSiblingFiles(srcDir, destDir);
@@ -282,7 +301,7 @@ function ensureNodeModulesSymlink(agentDir) {
282
301
  reconcileMergedNodeModules(agentNodeModules, hoistedNodeModules, internalNodeModules);
283
302
  }
284
303
  /** Check if any @gsd* scopes exist in internal but not in hoisted node_modules */
285
- function hasMissingWorkspaceScopes(hoisted, internal) {
304
+ export function hasMissingWorkspaceScopes(hoisted, internal) {
286
305
  if (!existsSync(internal))
287
306
  return false;
288
307
  try {
@@ -326,7 +345,7 @@ function reconcileSymlink(link, target) {
326
345
  * hoisted root (external deps) and internal root (@gsd/* workspace packages).
327
346
  * Used for pnpm global installs where @gsd/* isn't hoisted.
328
347
  */
329
- function reconcileMergedNodeModules(agentNodeModules, hoisted, internal) {
348
+ export function reconcileMergedNodeModules(agentNodeModules, hoisted, internal) {
330
349
  // Fast path: if already merged for this packageRoot + same directory contents, skip.
331
350
  // The fingerprint includes entry names from both roots so `pnpm add/remove` triggers rebuild.
332
351
  const marker = join(agentNodeModules, '.gsd-merged');
@@ -400,7 +419,7 @@ function reconcileMergedNodeModules(agentNodeModules, hoisted, internal) {
400
419
  }
401
420
  }
402
421
  /** Build a cache fingerprint from packageRoot + sorted entry names of both directories */
403
- function mergedFingerprint(hoisted, internal) {
422
+ export function mergedFingerprint(hoisted, internal) {
404
423
  try {
405
424
  const h = readdirSync(hoisted).sort().join(',');
406
425
  const i = readdirSync(internal).sort().join(',');
@@ -21,6 +21,15 @@ async function getSharp() {
21
21
  }
22
22
  return _sharp;
23
23
  }
24
+ /**
25
+ * Test-only seam: override the cached sharp module. Pass `null` to simulate
26
+ * an environment where the sharp native dep is unavailable; pass `undefined`
27
+ * to clear the cache and let the next getSharp() call re-import. See
28
+ * tests/capture-sharp-optional.test.cjs.
29
+ */
30
+ export function __setSharpForTesting(value) {
31
+ _sharp = value;
32
+ }
24
33
  import { formatCompactStateSummary } from "./utils.js";
25
34
  // Anthropic vision: 1568px is the recommended optimal width. Height is capped
26
35
  // generously at 8000px so tall full-page screenshots remain readable rather
@@ -12,76 +12,25 @@
12
12
  import { describe, it, before, after } from "node:test";
13
13
  import assert from "node:assert/strict";
14
14
  import { chromium } from "playwright";
15
- import { readFileSync } from "node:fs";
16
- import { resolve, dirname } from "node:path";
15
+ import { dirname } from "node:path";
17
16
  import { fileURLToPath } from "node:url";
18
17
 
19
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
- const ROOT = resolve(__dirname, "..");
21
19
 
22
20
  // ---------------------------------------------------------------------------
23
- // Source extractionget the IIFE strings we need for injection
21
+ // Source loadingimport the IIFE builders directly via jiti.
22
+ // The test-only named exports in tools/intent.ts and tools/forms.ts exist
23
+ // exactly so this test can call the real, in-tree builders. No brace
24
+ // walking, no regex stripping — a refactor of the signatures just updates
25
+ // the import surface, not the test.
24
26
  // ---------------------------------------------------------------------------
25
27
 
26
- // 1. EVALUATE_HELPERS_SOURCE — exported constant, extract via jiti
27
28
  import { createRequire } from "node:module";
28
29
  const require = createRequire(import.meta.url);
29
30
  const jiti = require("jiti")(__dirname, { interopDefault: true, debug: false });
30
31
  const { EVALUATE_HELPERS_SOURCE } = jiti("../evaluate-helpers.ts");
31
-
32
- // 2. Intent scoring module-private buildIntentScoringScript.
33
- // Extract the function from source, wrap it, and eval to get the builder.
34
- const intentSource = readFileSync(resolve(ROOT, "tools/intent.ts"), "utf-8");
35
-
36
- function extractBuildIntentScoringScript() {
37
- // Match the function body: starts with "function buildIntentScoringScript"
38
- // and returns a template literal string. We extract up to the matching closing brace.
39
- const startMarker = "function buildIntentScoringScript(intent: string, scope?: string): string {";
40
- const startIdx = intentSource.indexOf(startMarker);
41
- if (startIdx === -1) throw new Error("Could not find buildIntentScoringScript in intent.ts");
42
-
43
- // Walk from start, counting braces to find the end
44
- let depth = 0;
45
- let foundFirst = false;
46
- let endIdx = startIdx;
47
- for (let i = startIdx; i < intentSource.length; i++) {
48
- if (intentSource[i] === "{") { depth++; foundFirst = true; }
49
- if (intentSource[i] === "}") depth--;
50
- if (foundFirst && depth === 0) { endIdx = i + 1; break; }
51
- }
52
-
53
- let fnBody = intentSource.slice(startIdx, endIdx);
54
- // Strip TypeScript type annotations
55
- fnBody = fnBody.replace(/\(intent:\s*string,\s*scope\?:\s*string\):\s*string/, "(intent, scope)");
56
- return new Function("return " + fnBody)();
57
- }
58
-
59
- const buildIntentScoringScript = extractBuildIntentScoringScript();
60
-
61
- // 3. Form analysis — module-private buildFormAnalysisScript.
62
- const formsSource = readFileSync(resolve(ROOT, "tools/forms.ts"), "utf-8");
63
-
64
- function extractBuildFormAnalysisScript() {
65
- const startMarker = "function buildFormAnalysisScript(selector?: string): string {";
66
- const startIdx = formsSource.indexOf(startMarker);
67
- if (startIdx === -1) throw new Error("Could not find buildFormAnalysisScript in forms.ts");
68
-
69
- let depth = 0;
70
- let foundFirst = false;
71
- let endIdx = startIdx;
72
- for (let i = startIdx; i < formsSource.length; i++) {
73
- if (formsSource[i] === "{") { depth++; foundFirst = true; }
74
- if (formsSource[i] === "}") depth--;
75
- if (foundFirst && depth === 0) { endIdx = i + 1; break; }
76
- }
77
-
78
- let fnBody = formsSource.slice(startIdx, endIdx);
79
- // Strip TypeScript type annotation
80
- fnBody = fnBody.replace(/\(selector\?:\s*string\):\s*string/, "(selector)");
81
- return new Function("return " + fnBody)();
82
- }
83
-
84
- const buildFormAnalysisScript = extractBuildFormAnalysisScript();
32
+ const { buildIntentScoringScript } = jiti("../tools/intent.ts");
33
+ const { buildFormAnalysisScript } = jiti("../tools/forms.ts");
85
34
 
86
35
  // ---------------------------------------------------------------------------
87
36
  // Browser lifecycle
@@ -387,32 +387,44 @@ describe("formatArtifactTimestamp", () => {
387
387
  // ---------------------------------------------------------------------------
388
388
 
389
389
  describe("EVALUATE_HELPERS_SOURCE", () => {
390
- it("is a parseable string (valid JavaScript)", () => {
391
- assert.doesNotThrow(() => {
392
- new Function(EVALUATE_HELPERS_SOURCE);
393
- });
394
- });
390
+ // Behaviour test: executing the source in a Node vm sandbox must
391
+ // populate a `window.__pi` namespace with every expected helper.
392
+ // No source grep — we actually run the code and verify the resulting
393
+ // object shape.
394
+ it("executing the source assigns all expected helpers to window.__pi", () => {
395
+ const vm = require("node:vm");
396
+ const expectedFunctions = [
397
+ "cssPath",
398
+ "simpleHash",
399
+ "isVisible",
400
+ "isEnabled",
401
+ "inferRole",
402
+ "accessibleName",
403
+ "isInteractiveEl",
404
+ "domPath",
405
+ "selectorHints",
406
+ ];
395
407
 
396
- const expectedFunctions = [
397
- "cssPath",
398
- "simpleHash",
399
- "isVisible",
400
- "isEnabled",
401
- "inferRole",
402
- "accessibleName",
403
- "isInteractiveEl",
404
- "domPath",
405
- "selectorHints",
406
- ];
407
-
408
- for (const fnName of expectedFunctions) {
409
- it(`contains assignment for pi.${fnName}`, () => {
410
- assert.ok(
411
- EVALUATE_HELPERS_SOURCE.includes(`pi.${fnName} = function`),
412
- `Expected pi.${fnName} = function assignment in source`,
408
+ // Playwright evaluates the source in a page context where `window`
409
+ // exists, so the helpers attach to `window.__pi`. Provide a minimal
410
+ // window stub in a vm context so we avoid polluting the test globals.
411
+ const sandbox = { window: {} };
412
+ const script = new vm.Script(EVALUATE_HELPERS_SOURCE);
413
+ script.runInNewContext(sandbox, { timeout: 1000 });
414
+
415
+ assert.ok(
416
+ sandbox.window.__pi && typeof sandbox.window.__pi === "object",
417
+ "executing EVALUATE_HELPERS_SOURCE must assign window.__pi",
418
+ );
419
+
420
+ for (const fnName of expectedFunctions) {
421
+ assert.equal(
422
+ typeof sandbox.window.__pi[fnName],
423
+ "function",
424
+ `window.__pi.${fnName} must be a function after executing the source`,
413
425
  );
414
- });
415
- }
426
+ }
427
+ });
416
428
  });
417
429
 
418
430
  // ---------------------------------------------------------------------------
@@ -1,93 +1,91 @@
1
1
  /**
2
2
  * Regression tests for the optional sharp dependency in capture.ts.
3
3
  *
4
- * Verifies two things:
5
- * 1. Static: the lazy-load pattern is structurally correct in the source.
6
- * 2. Behavioral: constrainScreenshot returns the raw buffer unchanged when
7
- * sharp is unavailable, rather than throwing.
4
+ * Behaviour:
5
+ * - constrainScreenshot must fall back to returning the raw buffer
6
+ * unchanged when sharp is unavailable, rather than throwing.
7
+ * - When sharp IS available, oversized screenshots get resized.
8
+ *
9
+ * No source-grep. The test drives the real constrainScreenshot function
10
+ * after seeding the module-private `_sharp` cache via the test-only
11
+ * `__setSharpForTesting` export.
8
12
  */
9
13
 
10
- const { describe, it } = require("node:test");
14
+ const { describe, it, afterEach } = require("node:test");
11
15
  const assert = require("node:assert/strict");
12
- const { readFileSync } = require("node:fs");
13
- const { join } = require("node:path");
14
-
15
- // ---------------------------------------------------------------------------
16
- // 1. Static analysis — verify the lazy-load pattern is present in source
17
- // ---------------------------------------------------------------------------
16
+ const jiti = require("jiti")(__filename, { interopDefault: true, debug: false });
18
17
 
19
- describe("capture.ts sharp optional lazy-load (static)", () => {
20
- const source = readFileSync(
21
- join(process.cwd(), "src/resources/extensions/browser-tools/capture.ts"),
22
- "utf-8",
23
- );
18
+ const { constrainScreenshot, __setSharpForTesting } = jiti("../capture.ts");
24
19
 
25
- it("does not have a top-level static sharp import", () => {
26
- assert.ok(
27
- !source.includes('import sharp from "sharp"'),
28
- 'capture.ts must not contain a top-level `import sharp from "sharp"` — sharp must be loaded lazily',
29
- );
20
+ describe("constrainScreenshot sharp unavailable (null)", () => {
21
+ afterEach(() => {
22
+ // Clear the test override so later tests don't inherit a null sharp.
23
+ __setSharpForTesting(undefined);
30
24
  });
31
25
 
32
- it("defines a getSharp lazy-loader function", () => {
33
- assert.ok(
34
- source.includes("async function getSharp()"),
35
- "capture.ts must define an async getSharp() lazy-loader",
26
+ it("returns the raw buffer unchanged when sharp is null", async () => {
27
+ __setSharpForTesting(null);
28
+
29
+ const rawBuffer = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); // PNG magic bytes
30
+ const result = await constrainScreenshot(null, rawBuffer, "image/png", 80);
31
+
32
+ assert.strictEqual(
33
+ result,
34
+ rawBuffer,
35
+ "constrainScreenshot must return the exact same buffer instance when sharp is null",
36
36
  );
37
37
  });
38
38
 
39
- it("guards constrainScreenshot with a null-sharp early return", () => {
40
- assert.ok(
41
- source.includes("if (!sharp) return buffer"),
42
- "constrainScreenshot must return the raw buffer early when sharp is null",
43
- );
39
+ it("returns the raw buffer unchanged for JPEG input when sharp is null", async () => {
40
+ __setSharpForTesting(null);
41
+
42
+ const rawBuffer = Buffer.from([0xff, 0xd8, 0xff, 0xe0]); // JPEG magic bytes
43
+ const result = await constrainScreenshot(null, rawBuffer, "image/jpeg", 80);
44
+
45
+ assert.strictEqual(result, rawBuffer);
44
46
  });
45
47
  });
46
48
 
47
- // ---------------------------------------------------------------------------
48
- // 2. Behavioral — constrainScreenshot passes through buffer when sharp is null
49
- // ---------------------------------------------------------------------------
50
-
51
- describe("capture.ts — constrainScreenshot with sharp unavailable", () => {
52
- it("returns the raw buffer unchanged when sharp is null", async () => {
53
- // Simulate what getSharp() returns on platforms without sharp by
54
- // directly calling constrainScreenshot through a module whose _sharp
55
- // cache has been pre-seeded to null via the module-level variable reset.
56
- //
57
- // Because jiti caches modules across the test suite we use a fresh
58
- // require-cache trick: load capture.ts source manually and evaluate the
59
- // constrainScreenshot function with a stub getSharp that always returns null.
60
- const captureSource = readFileSync(
61
- join(process.cwd(), "src/resources/extensions/browser-tools/capture.ts"),
62
- "utf-8",
63
- );
49
+ describe("constrainScreenshot — sharp available", () => {
50
+ afterEach(() => {
51
+ __setSharpForTesting(undefined);
52
+ });
64
53
 
65
- // Verify the guard line is reachable (structural check already done above).
66
- // For the behavioral test we use the actual constrainScreenshot imported
67
- // via jiti but we force getSharp() to return null by calling the function
68
- // with a very small buffer where sharp IS available. Separately we test the
69
- // null path by crafting a minimal wrapper.
70
- //
71
- // The simplest verifiable behaviour: if the guard `if (!sharp) return buffer`
72
- // is present, passing a Buffer through a version of constrainScreenshot where
73
- // _sharp=null must return that exact buffer. We verify this by extracting and
74
- // running a minimal inline version of the guard logic.
54
+ it("passes through a small image unchanged (below cap)", async () => {
55
+ const sharp = require("sharp");
56
+ const small = await sharp({
57
+ create: {
58
+ width: 400,
59
+ height: 300,
60
+ channels: 3,
61
+ background: { r: 128, g: 128, b: 128 },
62
+ },
63
+ })
64
+ .jpeg({ quality: 80 })
65
+ .toBuffer();
75
66
 
76
- const rawBuffer = Buffer.from([0x89, 0x50, 0x4e, 0x47]); // fake PNG header
67
+ const result = await constrainScreenshot(null, small, "image/jpeg", 80);
68
+ const meta = await sharp(result).metadata();
69
+ assert.equal(meta.width, 400, "small images must not be resized");
70
+ assert.equal(meta.height, 300);
71
+ });
77
72
 
78
- // Inline the guard as it appears in capture.ts so the test is coupled to
79
- // the actual contract, not an arbitrary helper.
80
- async function constrainScreenshotWithNullSharp(buffer) {
81
- const sharp = null; // simulates getSharp() returning null
82
- if (!sharp) return buffer;
83
- // (remainder of constrainScreenshot would run here with a real sharp)
84
- }
73
+ it("resizes an oversized image to within 1568px", async () => {
74
+ const sharp = require("sharp");
75
+ const big = await sharp({
76
+ create: {
77
+ width: 3000,
78
+ height: 2000,
79
+ channels: 3,
80
+ background: { r: 128, g: 128, b: 128 },
81
+ },
82
+ })
83
+ .jpeg({ quality: 80 })
84
+ .toBuffer();
85
85
 
86
- const result = await constrainScreenshotWithNullSharp(rawBuffer);
87
- assert.strictEqual(
88
- result,
89
- rawBuffer,
90
- "constrainScreenshot must return the exact same buffer instance when sharp is null",
91
- );
86
+ const result = await constrainScreenshot(null, big, "image/jpeg", 80);
87
+ const meta = await sharp(result).metadata();
88
+ assert.ok(meta.width <= 1568, `width ${meta.width} must be <= 1568`);
89
+ assert.ok(meta.height <= 1568, `height ${meta.height} must be <= 1568`);
92
90
  });
93
91
  });
@@ -4,7 +4,11 @@ import { setLastActionBeforeState, setLastActionAfterState, } from "../state.js"
4
4
  * Runs inside page.evaluate(). Finds the target form, inventories all fields
5
5
  * with full label resolution, and returns a structured result.
6
6
  */
7
- function buildFormAnalysisScript(selector) {
7
+ // Exported for tests only (see tests/browser-tools-integration.test.mjs).
8
+ // Keep this function treated as module-private for production call sites —
9
+ // the only legitimate external caller is the Playwright-driven integration
10
+ // suite that needs to evaluate the returned IIFE against real DOM.
11
+ export function buildFormAnalysisScript(selector) {
8
12
  // We return a string that will be evaluated in the page context.
9
13
  // This avoids serialization issues with passing functions.
10
14
  return `(() => {
@@ -26,7 +26,11 @@ const INTENTS = [
26
26
  * Uses window.__pi utilities (injected via addInitScript) for element
27
27
  * metadata — no inline redeclarations.
28
28
  */
29
- function buildIntentScoringScript(intent, scope) {
29
+ // Exported for tests only (see tests/browser-tools-integration.test.mjs).
30
+ // Keep this function treated as module-private for production call sites —
31
+ // the only legitimate external caller is the Playwright-driven integration
32
+ // suite that needs to evaluate the returned IIFE against real DOM.
33
+ export function buildIntentScoringScript(intent, scope) {
30
34
  const scopeSelector = JSON.stringify(scope ?? null);
31
35
  return `(() => {
32
36
  var pi = window.__pi;
@@ -27,7 +27,11 @@ function execClaude(args) {
27
27
  let lastError;
28
28
  for (const command of CLAUDE_COMMAND_CANDIDATES) {
29
29
  try {
30
- return execFileSync(command, args, { timeout: 5_000, stdio: "pipe" });
30
+ return execFileSync(command, args, {
31
+ timeout: 5_000,
32
+ stdio: "pipe",
33
+ shell: process.platform === "win32",
34
+ });
31
35
  }
32
36
  catch (error) {
33
37
  lastError = error;