gsd-pi 2.76.0-dev.4100bd590 → 2.76.0-dev.479ad0e78

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 (362) hide show
  1. package/dist/claude-cli-check.js +32 -3
  2. package/dist/mcp-server.d.ts +7 -0
  3. package/dist/mcp-server.js +35 -1
  4. package/dist/onboarding.js +45 -0
  5. package/dist/resource-loader.d.ts +1 -1
  6. package/dist/resource-loader.js +2 -8
  7. package/dist/resources/extensions/claude-code-cli/readiness.js +4 -3
  8. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
  9. package/dist/resources/extensions/gsd/auto/loop.js +9 -0
  10. package/dist/resources/extensions/gsd/auto/phases.js +58 -5
  11. package/dist/resources/extensions/gsd/auto/run-unit.js +38 -2
  12. package/dist/resources/extensions/gsd/auto/session.js +22 -1
  13. package/dist/resources/extensions/gsd/auto-dispatch.js +16 -3
  14. package/dist/resources/extensions/gsd/auto-model-selection.js +14 -3
  15. package/dist/resources/extensions/gsd/auto-post-unit.js +25 -2
  16. package/dist/resources/extensions/gsd/auto-prompts.js +14 -0
  17. package/dist/resources/extensions/gsd/auto-recovery.js +32 -1
  18. package/dist/resources/extensions/gsd/auto-start.js +58 -57
  19. package/dist/resources/extensions/gsd/auto-worktree.js +51 -53
  20. package/dist/resources/extensions/gsd/auto.js +70 -28
  21. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
  22. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  23. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  24. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  25. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +52 -6
  26. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +34 -2
  27. package/dist/resources/extensions/gsd/clean-root-preflight.js +93 -0
  28. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
  29. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  30. package/dist/resources/extensions/gsd/complexity-classifier.js +5 -3
  31. package/dist/resources/extensions/gsd/error-classifier.js +10 -3
  32. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  33. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  34. package/dist/resources/extensions/gsd/gitignore.js +1 -0
  35. package/dist/resources/extensions/gsd/gsd-db.js +149 -31
  36. package/dist/resources/extensions/gsd/guided-flow.js +190 -1
  37. package/dist/resources/extensions/gsd/health-widget.js +4 -1
  38. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  39. package/dist/resources/extensions/gsd/key-manager.js +28 -0
  40. package/dist/resources/extensions/gsd/model-router.js +36 -3
  41. package/dist/resources/extensions/gsd/pre-execution-checks.js +44 -9
  42. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  43. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  44. package/dist/resources/extensions/gsd/preferences.js +17 -17
  45. package/dist/resources/extensions/gsd/prompt-loader.js +22 -7
  46. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  47. package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
  48. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  49. package/dist/resources/extensions/gsd/safety/evidence-collector.js +96 -0
  50. package/dist/resources/extensions/gsd/safety/file-change-validator.js +13 -5
  51. package/dist/resources/extensions/gsd/safety/safety-harness.js +5 -1
  52. package/dist/resources/extensions/gsd/token-counter.js +22 -5
  53. package/dist/resources/extensions/gsd/tools/complete-milestone.js +16 -10
  54. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  55. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  56. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  57. package/dist/resources/extensions/gsd/uok/plan-v2.js +20 -3
  58. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  59. package/dist/resources/extensions/gsd/worktree-resolver.js +50 -10
  60. package/dist/resources/skills/verify-before-complete/SKILL.md +2 -1
  61. package/dist/resources/skills/write-docs/SKILL.md +2 -1
  62. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  63. package/dist/web/standalone/.next/BUILD_ID +1 -1
  64. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  65. package/dist/web/standalone/.next/build-manifest.json +2 -2
  66. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  67. package/dist/web/standalone/.next/required-server-files.json +1 -1
  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/index.html +1 -1
  85. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  92. package/dist/web/standalone/.next/server/chunks/6897.js +2 -2
  93. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  95. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  96. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  97. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  98. package/dist/web/standalone/server.js +1 -1
  99. package/dist/welcome-screen.js +6 -1
  100. package/dist/wizard.js +2 -0
  101. package/package.json +1 -1
  102. package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
  103. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
  104. package/packages/mcp-server/dist/remote-questions.js +732 -0
  105. package/packages/mcp-server/dist/remote-questions.js.map +1 -0
  106. package/packages/mcp-server/dist/server.d.ts +7 -0
  107. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  108. package/packages/mcp-server/dist/server.js +70 -8
  109. package/packages/mcp-server/dist/server.js.map +1 -1
  110. package/packages/mcp-server/dist/session-manager.d.ts +14 -0
  111. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
  112. package/packages/mcp-server/dist/session-manager.js +49 -1
  113. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  114. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  115. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  116. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  117. package/packages/mcp-server/package.json +2 -1
  118. package/packages/mcp-server/src/mcp-server.test.ts +67 -0
  119. package/packages/mcp-server/src/remote-questions.test.ts +294 -0
  120. package/packages/mcp-server/src/remote-questions.ts +916 -0
  121. package/packages/mcp-server/src/server.ts +89 -14
  122. package/packages/mcp-server/src/session-manager.ts +43 -1
  123. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  124. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  125. package/packages/mcp-server/tsconfig.test.json +19 -0
  126. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  127. package/packages/pi-ai/dist/models/custom.d.ts +38 -0
  128. package/packages/pi-ai/dist/models/custom.d.ts.map +1 -1
  129. package/packages/pi-ai/dist/models/custom.js +41 -0
  130. package/packages/pi-ai/dist/models/custom.js.map +1 -1
  131. package/packages/pi-ai/dist/providers/anthropic-auth.test.js +1 -1
  132. package/packages/pi-ai/dist/providers/anthropic-auth.test.js.map +1 -1
  133. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  134. package/packages/pi-ai/dist/providers/anthropic-shared.js +27 -4
  135. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  136. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  137. package/packages/pi-ai/dist/providers/anthropic.js +8 -3
  138. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  139. package/packages/pi-ai/dist/providers/minimax-tool-name.test.d.ts +2 -0
  140. package/packages/pi-ai/dist/providers/minimax-tool-name.test.d.ts.map +1 -0
  141. package/packages/pi-ai/dist/providers/minimax-tool-name.test.js +80 -0
  142. package/packages/pi-ai/dist/providers/minimax-tool-name.test.js.map +1 -0
  143. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  144. package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
  145. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  146. package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
  147. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  148. package/packages/pi-ai/dist/providers/simple-options.js +16 -1
  149. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  150. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
  151. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
  152. package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
  153. package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
  154. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
  155. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
  156. package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
  157. package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
  158. package/packages/pi-ai/src/models/custom.ts +42 -0
  159. package/packages/pi-ai/src/providers/anthropic-auth.test.ts +1 -1
  160. package/packages/pi-ai/src/providers/anthropic-shared.ts +26 -5
  161. package/packages/pi-ai/src/providers/anthropic.ts +9 -3
  162. package/packages/pi-ai/src/providers/minimax-tool-name.test.ts +98 -0
  163. package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
  164. package/packages/pi-ai/src/providers/simple-options.ts +17 -1
  165. package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
  166. package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
  167. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  168. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +3 -2
  169. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  170. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +2 -0
  171. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  172. package/packages/pi-coding-agent/dist/core/agent-session.js +7 -0
  173. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
  175. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  176. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  177. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  178. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  179. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  180. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
  181. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
  182. package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
  183. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
  184. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
  185. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
  186. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
  187. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
  188. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
  189. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
  190. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
  191. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
  192. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -0
  193. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  194. package/packages/pi-coding-agent/dist/core/model-registry.js +90 -10
  195. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  196. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  197. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  198. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  199. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  200. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  201. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  202. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  203. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  204. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  205. package/packages/pi-coding-agent/dist/core/session-manager.js +10 -6
  206. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  207. package/packages/pi-coding-agent/dist/core/session-manager.test.js +45 -1
  208. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  209. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  210. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  211. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  212. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  213. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  214. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
  215. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  216. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  217. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  218. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  219. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  220. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  221. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
  222. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  223. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +3 -2
  224. package/packages/pi-coding-agent/src/core/agent-session.ts +11 -0
  225. package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -0
  226. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  227. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +19 -0
  228. package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
  229. package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
  230. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
  231. package/packages/pi-coding-agent/src/core/model-registry.ts +102 -10
  232. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  233. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  234. package/packages/pi-coding-agent/src/core/session-manager.test.ts +65 -1
  235. package/packages/pi-coding-agent/src/core/session-manager.ts +10 -6
  236. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  237. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -7
  238. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  239. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
  240. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  241. package/scripts/link-workspace-packages.cjs +1 -0
  242. package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
  243. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
  244. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
  245. package/src/resources/extensions/gsd/auto/loop-deps.ts +14 -0
  246. package/src/resources/extensions/gsd/auto/loop.ts +9 -0
  247. package/src/resources/extensions/gsd/auto/phases.ts +82 -4
  248. package/src/resources/extensions/gsd/auto/run-unit.ts +40 -2
  249. package/src/resources/extensions/gsd/auto/session.ts +35 -2
  250. package/src/resources/extensions/gsd/auto-dispatch.ts +16 -3
  251. package/src/resources/extensions/gsd/auto-model-selection.ts +17 -2
  252. package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
  253. package/src/resources/extensions/gsd/auto-prompts.ts +28 -1
  254. package/src/resources/extensions/gsd/auto-recovery.ts +26 -1
  255. package/src/resources/extensions/gsd/auto-start.ts +60 -68
  256. package/src/resources/extensions/gsd/auto-worktree.ts +62 -63
  257. package/src/resources/extensions/gsd/auto.ts +73 -28
  258. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
  259. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  260. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  261. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  262. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +54 -6
  263. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +35 -2
  264. package/src/resources/extensions/gsd/clean-root-preflight.ts +111 -0
  265. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
  266. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  267. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -3
  268. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  269. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  270. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  271. package/src/resources/extensions/gsd/gitignore.ts +1 -1
  272. package/src/resources/extensions/gsd/gsd-db.ts +157 -33
  273. package/src/resources/extensions/gsd/guided-flow.ts +222 -1
  274. package/src/resources/extensions/gsd/health-widget.ts +3 -1
  275. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  276. package/src/resources/extensions/gsd/journal.ts +2 -1
  277. package/src/resources/extensions/gsd/key-manager.ts +28 -0
  278. package/src/resources/extensions/gsd/model-router.ts +42 -1
  279. package/src/resources/extensions/gsd/pre-execution-checks.ts +46 -10
  280. package/src/resources/extensions/gsd/preferences-types.ts +46 -0
  281. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  282. package/src/resources/extensions/gsd/preferences.ts +17 -17
  283. package/src/resources/extensions/gsd/prompt-loader.ts +30 -7
  284. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  285. package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
  286. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  287. package/src/resources/extensions/gsd/safety/evidence-collector.ts +119 -0
  288. package/src/resources/extensions/gsd/safety/file-change-validator.ts +17 -4
  289. package/src/resources/extensions/gsd/safety/safety-harness.ts +9 -0
  290. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +188 -2
  291. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  292. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +12 -0
  293. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +49 -0
  294. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +141 -0
  295. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
  296. package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
  297. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +23 -0
  298. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +186 -0
  299. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  300. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +61 -1
  301. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  302. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  303. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
  304. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
  305. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
  306. package/src/resources/extensions/gsd/tests/double-merge-guard.test.ts +1 -1
  307. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +1 -1
  308. package/src/resources/extensions/gsd/tests/escalation.test.ts +1 -1
  309. package/src/resources/extensions/gsd/tests/exec-history.test.ts +237 -0
  310. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  311. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +58 -0
  312. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +447 -1
  313. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  314. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +1 -0
  315. package/src/resources/extensions/gsd/tests/integration/gitignore-tracked-gsd.test.ts +1 -0
  316. package/src/resources/extensions/gsd/tests/integration/idle-recovery.test.ts +30 -0
  317. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  318. package/src/resources/extensions/gsd/tests/issue-4540-regressions.test.ts +288 -0
  319. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +2 -0
  320. package/src/resources/extensions/gsd/tests/key-manager.test.ts +9 -0
  321. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  322. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +12 -0
  323. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  324. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +19 -0
  325. package/src/resources/extensions/gsd/tests/plan-gate-failed-doctor-heal-hint.test.ts +37 -0
  326. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  327. package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +272 -0
  328. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +356 -0
  329. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  330. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
  331. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +49 -0
  332. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  333. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
  334. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
  335. package/src/resources/extensions/gsd/tests/resume-dispatch-worktree.test.ts +230 -0
  336. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +205 -0
  337. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  338. package/src/resources/extensions/gsd/tests/schema-v21-sequence.test.ts +413 -0
  339. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
  340. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +56 -0
  341. package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
  342. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
  343. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +23 -0
  344. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
  345. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +35 -0
  346. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +6 -1
  347. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +78 -5
  348. package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
  349. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  350. package/src/resources/extensions/gsd/token-counter.ts +22 -5
  351. package/src/resources/extensions/gsd/tools/complete-milestone.ts +15 -9
  352. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  353. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  354. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  355. package/src/resources/extensions/gsd/uok/plan-v2.ts +26 -3
  356. package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
  357. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  358. package/src/resources/extensions/gsd/worktree-resolver.ts +54 -9
  359. package/src/resources/skills/verify-before-complete/SKILL.md +2 -1
  360. package/src/resources/skills/write-docs/SKILL.md +2 -1
  361. /package/dist/web/standalone/.next/static/{YnUwu2WWaT0_hyTLUF4nq → JgU2F-5N9mTyB7kUSSk9A}/_buildManifest.js +0 -0
  362. /package/dist/web/standalone/.next/static/{YnUwu2WWaT0_hyTLUF4nq → JgU2F-5N9mTyB7kUSSk9A}/_ssgManifest.js +0 -0
@@ -109,9 +109,9 @@ console.log('\n=== complete-task: schema v5 migration ===');
109
109
 
110
110
  const adapter = _getAdapter()!;
111
111
 
112
- // Verify schema version is current (v21ADR-013 structured_fields column)
112
+ // Verify schema version is current (v22quality_gates DDL fix)
113
113
  const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
114
- assertEq(versionRow?.['v'], 21, 'schema version should be 21');
114
+ assertEq(versionRow?.['v'], 22, 'schema version should be 22');
115
115
 
116
116
  // Verify all 4 new tables exist
117
117
  const tables = adapter.prepare(
@@ -21,9 +21,9 @@ test("tierOrdinal returns correct ordering", () => {
21
21
 
22
22
  // ─── Unit Type Classification ────────────────────────────────────────────────
23
23
 
24
- test("complete-slice classifies as light", () => {
24
+ test("complete-slice classifies as standard", () => {
25
25
  const result = classifyUnitComplexity("complete-slice", "M001/S01", "/tmp/fake");
26
- assert.equal(result.tier, "light");
26
+ assert.equal(result.tier, "standard");
27
27
  });
28
28
 
29
29
  test("run-uat classifies as light", () => {
@@ -145,7 +145,7 @@ test("budget pressure at 90% downgrades standard to light", () => {
145
145
  assert.equal(result.downgraded, true);
146
146
  });
147
147
 
148
- test("budget pressure at 90% downgrades light stays light", () => {
148
+ test("budget pressure at 90% downgrades complete-slice standard to light", () => {
149
149
  const result = classifyUnitComplexity("complete-slice", "M001/S01", "/tmp/fake", 0.95);
150
150
  assert.equal(result.tier, "light");
151
151
  });
@@ -179,6 +179,8 @@ function makeMockDeps(overrides?: Partial<LoopDeps>): LoopDeps & { callLog: stri
179
179
  autoWorktreeBranch: () => "auto/M001",
180
180
  resolveMilestoneFile: () => null,
181
181
  reconcileMergeState: () => "clean",
182
+ preflightCleanRoot: () => ({ stashPushed: false, summary: "" }),
183
+ postflightPopStash: () => {},
182
184
  getLedger: () => null,
183
185
  getProjectTotals: () => ({ cost: 0 }),
184
186
  formatCost: (c: number) => `$${c.toFixed(2)}`,
@@ -768,6 +768,37 @@ test("runProviderChecks detects claude.cmd in PATH on Windows (#4503)", { skip:
768
768
  });
769
769
  });
770
770
 
771
+ test("runProviderChecks detects claude.exe in PATH on Windows (#4548)", { skip: process.platform !== "win32" }, () => {
772
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-cc-exe-home-")));
773
+ const binDir = join(tmpHome, "bin");
774
+ mkdirSync(binDir, { recursive: true });
775
+
776
+ // Some Windows installs ship a direct claude.exe binary (not a .cmd shim).
777
+ const fakeClaudeExe = join(binDir, "claude.exe");
778
+ writeFileSync(fakeClaudeExe, "");
779
+
780
+ withEnv({
781
+ HOME: tmpHome,
782
+ ANTHROPIC_API_KEY: undefined,
783
+ ANTHROPIC_OAUTH_TOKEN: undefined,
784
+ COPILOT_GITHUB_TOKEN: undefined,
785
+ GH_TOKEN: undefined,
786
+ GITHUB_TOKEN: undefined,
787
+ PATH: `${binDir};${process.env.PATH ?? ""}`,
788
+ PATHEXT: ".COM;.EXE;.BAT;.CMD",
789
+ }, () => {
790
+ try {
791
+ const results = runProviderChecks();
792
+ const anthropic = results.find(r => r.name === "anthropic");
793
+ assert.ok(anthropic, "anthropic result should exist");
794
+ assert.equal(anthropic!.status, "ok", "should be ok when claude.exe is in PATH (#4548)");
795
+ assert.ok(anthropic!.message.toLowerCase().includes("claude"), "should mention claude-code as source");
796
+ } finally {
797
+ rmSync(tmpHome, { recursive: true, force: true });
798
+ }
799
+ });
800
+ });
801
+
771
802
  test("PROVIDER_ROUTES includes google-gemini-cli as route for google (#2922)", async () => {
772
803
  const { readFileSync: readFS } = await import("node:fs");
773
804
  const { dirname: dirn, join: joinPath } = await import("node:path");
@@ -42,7 +42,7 @@ describe("double mergeAndExit guard (#2645)", () => {
42
42
  const allCompleteIdx = phasesSrc.indexOf("incomplete.length === 0");
43
43
  assert.ok(allCompleteIdx > 0, "phases.ts should have an all-milestones-complete check");
44
44
 
45
- const afterAllComplete = phasesSrc.slice(allCompleteIdx, allCompleteIdx + 600);
45
+ const afterAllComplete = phasesSrc.slice(allCompleteIdx, allCompleteIdx + 800);
46
46
  const mergeIdx = afterAllComplete.indexOf("deps.resolver.mergeAndExit");
47
47
  const flagIdx = afterAllComplete.indexOf("s.milestoneMergedInPhases = true");
48
48
 
@@ -389,7 +389,7 @@ describe('ensure-db-open', () => {
389
389
  assert.ok(db, 'adapter should be available after ensureDbOpen');
390
390
  assert.equal(
391
391
  db.prepare('SELECT MAX(version) as version FROM schema_version').get()?.version,
392
- 21,
392
+ 22,
393
393
  'legacy DB should migrate to current schema version',
394
394
  );
395
395
 
@@ -348,7 +348,7 @@ test("ADR-011 P2: schema v20 fresh DB has all escalation columns on tasks + sour
348
348
  assert.ok(decCols.includes("source"), "decisions table must have source column");
349
349
 
350
350
  const version = adapter.prepare("SELECT MAX(version) as v FROM schema_version").get();
351
- assert.equal(version?.["v"], 21);
351
+ assert.equal(version?.["v"], 22);
352
352
  });
353
353
 
354
354
  test("ADR-011 P2: findUnappliedEscalationOverride returns null when escalation_pending=1 (still pending)", (t) => {
@@ -0,0 +1,237 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { listExecHistory, searchExecHistory } from '../exec-history.ts';
8
+ import { executeExecSearch } from '../tools/exec-search-tool.ts';
9
+
10
+ function freshBase(): string {
11
+ return mkdtempSync(join(tmpdir(), 'gsd-exec-history-'));
12
+ }
13
+
14
+ function cleanup(dir: string): void {
15
+ rmSync(dir, { recursive: true, force: true });
16
+ }
17
+
18
+ function writeRun(base: string, id: string, overrides: Record<string, unknown> = {}): void {
19
+ const dir = join(base, '.gsd', 'exec');
20
+ mkdirSync(dir, { recursive: true });
21
+ const stdoutPath = join(dir, `${id}.stdout`);
22
+ const stderrPath = join(dir, `${id}.stderr`);
23
+ const metaPath = join(dir, `${id}.meta.json`);
24
+ writeFileSync(stdoutPath, (overrides.stdout as string | undefined) ?? `stdout for ${id}\n`);
25
+ writeFileSync(stderrPath, '');
26
+ writeFileSync(
27
+ metaPath,
28
+ JSON.stringify({
29
+ id,
30
+ runtime: 'bash',
31
+ purpose: `purpose for ${id}`,
32
+ started_at: '2026-04-20T12:00:00.000Z',
33
+ finished_at: '2026-04-20T12:00:00.100Z',
34
+ duration_ms: 100,
35
+ exit_code: 0,
36
+ signal: null,
37
+ timed_out: false,
38
+ stdout_bytes: 12,
39
+ stderr_bytes: 0,
40
+ stdout_truncated: false,
41
+ stderr_truncated: false,
42
+ stdout_path: stdoutPath,
43
+ stderr_path: stderrPath,
44
+ ...overrides,
45
+ }),
46
+ );
47
+ }
48
+
49
+ test('listExecHistory: returns empty list when .gsd/exec missing', () => {
50
+ const base = freshBase();
51
+ try {
52
+ assert.deepEqual(listExecHistory(base), []);
53
+ } finally {
54
+ cleanup(base);
55
+ }
56
+ });
57
+
58
+ test('listExecHistory: skips malformed meta files', () => {
59
+ const base = freshBase();
60
+ try {
61
+ const dir = join(base, '.gsd', 'exec');
62
+ mkdirSync(dir, { recursive: true });
63
+ writeFileSync(join(dir, 'bad.meta.json'), '{not-json');
64
+ writeRun(base, 'ok-1');
65
+ const list = listExecHistory(base);
66
+ assert.equal(list.length, 1);
67
+ assert.equal(list[0]!.id, 'ok-1');
68
+ } finally {
69
+ cleanup(base);
70
+ }
71
+ });
72
+
73
+ test('searchExecHistory: filters by query, runtime, and failing_only', () => {
74
+ const base = freshBase();
75
+ try {
76
+ writeRun(base, 'playwright-run', { purpose: 'playwright snapshot' });
77
+ writeRun(base, 'grep-run', { purpose: 'grep TODOs' });
78
+ writeRun(base, 'failing-run', { exit_code: 1, purpose: 'boom' });
79
+ writeRun(base, 'node-run', { runtime: 'node', purpose: 'dedupe' });
80
+
81
+ const playwrightHits = searchExecHistory(base, { query: 'playwright' });
82
+ assert.equal(playwrightHits.length, 1);
83
+ assert.equal(playwrightHits[0]!.entry.id, 'playwright-run');
84
+
85
+ const failingHits = searchExecHistory(base, { failing_only: true });
86
+ assert.equal(failingHits.length, 1);
87
+ assert.equal(failingHits[0]!.entry.id, 'failing-run');
88
+
89
+ const nodeHits = searchExecHistory(base, { runtime: 'node' });
90
+ assert.equal(nodeHits.length, 1);
91
+ assert.equal(nodeHits[0]!.entry.runtime, 'node');
92
+
93
+ const unlimited = searchExecHistory(base, {});
94
+ assert.equal(unlimited.length, 4);
95
+ } finally {
96
+ cleanup(base);
97
+ }
98
+ });
99
+
100
+ test('executeExecSearch: returns helpful empty-state message when no matches', () => {
101
+ const base = freshBase();
102
+ try {
103
+ const result = executeExecSearch({ query: 'missing' }, { baseDir: base });
104
+ assert.ok(!result.isError);
105
+ assert.match(result.content[0].text, /No prior gsd_exec runs/);
106
+ } finally {
107
+ cleanup(base);
108
+ }
109
+ });
110
+
111
+ test('executeExecSearch: includes stdout_path and preview in details', () => {
112
+ const base = freshBase();
113
+ try {
114
+ writeRun(base, 'summary-run', { stdout: 'found 42 TODOs\n' });
115
+ const result = executeExecSearch({ query: 'summary' }, { baseDir: base });
116
+ const details = result.details as { results: Array<{ id: string; stdout_path: string }> };
117
+ assert.equal(details.results.length, 1);
118
+ assert.equal(details.results[0]!.id, 'summary-run');
119
+ assert.match(details.results[0]!.stdout_path, /summary-run\.stdout$/);
120
+ assert.match(result.content[0].text, /found 42 TODOs/);
121
+ } finally {
122
+ cleanup(base);
123
+ }
124
+ });
125
+
126
+ // ── Path traversal security tests (issue #4590) ───────────────────────────
127
+
128
+ test('safeReadMeta: ignores malicious stdout_path in JSON, derives path from meta file location', () => {
129
+ // Arrange: write a .meta.json whose JSON content has a path-traversal value
130
+ // in stdout_path / stderr_path. The read-side must silently discard these
131
+ // and derive sibling paths from the actual .meta.json location instead.
132
+ const base = freshBase();
133
+ try {
134
+ const dir = join(base, '.gsd', 'exec');
135
+ mkdirSync(dir, { recursive: true });
136
+ const id = 'traversal-test-run';
137
+ const metaPath = join(dir, `${id}.meta.json`);
138
+ const stdoutPath = join(dir, `${id}.stdout`);
139
+ const stderrPath = join(dir, `${id}.stderr`);
140
+ // Write real sibling files so digest_preview can succeed.
141
+ writeFileSync(stdoutPath, 'legitimate stdout content\n');
142
+ writeFileSync(stderrPath, '');
143
+ // Write a meta.json that tries to point stdout_path outside the exec dir.
144
+ writeFileSync(
145
+ metaPath,
146
+ JSON.stringify({
147
+ id,
148
+ runtime: 'bash',
149
+ purpose: 'test run',
150
+ started_at: '2026-04-20T12:00:00.000Z',
151
+ finished_at: '2026-04-20T12:00:00.100Z',
152
+ duration_ms: 100,
153
+ exit_code: 0,
154
+ signal: null,
155
+ timed_out: false,
156
+ stdout_bytes: 24,
157
+ stderr_bytes: 0,
158
+ stdout_truncated: false,
159
+ stderr_truncated: false,
160
+ // These malicious values must NEVER be used as filesystem paths.
161
+ stdout_path: '../../etc/passwd',
162
+ stderr_path: '../../etc/shadow',
163
+ }),
164
+ );
165
+
166
+ const entries = listExecHistory(base);
167
+ assert.equal(entries.length, 1);
168
+ const entry = entries[0]!;
169
+
170
+ // stdout_path must be derived from the meta file location, not from JSON.
171
+ assert.equal(entry.stdout_path, stdoutPath,
172
+ `stdout_path must be a sibling of the meta file; got: ${entry.stdout_path}`);
173
+ assert.equal(entry.stderr_path, stderrPath,
174
+ `stderr_path must be a sibling of the meta file; got: ${entry.stderr_path}`);
175
+
176
+ // Verify neither traversal string leaked into the returned entry.
177
+ assert.ok(!entry.stdout_path.includes('..'),
178
+ `stdout_path must not contain path traversal sequences: ${entry.stdout_path}`);
179
+ assert.ok(!entry.stderr_path.includes('..'),
180
+ `stderr_path must not contain path traversal sequences: ${entry.stderr_path}`);
181
+ assert.ok(!entry.stdout_path.includes('etc/passwd'),
182
+ `stdout_path must not point to /etc/passwd: ${entry.stdout_path}`);
183
+ } finally {
184
+ cleanup(base);
185
+ }
186
+ });
187
+
188
+ test('searchExecHistory: digest_preview is read from derived sibling path, not JSON stdout_path', () => {
189
+ // Arrange: a .meta.json with a malicious stdout_path pointing to /etc/passwd.
190
+ // The digest_preview should be read from the real sibling .stdout file,
191
+ // not from the JSON-supplied path.
192
+ const base = freshBase();
193
+ try {
194
+ const dir = join(base, '.gsd', 'exec');
195
+ mkdirSync(dir, { recursive: true });
196
+ const id = 'preview-traversal-run';
197
+ const metaPath = join(dir, `${id}.meta.json`);
198
+ const stdoutPath = join(dir, `${id}.stdout`);
199
+ writeFileSync(stdoutPath, 'safe-sentinel-content\n');
200
+ writeFileSync(join(dir, `${id}.stderr`), '');
201
+ writeFileSync(
202
+ metaPath,
203
+ JSON.stringify({
204
+ id,
205
+ runtime: 'bash',
206
+ purpose: null,
207
+ started_at: '2026-04-20T12:00:00.000Z',
208
+ finished_at: '2026-04-20T12:00:00.100Z',
209
+ duration_ms: 50,
210
+ exit_code: 0,
211
+ signal: null,
212
+ timed_out: false,
213
+ stdout_bytes: 21,
214
+ stderr_bytes: 0,
215
+ stdout_truncated: false,
216
+ stderr_truncated: false,
217
+ // Attacker-controlled path — must be ignored.
218
+ stdout_path: '/etc/passwd',
219
+ stderr_path: '/etc/shadow',
220
+ }),
221
+ );
222
+
223
+ const hits = searchExecHistory(base, {});
224
+ assert.equal(hits.length, 1);
225
+ const hit = hits[0]!;
226
+
227
+ // The preview must come from the safe sibling, not /etc/passwd.
228
+ assert.ok(
229
+ hit.digest_preview?.includes('safe-sentinel-content'),
230
+ `digest_preview should contain safe-sentinel-content; got: ${hit.digest_preview}`,
231
+ );
232
+ // Ensure the entry paths are the derived ones.
233
+ assert.equal(hit.entry.stdout_path, stdoutPath);
234
+ } finally {
235
+ cleanup(base);
236
+ }
237
+ });
@@ -0,0 +1,210 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { EXEC_DEFAULTS, runExecSandbox, type ExecSandboxOptions } from '../exec-sandbox.ts';
8
+ import { buildExecOptions, executeGsdExec } from '../tools/exec-tool.ts';
9
+ import { isContextModeEnabled } from '../preferences-types.ts';
10
+
11
+ function freshBase(): string {
12
+ return mkdtempSync(join(tmpdir(), 'gsd-exec-test-'));
13
+ }
14
+
15
+ function cleanup(dir: string): void {
16
+ rmSync(dir, { recursive: true, force: true });
17
+ }
18
+
19
+ function baseOpts(base: string, overrides: Partial<ExecSandboxOptions> = {}): ExecSandboxOptions {
20
+ return {
21
+ baseDir: base,
22
+ clamp_timeout_ms: EXEC_DEFAULTS.clampTimeoutMs,
23
+ default_timeout_ms: 10_000,
24
+ stdout_cap_bytes: 1_024,
25
+ stderr_cap_bytes: 1_024,
26
+ digest_chars: 120,
27
+ env_allowlist: EXEC_DEFAULTS.envAllowlist,
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ test('runExecSandbox: captures stdout, persists artifacts, returns digest', async () => {
33
+ const base = freshBase();
34
+ try {
35
+ const result = await runExecSandbox(
36
+ { runtime: 'bash', script: 'echo hello world' },
37
+ baseOpts(base),
38
+ );
39
+ assert.equal(result.exit_code, 0);
40
+ assert.equal(result.timed_out, false);
41
+ assert.ok(result.digest.includes('hello world'), `digest should contain stdout: ${result.digest}`);
42
+ assert.ok(result.stdout_path.startsWith(join(base, '.gsd', 'exec')), 'stdout path under .gsd/exec');
43
+ assert.equal(readFileSync(result.stdout_path, 'utf-8').trim(), 'hello world');
44
+ const meta = JSON.parse(readFileSync(result.meta_path, 'utf-8')) as Record<string, unknown>;
45
+ assert.equal(meta.runtime, 'bash');
46
+ assert.equal(meta.exit_code, 0);
47
+ } finally {
48
+ cleanup(base);
49
+ }
50
+ });
51
+
52
+ test('runExecSandbox: enforces stdout cap and marks truncation', async () => {
53
+ const base = freshBase();
54
+ try {
55
+ const result = await runExecSandbox(
56
+ // Emit far more than the cap so truncation triggers.
57
+ { runtime: 'bash', script: 'head -c 8000 /dev/urandom | base64' },
58
+ baseOpts(base, { stdout_cap_bytes: 256 }),
59
+ );
60
+ assert.equal(result.stdout_truncated, true, 'should mark stdout truncated');
61
+ assert.ok(result.stdout_bytes <= 256, `stdout_bytes within cap (got ${result.stdout_bytes})`);
62
+ const stdout = readFileSync(result.stdout_path, 'utf-8');
63
+ assert.ok(stdout.endsWith('[truncated: stdout cap reached]\n'), 'truncation marker appended');
64
+ } finally {
65
+ cleanup(base);
66
+ }
67
+ });
68
+
69
+ test('runExecSandbox: enforces timeout and surfaces timed_out', async () => {
70
+ const base = freshBase();
71
+ try {
72
+ const started = Date.now();
73
+ const result = await runExecSandbox(
74
+ { runtime: 'bash', script: 'sleep 10' },
75
+ baseOpts(base, { default_timeout_ms: 150, clamp_timeout_ms: 150 }),
76
+ );
77
+ const elapsed = Date.now() - started;
78
+ assert.equal(result.timed_out, true);
79
+ assert.ok(elapsed < 5_000, `should return well before 10s (took ${elapsed}ms)`);
80
+ } finally {
81
+ cleanup(base);
82
+ }
83
+ });
84
+
85
+ test('runExecSandbox: forwards only allowlisted env vars', async () => {
86
+ const base = freshBase();
87
+ try {
88
+ const result = await runExecSandbox(
89
+ { runtime: 'bash', script: 'echo PATH=$PATH SECRET=$GSD_TEST_SECRET' },
90
+ baseOpts(base, {
91
+ env_allowlist: [],
92
+ env: { PATH: '/usr/bin:/bin', HOME: '/tmp', GSD_TEST_SECRET: 'should-be-blocked' },
93
+ }),
94
+ );
95
+ const stdout = readFileSync(result.stdout_path, 'utf-8');
96
+ assert.ok(stdout.includes('PATH=/usr/bin:/bin'), 'PATH forwarded');
97
+ assert.ok(!stdout.includes('should-be-blocked'), 'non-allowlisted var blocked');
98
+ } finally {
99
+ cleanup(base);
100
+ }
101
+ });
102
+
103
+ test('runExecSandbox: node runtime executes JS', async () => {
104
+ const base = freshBase();
105
+ try {
106
+ const result = await runExecSandbox(
107
+ { runtime: 'node', script: 'console.log("node-ok:" + (1+2))' },
108
+ baseOpts(base),
109
+ );
110
+ assert.equal(result.exit_code, 0);
111
+ assert.ok(result.digest.includes('node-ok:3'));
112
+ } finally {
113
+ cleanup(base);
114
+ }
115
+ });
116
+
117
+ // ── exec-tool executor ────────────────────────────────────────────────────
118
+
119
+ test('executeGsdExec: runs by default when context_mode is unset', async () => {
120
+ const base = freshBase();
121
+ try {
122
+ const result = await executeGsdExec(
123
+ { runtime: 'bash', script: 'echo default-on-run' },
124
+ { baseDir: base, preferences: {} },
125
+ );
126
+ assert.ok(!result.isError, 'should succeed with no preferences');
127
+ assert.equal(result.details.operation, 'gsd_exec');
128
+ assert.equal(result.details.exit_code, 0);
129
+ assert.ok(result.content[0].text.includes('default-on-run'));
130
+ } finally {
131
+ cleanup(base);
132
+ }
133
+ });
134
+
135
+ test('executeGsdExec: runs when preferences is null (fresh project)', async () => {
136
+ const base = freshBase();
137
+ try {
138
+ const result = await executeGsdExec(
139
+ { runtime: 'bash', script: 'echo null-prefs-run' },
140
+ { baseDir: base, preferences: null },
141
+ );
142
+ assert.ok(!result.isError, 'null preferences should not disable');
143
+ assert.ok(result.content[0].text.includes('null-prefs-run'));
144
+ } finally {
145
+ cleanup(base);
146
+ }
147
+ });
148
+
149
+ test('executeGsdExec: blocked only when context_mode.enabled=false', async () => {
150
+ const base = freshBase();
151
+ try {
152
+ const result = await executeGsdExec(
153
+ { runtime: 'bash', script: 'echo should-not-run' },
154
+ { baseDir: base, preferences: { context_mode: { enabled: false } } },
155
+ );
156
+ assert.equal(result.isError, true);
157
+ assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
158
+ } finally {
159
+ cleanup(base);
160
+ }
161
+ });
162
+
163
+ test('executeGsdExec: runs when enabled explicitly set to true', async () => {
164
+ const base = freshBase();
165
+ try {
166
+ const result = await executeGsdExec(
167
+ { runtime: 'bash', script: 'echo explicit-on' },
168
+ { baseDir: base, preferences: { context_mode: { enabled: true } } },
169
+ );
170
+ assert.ok(!result.isError);
171
+ assert.ok(result.content[0].text.includes('explicit-on'));
172
+ } finally {
173
+ cleanup(base);
174
+ }
175
+ });
176
+
177
+ test('executeGsdExec: rejects empty script', async () => {
178
+ const base = freshBase();
179
+ try {
180
+ const result = await executeGsdExec(
181
+ { runtime: 'bash', script: ' ' },
182
+ { baseDir: base, preferences: { context_mode: { enabled: true } } },
183
+ );
184
+ assert.equal(result.isError, true);
185
+ assert.equal((result.details as { error?: string }).error, 'invalid_params');
186
+ } finally {
187
+ cleanup(base);
188
+ }
189
+ });
190
+
191
+ test('isContextModeEnabled: defaults to true; only explicit false disables', () => {
192
+ assert.equal(isContextModeEnabled(undefined), true, 'undefined prefs → on');
193
+ assert.equal(isContextModeEnabled(null), true, 'null prefs → on');
194
+ assert.equal(isContextModeEnabled({}), true, 'empty prefs → on');
195
+ assert.equal(isContextModeEnabled({ context_mode: {} }), true, 'empty block → on');
196
+ assert.equal(isContextModeEnabled({ context_mode: { enabled: true } }), true);
197
+ assert.equal(isContextModeEnabled({ context_mode: { enabled: false } }), false);
198
+ });
199
+
200
+ test('buildExecOptions: clamps out-of-range values to safe defaults', () => {
201
+ const opts = buildExecOptions('/tmp/base', {
202
+ enabled: true,
203
+ exec_timeout_ms: 999_999_999,
204
+ exec_stdout_cap_bytes: 1,
205
+ exec_digest_chars: -20,
206
+ });
207
+ assert.equal(opts.default_timeout_ms, EXEC_DEFAULTS.clampTimeoutMs, 'timeout clamped to upper bound');
208
+ assert.equal(opts.stdout_cap_bytes, 4_096, 'stdout cap clamped to floor');
209
+ assert.equal(opts.digest_chars, 0, 'digest chars clamped to floor');
210
+ });
@@ -15,6 +15,64 @@ function git(cwd: string, ...args: string[]): string {
15
15
  }).trim();
16
16
  }
17
17
 
18
+ test("validateFileChanges works on repos with a single commit (no HEAD~1)", (t) => {
19
+ const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
20
+ t.after(() => rmSync(base, { recursive: true, force: true }));
21
+
22
+ git(base, "init");
23
+ git(base, "config", "user.email", "test@example.com");
24
+ git(base, "config", "user.name", "Test User");
25
+
26
+ writeFileSync(join(base, "foo.ts"), "export const x = 1;\n");
27
+ git(base, "add", ".");
28
+ git(base, "commit", "-m", "initial");
29
+
30
+ // With only one commit, HEAD~1 doesn't exist — this must not throw
31
+ const audit = validateFileChanges(base, ["foo.ts"], []);
32
+
33
+ assert.ok(audit, "audit should be produced for single-commit repo");
34
+ assert.deepEqual(audit.unexpectedFiles, []);
35
+ assert.deepEqual(audit.missingFiles, []);
36
+ });
37
+
38
+ test("validateFileChanges excludes allowlisted files from unexpected-change warnings", (t) => {
39
+ const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
40
+ t.after(() => rmSync(base, { recursive: true, force: true }));
41
+
42
+ mkdirSync(join(base, "tracking", "history"), { recursive: true });
43
+ git(base, "init");
44
+ git(base, "config", "user.email", "test@example.com");
45
+ git(base, "config", "user.name", "Test User");
46
+
47
+ writeFileSync(join(base, "src.ts"), "initial\n");
48
+ writeFileSync(join(base, "tracking", "history", "2026-04-20-snapshot.md"), "initial\n");
49
+ git(base, "add", ".");
50
+ git(base, "commit", "-m", "initial");
51
+
52
+ writeFileSync(join(base, "src.ts"), "updated\n");
53
+ writeFileSync(join(base, "tracking", "history", "2026-04-20-snapshot.md"), "updated\n");
54
+ git(base, "add", ".");
55
+ git(base, "commit", "-m", "update");
56
+
57
+ // Without allowlist: tracking/history snapshot is unexpected
58
+ const auditWithout = validateFileChanges(base, ["src.ts"], []);
59
+ assert.ok(auditWithout, "audit should be produced");
60
+ assert.ok(
61
+ auditWithout.unexpectedFiles.includes("tracking/history/2026-04-20-snapshot.md"),
62
+ "snapshot should be unexpected without allowlist",
63
+ );
64
+
65
+ // With glob allowlist: snapshot is excluded
66
+ const auditWith = validateFileChanges(base, ["src.ts"], [], ["tracking/history/**"]);
67
+ assert.ok(auditWith, "audit should be produced with allowlist");
68
+ assert.deepEqual(auditWith.unexpectedFiles, [], "no unexpected files when snapshot is allowlisted");
69
+ assert.equal(
70
+ auditWith.violations.filter(v => v.severity === "warning").length,
71
+ 0,
72
+ "no warnings when all unexpected files are allowlisted",
73
+ );
74
+ });
75
+
18
76
  test("validateFileChanges ignores inline descriptions in expected output paths", (t) => {
19
77
  const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
20
78
  t.after(() => rmSync(base, { recursive: true, force: true }));