gsd-pi 2.71.0-dev.e17e0ce → 2.72.0-dev.3118184

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 (352) hide show
  1. package/README.md +46 -3
  2. package/dist/cli.js +76 -3
  3. package/dist/mcp-server.js +37 -14
  4. package/dist/onboarding.js +10 -0
  5. package/dist/resources/agents/debugger.md +58 -0
  6. package/dist/resources/agents/doc-writer.md +43 -0
  7. package/dist/resources/agents/git-ops.md +56 -0
  8. package/dist/resources/agents/javascript-pro.md +46 -271
  9. package/dist/resources/agents/planner.md +55 -0
  10. package/dist/resources/agents/refactorer.md +47 -0
  11. package/dist/resources/agents/reviewer.md +48 -0
  12. package/dist/resources/agents/security.md +59 -0
  13. package/dist/resources/agents/tester.md +50 -0
  14. package/dist/resources/agents/typescript-pro.md +41 -235
  15. package/dist/resources/extensions/async-jobs/await-tool.js +7 -4
  16. package/dist/resources/extensions/async-jobs/job-manager.js +28 -3
  17. package/dist/resources/extensions/claude-code-cli/partial-builder.js +40 -12
  18. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +132 -10
  19. package/dist/resources/extensions/gsd/auto/loop.js +84 -1
  20. package/dist/resources/extensions/gsd/auto/phases.js +4 -0
  21. package/dist/resources/extensions/gsd/auto-post-unit.js +6 -0
  22. package/dist/resources/extensions/gsd/auto-prompts.js +88 -33
  23. package/dist/resources/extensions/gsd/auto-recovery.js +11 -0
  24. package/dist/resources/extensions/gsd/auto-start.js +24 -4
  25. package/dist/resources/extensions/gsd/auto.js +29 -19
  26. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -3
  27. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +9 -11
  28. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +2 -5
  29. package/dist/resources/extensions/gsd/commands-handlers.js +4 -1
  30. package/dist/resources/extensions/gsd/context-injector.js +1 -1
  31. package/dist/resources/extensions/gsd/custom-workflow-engine.js +3 -7
  32. package/dist/resources/extensions/gsd/definition-io.js +15 -0
  33. package/dist/resources/extensions/gsd/dispatch-guard.js +4 -0
  34. package/dist/resources/extensions/gsd/doctor-providers.js +23 -0
  35. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +6 -3
  36. package/dist/resources/extensions/gsd/error-classifier.js +4 -1
  37. package/dist/resources/extensions/gsd/gate-registry.js +208 -0
  38. package/dist/resources/extensions/gsd/git-service.js +11 -8
  39. package/dist/resources/extensions/gsd/gitignore.js +12 -6
  40. package/dist/resources/extensions/gsd/gsd-db.js +90 -6
  41. package/dist/resources/extensions/gsd/key-manager.js +2 -0
  42. package/dist/resources/extensions/gsd/milestone-validation-gates.js +11 -12
  43. package/dist/resources/extensions/gsd/notification-overlay.js +26 -12
  44. package/dist/resources/extensions/gsd/notification-store.js +5 -4
  45. package/dist/resources/extensions/gsd/preferences-skills.js +2 -34
  46. package/dist/resources/extensions/gsd/preferences-types.js +15 -0
  47. package/dist/resources/extensions/gsd/preferences.js +16 -3
  48. package/dist/resources/extensions/gsd/prompt-loader.js +4 -1
  49. package/dist/resources/extensions/gsd/prompt-validation.js +126 -0
  50. package/dist/resources/extensions/gsd/prompts/complete-slice.md +3 -1
  51. package/dist/resources/extensions/gsd/prompts/discuss.md +122 -13
  52. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
  53. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  54. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  55. package/dist/resources/extensions/gsd/shortcut-defs.js +7 -1
  56. package/dist/resources/extensions/gsd/state.js +29 -2
  57. package/dist/resources/extensions/gsd/tools/complete-slice.js +52 -1
  58. package/dist/resources/extensions/gsd/tools/complete-task.js +51 -1
  59. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +4 -1
  60. package/dist/resources/extensions/gsd/workflow-projections.js +7 -0
  61. package/dist/resources/extensions/gsd/worktree-manager.js +30 -3
  62. package/dist/resources/extensions/gsd/write-intercept.js +10 -1
  63. package/dist/resources/extensions/ollama/index.js +17 -10
  64. package/dist/resources/extensions/ollama/ollama-client.js +35 -6
  65. package/dist/resources/extensions/ollama/ollama-discovery.js +32 -6
  66. package/dist/resources/extensions/shared/gsd-phase-state.js +35 -0
  67. package/dist/resources/extensions/subagent/agents.js +8 -0
  68. package/dist/resources/extensions/subagent/index.js +17 -0
  69. package/dist/startup-model-validation.d.ts +0 -1
  70. package/dist/startup-model-validation.js +6 -2
  71. package/dist/web/standalone/.next/BUILD_ID +1 -1
  72. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  73. package/dist/web/standalone/.next/build-manifest.json +2 -2
  74. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  75. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  76. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  78. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  86. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  88. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  99. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  105. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  115. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  120. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  123. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  130. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  131. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  132. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +3 -3
  133. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  134. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  135. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  136. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  137. package/dist/web/standalone/.next/server/app/index.html +1 -1
  138. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  139. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  140. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  141. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  142. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  143. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  144. package/dist/web/standalone/.next/server/app/page.js +2 -2
  145. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  146. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  147. package/dist/web/standalone/.next/server/chunks/2331.js +16 -16
  148. package/dist/web/standalone/.next/server/chunks/4741.js +12 -12
  149. package/dist/web/standalone/.next/server/chunks/5822.js +2 -2
  150. package/dist/web/standalone/.next/server/chunks/63.js +8 -8
  151. package/dist/web/standalone/.next/server/chunks/6897.js +3 -3
  152. package/dist/web/standalone/.next/server/edge-runtime-webpack.js +2 -0
  153. package/dist/web/standalone/.next/server/functions-config-manifest.json +0 -9
  154. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  155. package/dist/web/standalone/.next/server/middleware-manifest.json +29 -2
  156. package/dist/web/standalone/.next/server/middleware.js +4 -12
  157. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  158. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  159. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  160. package/dist/web/standalone/.next/server/webpack-runtime.js +1 -1
  161. package/package.json +1 -1
  162. package/packages/mcp-server/dist/server.d.ts +12 -1
  163. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  164. package/packages/mcp-server/dist/server.js +90 -42
  165. package/packages/mcp-server/dist/server.js.map +1 -1
  166. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  167. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  168. package/packages/mcp-server/src/server.ts +110 -38
  169. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  170. package/packages/pi-ai/dist/env-api-keys.js +1 -0
  171. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  172. package/packages/pi-ai/dist/models.custom.d.ts +105 -0
  173. package/packages/pi-ai/dist/models.custom.d.ts.map +1 -1
  174. package/packages/pi-ai/dist/models.custom.js +97 -0
  175. package/packages/pi-ai/dist/models.custom.js.map +1 -1
  176. package/packages/pi-ai/dist/models.generated.d.ts +648 -140
  177. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  178. package/packages/pi-ai/dist/models.generated.js +867 -370
  179. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  180. package/packages/pi-ai/dist/models.generated.test.d.ts +2 -0
  181. package/packages/pi-ai/dist/models.generated.test.d.ts.map +1 -0
  182. package/packages/pi-ai/dist/models.generated.test.js +334 -0
  183. package/packages/pi-ai/dist/models.generated.test.js.map +1 -0
  184. package/packages/pi-ai/dist/models.test.js +105 -0
  185. package/packages/pi-ai/dist/models.test.js.map +1 -1
  186. package/packages/pi-ai/dist/types.d.ts +1 -1
  187. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  188. package/packages/pi-ai/dist/types.js.map +1 -1
  189. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  190. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +5 -1
  191. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  192. package/packages/pi-ai/dist/utils/oauth/github-copilot.test.d.ts +2 -0
  193. package/packages/pi-ai/dist/utils/oauth/github-copilot.test.d.ts.map +1 -0
  194. package/packages/pi-ai/dist/utils/oauth/github-copilot.test.js +57 -0
  195. package/packages/pi-ai/dist/utils/oauth/github-copilot.test.js.map +1 -0
  196. package/packages/pi-ai/src/env-api-keys.ts +1 -0
  197. package/packages/pi-ai/src/models.custom.ts +98 -0
  198. package/packages/pi-ai/src/models.generated.test.ts +373 -0
  199. package/packages/pi-ai/src/models.generated.ts +867 -370
  200. package/packages/pi-ai/src/models.test.ts +135 -0
  201. package/packages/pi-ai/src/types.ts +1 -0
  202. package/packages/pi-ai/src/utils/oauth/github-copilot.test.ts +71 -0
  203. package/packages/pi-ai/src/utils/oauth/github-copilot.ts +4 -1
  204. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  205. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  206. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  207. package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts +8 -0
  208. package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts.map +1 -0
  209. package/packages/pi-coding-agent/dist/core/model-resolver.test.js +75 -0
  210. package/packages/pi-coding-agent/dist/core/model-resolver.test.js.map +1 -0
  211. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +5 -0
  212. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  213. package/packages/pi-coding-agent/dist/core/retry-handler.js +55 -1
  214. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  215. package/packages/pi-coding-agent/dist/core/retry-handler.test.js +57 -0
  216. package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
  217. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  218. package/packages/pi-coding-agent/dist/core/sdk.js +9 -0
  219. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  220. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +36 -0
  221. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  222. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  223. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
  224. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  225. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  226. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +87 -12
  227. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  228. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts.map +1 -1
  229. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js +6 -1
  230. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js.map +1 -1
  231. package/packages/pi-coding-agent/package.json +1 -1
  232. package/packages/pi-coding-agent/src/core/model-resolver.test.ts +85 -0
  233. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  234. package/packages/pi-coding-agent/src/core/retry-handler.test.ts +83 -0
  235. package/packages/pi-coding-agent/src/core/retry-handler.ts +60 -1
  236. package/packages/pi-coding-agent/src/core/sdk.ts +10 -0
  237. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +72 -0
  238. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +15 -6
  239. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +84 -12
  240. package/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +6 -1
  241. package/packages/pi-tui/dist/components/__tests__/editor.test.js +12 -0
  242. package/packages/pi-tui/dist/components/__tests__/editor.test.js.map +1 -1
  243. package/packages/pi-tui/dist/components/__tests__/input.test.js +12 -0
  244. package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
  245. package/packages/pi-tui/dist/keys.d.ts.map +1 -1
  246. package/packages/pi-tui/dist/keys.js +27 -0
  247. package/packages/pi-tui/dist/keys.js.map +1 -1
  248. package/packages/pi-tui/src/components/__tests__/editor.test.ts +18 -0
  249. package/packages/pi-tui/src/components/__tests__/input.test.ts +18 -0
  250. package/packages/pi-tui/src/keys.ts +32 -0
  251. package/pkg/package.json +1 -1
  252. package/src/resources/agents/debugger.md +58 -0
  253. package/src/resources/agents/doc-writer.md +43 -0
  254. package/src/resources/agents/git-ops.md +56 -0
  255. package/src/resources/agents/javascript-pro.md +46 -271
  256. package/src/resources/agents/planner.md +55 -0
  257. package/src/resources/agents/refactorer.md +47 -0
  258. package/src/resources/agents/reviewer.md +48 -0
  259. package/src/resources/agents/security.md +59 -0
  260. package/src/resources/agents/tester.md +50 -0
  261. package/src/resources/agents/typescript-pro.md +41 -235
  262. package/src/resources/extensions/async-jobs/await-tool.test.ts +40 -7
  263. package/src/resources/extensions/async-jobs/await-tool.ts +7 -4
  264. package/src/resources/extensions/async-jobs/job-manager.ts +33 -3
  265. package/src/resources/extensions/claude-code-cli/partial-builder.ts +45 -12
  266. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +139 -8
  267. package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +91 -2
  268. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +245 -2
  269. package/src/resources/extensions/gsd/auto/loop.ts +89 -1
  270. package/src/resources/extensions/gsd/auto/phases.ts +4 -0
  271. package/src/resources/extensions/gsd/auto-post-unit.ts +7 -0
  272. package/src/resources/extensions/gsd/auto-prompts.ts +111 -33
  273. package/src/resources/extensions/gsd/auto-recovery.ts +10 -0
  274. package/src/resources/extensions/gsd/auto-start.ts +31 -4
  275. package/src/resources/extensions/gsd/auto.ts +29 -20
  276. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -3
  277. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -10
  278. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +2 -5
  279. package/src/resources/extensions/gsd/commands-handlers.ts +5 -1
  280. package/src/resources/extensions/gsd/context-injector.ts +1 -1
  281. package/src/resources/extensions/gsd/custom-workflow-engine.ts +4 -8
  282. package/src/resources/extensions/gsd/definition-io.ts +18 -0
  283. package/src/resources/extensions/gsd/dispatch-guard.ts +5 -0
  284. package/src/resources/extensions/gsd/doctor-providers.ts +24 -0
  285. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +6 -3
  286. package/src/resources/extensions/gsd/error-classifier.ts +4 -1
  287. package/src/resources/extensions/gsd/gate-registry.ts +251 -0
  288. package/src/resources/extensions/gsd/git-service.ts +11 -8
  289. package/src/resources/extensions/gsd/gitignore.ts +12 -6
  290. package/src/resources/extensions/gsd/gsd-db.ts +105 -6
  291. package/src/resources/extensions/gsd/key-manager.ts +2 -0
  292. package/src/resources/extensions/gsd/milestone-validation-gates.ts +11 -13
  293. package/src/resources/extensions/gsd/notification-overlay.ts +27 -11
  294. package/src/resources/extensions/gsd/notification-store.ts +5 -4
  295. package/src/resources/extensions/gsd/preferences-skills.ts +2 -36
  296. package/src/resources/extensions/gsd/preferences-types.ts +16 -0
  297. package/src/resources/extensions/gsd/preferences.ts +19 -6
  298. package/src/resources/extensions/gsd/prompt-loader.ts +6 -1
  299. package/src/resources/extensions/gsd/prompt-validation.ts +157 -0
  300. package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -1
  301. package/src/resources/extensions/gsd/prompts/discuss.md +122 -13
  302. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
  303. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  304. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  305. package/src/resources/extensions/gsd/shortcut-defs.ts +8 -1
  306. package/src/resources/extensions/gsd/state.ts +33 -2
  307. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +27 -0
  308. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +14 -0
  309. package/src/resources/extensions/gsd/tests/block-db-writes.test.ts +63 -0
  310. package/src/resources/extensions/gsd/tests/complete-slice-gate-closure.test.ts +167 -0
  311. package/src/resources/extensions/gsd/tests/definition-io.test.ts +57 -0
  312. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +26 -0
  313. package/src/resources/extensions/gsd/tests/doctor-heal-fixable-warnings.test.ts +14 -0
  314. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +36 -0
  315. package/src/resources/extensions/gsd/tests/false-degraded-mode-warning.test.ts +104 -0
  316. package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +16 -0
  317. package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +27 -0
  318. package/src/resources/extensions/gsd/tests/gate-registry.test.ts +140 -0
  319. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +107 -5
  320. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +8 -6
  321. package/src/resources/extensions/gsd/tests/key-manager.test.ts +63 -0
  322. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +54 -0
  323. package/src/resources/extensions/gsd/tests/plan-milestone-artifact-verification.test.ts +62 -0
  324. package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +34 -0
  325. package/src/resources/extensions/gsd/tests/preferences-formatting.test.ts +87 -0
  326. package/src/resources/extensions/gsd/tests/preferences.test.ts +53 -0
  327. package/src/resources/extensions/gsd/tests/projection-regression.test.ts +96 -1
  328. package/src/resources/extensions/gsd/tests/prompt-loader-working-directory.test.ts +19 -0
  329. package/src/resources/extensions/gsd/tests/prompt-system-gate-coverage.test.ts +208 -0
  330. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +9 -0
  331. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +97 -0
  332. package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +3 -2
  333. package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +41 -0
  334. package/src/resources/extensions/gsd/tools/complete-slice.ts +63 -0
  335. package/src/resources/extensions/gsd/tools/complete-task.ts +63 -0
  336. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +4 -1
  337. package/src/resources/extensions/gsd/types.ts +26 -0
  338. package/src/resources/extensions/gsd/workflow-projections.ts +8 -0
  339. package/src/resources/extensions/gsd/worktree-manager.ts +29 -3
  340. package/src/resources/extensions/gsd/write-intercept.ts +10 -1
  341. package/src/resources/extensions/ollama/index.ts +17 -8
  342. package/src/resources/extensions/ollama/ollama-client.ts +35 -6
  343. package/src/resources/extensions/ollama/ollama-discovery.ts +37 -6
  344. package/src/resources/extensions/ollama/ollama-status-indicator.test.ts +28 -0
  345. package/src/resources/extensions/ollama/tests/ollama-discovery.test.ts +54 -0
  346. package/src/resources/extensions/shared/gsd-phase-state.ts +42 -0
  347. package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +48 -0
  348. package/src/resources/extensions/subagent/agents.ts +10 -0
  349. package/src/resources/extensions/subagent/index.ts +18 -0
  350. package/src/resources/extensions/subagent/tests/agents-conflicts.test.ts +33 -0
  351. /package/dist/web/standalone/.next/static/{cYPZv_bAhZk2ms-Pz6vsY → NzO79SOz9jHX-VY5-0t2O}/_buildManifest.js +0 -0
  352. /package/dist/web/standalone/.next/static/{cYPZv_bAhZk2ms-Pz6vsY → NzO79SOz9jHX-VY5-0t2O}/_ssgManifest.js +0 -0
@@ -187,20 +187,36 @@ function extractMessageText(msg: { role: string; content: unknown }): string {
187
187
  * call effectively stateless. This version serialises the complete
188
188
  * conversation history (system prompt + all user/assistant turns) so
189
189
  * Claude Code has full context for multi-turn continuity.
190
+ *
191
+ * History is wrapped in XML-tag structure rather than `[User]`/`[Assistant]`
192
+ * bracket headers. Bracket headers read to the model as an in-context
193
+ * demonstration of how turns are delimited, causing it to fabricate fake
194
+ * user turns in its own output. XML tags read as document structure and
195
+ * don't get mirrored in free text.
190
196
  */
191
197
  export function buildPromptFromContext(context: Context): string {
192
- const parts: string[] = [];
198
+ const hasContent = Boolean(context.systemPrompt) || context.messages.some((m) => extractMessageText(m));
199
+ if (!hasContent) return "";
200
+
201
+ const parts: string[] = [
202
+ "Respond only to the final user message below. " +
203
+ "Do not emit <user_message>, <assistant_message>, or <prior_system_context> tags in your response.",
204
+ ];
193
205
 
194
206
  if (context.systemPrompt) {
195
- parts.push(`[System]\n${context.systemPrompt}`);
207
+ parts.push(`<prior_system_context>\n${context.systemPrompt}\n</prior_system_context>`);
196
208
  }
197
209
 
210
+ const turns: string[] = [];
198
211
  for (const msg of context.messages) {
199
212
  const text = extractMessageText(msg);
200
213
  if (!text) continue;
201
-
202
- const label = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
203
- parts.push(`[${label}]\n${text}`);
214
+ const tag =
215
+ msg.role === "user" ? "user_message" : msg.role === "assistant" ? "assistant_message" : "system_message";
216
+ turns.push(`<${tag}>\n${text}\n</${tag}>`);
217
+ }
218
+ if (turns.length > 0) {
219
+ parts.push(`<conversation_history>\n${turns.join("\n")}\n</conversation_history>`);
204
220
  }
205
221
 
206
222
  return parts.join("\n\n");
@@ -518,34 +534,104 @@ export function createClaudeCodeElicitationHandler(
518
534
  };
519
535
  }
520
536
 
537
+ /**
538
+ * Aborted by the caller's AbortSignal — distinct from exhaustion. GSD's
539
+ * agent loop keys off `stopReason === "aborted"` to treat this as a clean
540
+ * user cancel instead of a retry-eligible provider failure.
541
+ */
542
+ export function makeAbortedMessage(model: string, lastTextContent: string): AssistantMessage {
543
+ const message: AssistantMessage = {
544
+ role: "assistant",
545
+ content: lastTextContent
546
+ ? [{ type: "text", text: lastTextContent }]
547
+ : [{ type: "text", text: "Claude Code stream aborted by caller" }],
548
+ api: "anthropic-messages",
549
+ provider: "claude-code",
550
+ model,
551
+ usage: { ...ZERO_USAGE },
552
+ stopReason: "aborted",
553
+ timestamp: Date.now(),
554
+ };
555
+ return message;
556
+ }
557
+
521
558
  // ---------------------------------------------------------------------------
522
559
  // SDK options builder
523
560
  // ---------------------------------------------------------------------------
524
561
 
562
+ /**
563
+ * Resolve the Claude Code permission mode for the current run.
564
+ *
565
+ * GSD subagents run underneath a host Claude Code session the user has
566
+ * already consented to, and their work (edits, shell inspection, MCP calls)
567
+ * spans the full workflow toolset. Defaulting the inner SDK to
568
+ * `bypassPermissions` avoids per-tool approval prompts that offer no
569
+ * meaningful safety beyond what the host session and the subagent prompts
570
+ * already enforce. `GSD_CLAUDE_CODE_PERMISSION_MODE` lets security-conscious
571
+ * users opt into a stricter mode (`acceptEdits`, `default`, `plan`).
572
+ *
573
+ * Tradeoff: bypass means a prompt-injection payload read from an untrusted
574
+ * file could trigger tool calls without a second gate. Accepted for GSD
575
+ * because the workflow is explicit user intent and the alternative
576
+ * (#4099) is continuous approval fatigue that blocks real work.
577
+ */
578
+ export async function resolveClaudePermissionMode(
579
+ env: NodeJS.ProcessEnv = process.env,
580
+ ): Promise<"bypassPermissions" | "acceptEdits" | "default" | "plan"> {
581
+ const override = env.GSD_CLAUDE_CODE_PERMISSION_MODE?.trim();
582
+ if (override === "bypassPermissions" || override === "acceptEdits" || override === "default" || override === "plan") {
583
+ return override;
584
+ }
585
+ return "bypassPermissions";
586
+ }
587
+
525
588
  /**
526
589
  * Build the options object passed to the Claude Agent SDK's `query()` call.
527
590
  *
528
591
  * Extracted for testability — callers can verify session persistence,
529
592
  * beta flags, and other configuration without mocking the full SDK.
593
+ *
594
+ * `permissionMode` / `allowDangerouslySkipPermissions` are resolved through
595
+ * {@link resolveClaudePermissionMode} so interactive runs don't silently
596
+ * bypass the SDK's permission gate. Callers that want the old always-bypass
597
+ * behaviour pass `permissionMode: "bypassPermissions"` explicitly.
530
598
  */
531
599
  export function buildSdkOptions(
532
600
  modelId: string,
533
601
  prompt: string,
602
+ overrides?: { permissionMode?: "bypassPermissions" | "acceptEdits" | "default" | "plan" },
534
603
  extraOptions: Record<string, unknown> = {},
535
604
  ): Record<string, unknown> {
536
605
  const mcpServers = buildWorkflowMcpServers();
606
+ const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
537
607
  const disallowedTools = ["AskUserQuestion"];
608
+ // Pre-authorize the safe built-ins and every registered workflow MCP
609
+ // server's tools. `acceptEdits` mode (the interactive default) only
610
+ // auto-approves file edits — Read/Glob/Grep, basic shell inspection, and
611
+ // every `mcp__gsd-workflow__*` call still surface as "This command
612
+ // requires approval" and block GSD actions (#4099).
613
+ const allowedTools = [
614
+ "Read",
615
+ "Write",
616
+ "Edit",
617
+ "Glob",
618
+ "Grep",
619
+ "Bash(ls:*)",
620
+ "Bash(pwd)",
621
+ ...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []),
622
+ ];
538
623
  return {
539
624
  pathToClaudeCodeExecutable: getClaudePath(),
540
625
  model: modelId,
541
626
  includePartialMessages: true,
542
627
  persistSession: true,
543
628
  cwd: process.cwd(),
544
- permissionMode: "bypassPermissions",
545
- allowDangerouslySkipPermissions: true,
629
+ permissionMode,
630
+ allowDangerouslySkipPermissions: permissionMode === "bypassPermissions",
546
631
  settingSources: ["project"],
547
632
  systemPrompt: { type: "preset", preset: "claude_code" },
548
633
  disallowedTools,
634
+ ...(allowedTools.length > 0 ? { allowedTools } : {}),
549
635
  ...(mcpServers ? { mcpServers } : {}),
550
636
  betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
551
637
  ...extraOptions,
@@ -656,6 +742,29 @@ function attachExternalResultsToToolBlocks(
656
742
  }
657
743
  }
658
744
 
745
+ /**
746
+ * Merge tool-call blocks from the active partial-message builder into the
747
+ * running list of intermediate tool calls, preserving order and de-duping
748
+ * by tool-call id. Exposed for testing the F3 fix (final-turn tool calls
749
+ * dropped when `result` arrives without a preceding synthetic `user`).
750
+ */
751
+ export function mergePendingToolCalls(
752
+ intermediate: AssistantMessage["content"],
753
+ pending: AssistantMessage["content"],
754
+ ): AssistantMessage["content"] {
755
+ const alreadyIncluded = new Set<string>();
756
+ for (const block of intermediate) {
757
+ if (block.type === "toolCall") alreadyIncluded.add(block.id);
758
+ }
759
+ for (const block of pending) {
760
+ if (block.type !== "toolCall") continue;
761
+ if (alreadyIncluded.has(block.id)) continue;
762
+ alreadyIncluded.add(block.id);
763
+ intermediate.push(block);
764
+ }
765
+ return intermediate;
766
+ }
767
+
659
768
  // ---------------------------------------------------------------------------
660
769
  // streamSimple implementation
661
770
  // ---------------------------------------------------------------------------
@@ -712,9 +821,11 @@ async function pumpSdkMessages(
712
821
  }
713
822
 
714
823
  const prompt = buildPromptFromContext(context);
824
+ const permissionMode = await resolveClaudePermissionMode();
715
825
  const sdkOpts = buildSdkOptions(
716
826
  modelId,
717
827
  prompt,
828
+ { permissionMode },
718
829
  typeof (options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext === "object"
719
830
  ? {
720
831
  onElicitation: createClaudeCodeElicitationHandler(
@@ -746,7 +857,17 @@ async function pumpSdkMessages(
746
857
  stream.push({ type: "start", partial: initialPartial });
747
858
 
748
859
  for await (const msg of queryResult as AsyncIterable<SDKMessage>) {
749
- if (options?.signal?.aborted) break;
860
+ if (options?.signal?.aborted) {
861
+ // User-initiated cancel — emit an aborted error so the agent
862
+ // loop classifies this as a deliberate stop, not a transient
863
+ // provider failure that should be retried.
864
+ stream.push({
865
+ type: "error",
866
+ reason: "aborted",
867
+ error: makeAbortedMessage(modelId, lastTextContent),
868
+ });
869
+ return;
870
+ }
750
871
 
751
872
  switch (msg.type) {
752
873
  // -- Init --
@@ -857,6 +978,16 @@ async function pumpSdkMessages(
857
978
  // events for proper TUI rendering, followed by the text response.
858
979
  const finalContent: AssistantMessage["content"] = [];
859
980
 
981
+ // If the final turn ended without a synthetic user message
982
+ // (e.g. stop_reason: "tool_use" followed directly by result,
983
+ // or a turn with text but no tool execution), the `builder`
984
+ // still holds toolCall blocks that were never pushed into
985
+ // `intermediateToolBlocks`. Fold them in here so they aren't
986
+ // dropped from the final AssistantMessage.
987
+ if (builder) {
988
+ mergePendingToolCalls(intermediateToolBlocks, builder.message.content);
989
+ }
990
+
860
991
  // Add tool calls from intermediate turns first (renders above text)
861
992
  attachExternalResultsToToolBlocks(intermediateToolBlocks, toolResultsById);
862
993
  finalContent.push(...intermediateToolBlocks);
@@ -1,7 +1,7 @@
1
1
  import { describe, test } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { PartialMessageBuilder } from "../partial-builder.ts";
4
- import type { BetaRawMessageStreamEvent } from "../sdk-types.ts";
3
+ import { mapContentBlock, parseMcpToolName, PartialMessageBuilder } from "../partial-builder.ts";
4
+ import type { BetaContentBlock, BetaRawMessageStreamEvent } from "../sdk-types.ts";
5
5
 
6
6
  describe("PartialMessageBuilder — malformed tool arguments (#2574)", () => {
7
7
  /**
@@ -148,3 +148,92 @@ describe("PartialMessageBuilder — malformed tool arguments (#2574)", () => {
148
148
  }
149
149
  });
150
150
  });
151
+
152
+ describe("parseMcpToolName", () => {
153
+ test("splits mcp__<server>__<tool> into parts", () => {
154
+ assert.deepEqual(
155
+ parseMcpToolName("mcp__gsd-workflow__gsd_plan_milestone"),
156
+ { server: "gsd-workflow", tool: "gsd_plan_milestone" },
157
+ );
158
+ });
159
+
160
+ test("preserves server names containing hyphens", () => {
161
+ assert.deepEqual(
162
+ parseMcpToolName("mcp__my-cool-server__do_thing"),
163
+ { server: "my-cool-server", tool: "do_thing" },
164
+ );
165
+ });
166
+
167
+ test("preserves tool names containing underscores", () => {
168
+ assert.deepEqual(
169
+ parseMcpToolName("mcp__srv__a_b_c_d"),
170
+ { server: "srv", tool: "a_b_c_d" },
171
+ );
172
+ });
173
+
174
+ test("returns null for non-prefixed names", () => {
175
+ assert.equal(parseMcpToolName("Bash"), null);
176
+ assert.equal(parseMcpToolName("gsd_plan_milestone"), null);
177
+ });
178
+
179
+ test("returns null for malformed prefixes", () => {
180
+ assert.equal(parseMcpToolName("mcp__"), null);
181
+ assert.equal(parseMcpToolName("mcp__server"), null);
182
+ assert.equal(parseMcpToolName("mcp__server__"), null);
183
+ assert.equal(parseMcpToolName("mcp____tool"), null);
184
+ });
185
+ });
186
+
187
+ describe("PartialMessageBuilder — MCP tool name normalization", () => {
188
+ test("strips mcp__<server>__ prefix on content_block_start", () => {
189
+ const builder = new PartialMessageBuilder("claude-sonnet-4-20250514");
190
+ const event = builder.handleEvent({
191
+ type: "content_block_start",
192
+ index: 0,
193
+ content_block: {
194
+ type: "tool_use",
195
+ id: "tool_1",
196
+ name: "mcp__gsd-workflow__gsd_plan_milestone",
197
+ input: {},
198
+ },
199
+ } as BetaRawMessageStreamEvent);
200
+
201
+ assert.ok(event, "event should not be null");
202
+ assert.equal(event!.type, "toolcall_start");
203
+ if (event!.type === "toolcall_start") {
204
+ const toolCall = (event.partial.content[event.contentIndex] as any);
205
+ assert.equal(toolCall.name, "gsd_plan_milestone");
206
+ assert.equal(toolCall.mcpServer, "gsd-workflow");
207
+ }
208
+ });
209
+
210
+ test("leaves non-MCP tool names untouched", () => {
211
+ const builder = new PartialMessageBuilder("claude-sonnet-4-20250514");
212
+ const event = builder.handleEvent({
213
+ type: "content_block_start",
214
+ index: 0,
215
+ content_block: { type: "tool_use", id: "tool_1", name: "Bash", input: {} },
216
+ } as BetaRawMessageStreamEvent);
217
+
218
+ assert.ok(event);
219
+ if (event!.type === "toolcall_start") {
220
+ const toolCall = (event.partial.content[event.contentIndex] as any);
221
+ assert.equal(toolCall.name, "Bash");
222
+ assert.equal(toolCall.mcpServer, undefined);
223
+ }
224
+ });
225
+
226
+ test("mapContentBlock strips MCP prefix on full tool_use blocks", () => {
227
+ const block: BetaContentBlock = {
228
+ type: "tool_use",
229
+ id: "tool_2",
230
+ name: "mcp__gsd-workflow__gsd_task_complete",
231
+ input: { taskId: "T001" },
232
+ };
233
+ const mapped = mapContentBlock(block) as any;
234
+ assert.equal(mapped.type, "toolCall");
235
+ assert.equal(mapped.name, "gsd_task_complete");
236
+ assert.equal(mapped.mcpServer, "gsd-workflow");
237
+ assert.deepEqual(mapped.arguments, { taskId: "T001" });
238
+ });
239
+ });
@@ -6,6 +6,9 @@ import { tmpdir } from "node:os";
6
6
  import {
7
7
  makeStreamExhaustedErrorMessage,
8
8
  getResultErrorMessage,
9
+ makeAbortedMessage,
10
+ mergePendingToolCalls,
11
+ resolveClaudePermissionMode,
9
12
  buildPromptFromContext,
10
13
  buildSdkOptions,
11
14
  createClaudeCodeElicitationHandler,
@@ -16,7 +19,7 @@ import {
16
19
  parseClaudeLookupOutput,
17
20
  roundResultToElicitationContent,
18
21
  } from "../stream-adapter.ts";
19
- import type { Context, Message } from "@gsd/pi-ai";
22
+ import type { AssistantMessage, Context, Message } from "@gsd/pi-ai";
20
23
  import type { SDKUserMessage } from "../sdk-types.ts";
21
24
 
22
25
  // ---------------------------------------------------------------------------
@@ -164,6 +167,98 @@ describe("stream-adapter — full context prompt (#2859)", () => {
164
167
  });
165
168
  });
166
169
 
170
+ // ---------------------------------------------------------------------------
171
+ // Bug #4102 — transcript fabrication regression tests
172
+ // ---------------------------------------------------------------------------
173
+
174
+ describe("stream-adapter — no transcript fabrication (#4102)", () => {
175
+ test("buildPromptFromContext never emits forbidden [User]/[Assistant] bracket headers", () => {
176
+ const context: Context = {
177
+ systemPrompt: "You are a helpful assistant.",
178
+ messages: [
179
+ { role: "user", content: "First" } as Message,
180
+ {
181
+ role: "assistant",
182
+ content: [{ type: "text", text: "Second" }],
183
+ api: "anthropic-messages",
184
+ provider: "claude-code",
185
+ model: "claude-sonnet-4-20250514",
186
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
187
+ stopReason: "stop",
188
+ timestamp: Date.now(),
189
+ } as Message,
190
+ { role: "user", content: "Third" } as Message,
191
+ ],
192
+ };
193
+
194
+ const prompt = buildPromptFromContext(context);
195
+
196
+ assert.ok(!prompt.includes("[User]"), "prompt must not include literal [User] bracket header");
197
+ assert.ok(!prompt.includes("[Assistant]"), "prompt must not include literal [Assistant] bracket header");
198
+ assert.ok(!prompt.includes("[System]"), "prompt must not include literal [System] bracket header");
199
+ });
200
+
201
+ test("buildPromptFromContext wraps history in XML-tag structure", () => {
202
+ const context: Context = {
203
+ systemPrompt: "You are helpful.",
204
+ messages: [
205
+ { role: "user", content: "Hello" } as Message,
206
+ {
207
+ role: "assistant",
208
+ content: [{ type: "text", text: "Hi there" }],
209
+ api: "anthropic-messages",
210
+ provider: "claude-code",
211
+ model: "claude-sonnet-4-20250514",
212
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
213
+ stopReason: "stop",
214
+ timestamp: Date.now(),
215
+ } as Message,
216
+ ],
217
+ };
218
+
219
+ const prompt = buildPromptFromContext(context);
220
+
221
+ assert.ok(prompt.includes("<conversation_history>"), "prompt must wrap history in <conversation_history>");
222
+ assert.ok(prompt.includes("</conversation_history>"), "prompt must close <conversation_history>");
223
+ assert.ok(prompt.includes("<user_message>\nHello\n</user_message>"), "user turn must use <user_message> tags");
224
+ assert.ok(prompt.includes("<assistant_message>\nHi there\n</assistant_message>"), "assistant turn must use <assistant_message> tags");
225
+ assert.ok(prompt.includes("<prior_system_context>\nYou are helpful.\n</prior_system_context>"), "system prompt must use <prior_system_context> tags");
226
+ });
227
+
228
+ test("buildPromptFromContext includes a do-not-echo-tags directive as primary instruction", () => {
229
+ const context: Context = {
230
+ messages: [{ role: "user", content: "Anything" } as Message],
231
+ };
232
+
233
+ const prompt = buildPromptFromContext(context);
234
+
235
+ assert.ok(
236
+ prompt.startsWith("Respond only to the final user message"),
237
+ "primary directive must lead the prompt",
238
+ );
239
+ assert.ok(prompt.includes("Do not emit <user_message>"), "directive must forbid emitting user_message tag");
240
+ assert.ok(prompt.includes("<assistant_message>"), "directive must mention assistant_message tag");
241
+ });
242
+
243
+ test("buildPromptFromContext omits <conversation_history> when there are no messages but a system prompt", () => {
244
+ const context: Context = {
245
+ systemPrompt: "Seed",
246
+ messages: [],
247
+ };
248
+
249
+ const prompt = buildPromptFromContext(context);
250
+
251
+ assert.ok(prompt.includes("<prior_system_context>"), "system prompt must still render");
252
+ assert.ok(!prompt.includes("<conversation_history>"), "no history wrapper when messages are empty");
253
+ });
254
+
255
+ test("buildPromptFromContext still returns empty string when context is entirely empty", () => {
256
+ const context: Context = { messages: [] };
257
+ const prompt = buildPromptFromContext(context);
258
+ assert.equal(prompt, "", "empty context must not emit a bare directive");
259
+ });
260
+ });
261
+
167
262
  describe("stream-adapter — Claude Code external tool results", () => {
168
263
  test("extractToolResultsFromSdkUserMessage maps tool_result content to tool payloads", () => {
169
264
  const message: SDKUserMessage = {
@@ -274,6 +369,16 @@ describe("stream-adapter — session persistence (#2859)", () => {
274
369
  assert.equal(srv.env.GSD_PERSIST_WRITE_GATE_STATE, "1");
275
370
  assert.equal(srv.env.GSD_WORKFLOW_PROJECT_ROOT, "/tmp/project");
276
371
  assert.deepEqual(options.disallowedTools, ["AskUserQuestion"]);
372
+ assert.deepEqual(options.allowedTools, [
373
+ "Read",
374
+ "Write",
375
+ "Edit",
376
+ "Glob",
377
+ "Grep",
378
+ "Bash(ls:*)",
379
+ "Bash(pwd)",
380
+ "mcp__gsd-workflow__*",
381
+ ]);
277
382
  } finally {
278
383
  process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
279
384
  process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
@@ -302,6 +407,16 @@ describe("stream-adapter — session persistence (#2859)", () => {
302
407
  const mcpServers = options.mcpServers as Record<string, any>;
303
408
  assert.ok(mcpServers?.["custom-workflow"], "expected custom workflow server config");
304
409
  assert.deepEqual(options.disallowedTools, ["AskUserQuestion"]);
410
+ assert.deepEqual(options.allowedTools, [
411
+ "Read",
412
+ "Write",
413
+ "Edit",
414
+ "Glob",
415
+ "Grep",
416
+ "Bash(ls:*)",
417
+ "Bash(pwd)",
418
+ "mcp__custom-workflow__*",
419
+ ]);
305
420
  } finally {
306
421
  process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
307
422
  process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
@@ -414,7 +529,7 @@ describe("stream-adapter — session persistence (#2859)", () => {
414
529
  delete process.env.GSD_WORKFLOW_MCP_ARGS;
415
530
  delete process.env.GSD_WORKFLOW_MCP_ENV;
416
531
  delete process.env.GSD_WORKFLOW_MCP_CWD;
417
- const options = buildSdkOptions("claude-sonnet-4-20250514", "test", { onElicitation });
532
+ const options = buildSdkOptions("claude-sonnet-4-20250514", "test", undefined, { onElicitation });
418
533
  assert.equal(options.onElicitation, onElicitation);
419
534
  } finally {
420
535
  process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
@@ -680,6 +795,134 @@ describe("stream-adapter — MCP elicitation bridge", () => {
680
795
  });
681
796
  });
682
797
 
798
+ // ---------------------------------------------------------------------------
799
+ // F2 — abort vs stream-exhausted classification
800
+ // ---------------------------------------------------------------------------
801
+
802
+ describe("stream-adapter — abort classification (F2)", () => {
803
+ test("makeAbortedMessage sets stopReason to 'aborted', not 'error'", () => {
804
+ const message = makeAbortedMessage("claude-sonnet-4-6", "");
805
+ assert.equal(message.stopReason, "aborted");
806
+ assert.equal(message.errorMessage, undefined);
807
+ });
808
+
809
+ test("makeAbortedMessage preserves last-seen text content", () => {
810
+ const message = makeAbortedMessage("claude-sonnet-4-6", "partial mid-stream text");
811
+ assert.deepEqual(message.content, [{ type: "text", text: "partial mid-stream text" }]);
812
+ });
813
+
814
+ test("aborted message is distinguishable from stream-exhausted error", () => {
815
+ const aborted = makeAbortedMessage("claude-sonnet-4-6", "");
816
+ const exhausted = makeStreamExhaustedErrorMessage("claude-sonnet-4-6", "");
817
+ assert.notEqual(aborted.stopReason, exhausted.stopReason);
818
+ assert.equal(exhausted.errorMessage, "stream_exhausted_without_result");
819
+ });
820
+ });
821
+
822
+ // ---------------------------------------------------------------------------
823
+ // F3 — final-turn tool calls not dropped
824
+ // ---------------------------------------------------------------------------
825
+
826
+ describe("stream-adapter — final-turn tool-call merge (F3)", () => {
827
+ function toolCall(id: string, name = "bash"): AssistantMessage["content"][number] {
828
+ return { type: "toolCall", id, name, arguments: {} };
829
+ }
830
+
831
+ test("mergePendingToolCalls appends tool calls not already in intermediate", () => {
832
+ const intermediate: AssistantMessage["content"] = [toolCall("tool-1")];
833
+ const pending: AssistantMessage["content"] = [
834
+ toolCall("tool-2"),
835
+ { type: "text", text: "trailing text" },
836
+ ];
837
+ const merged = mergePendingToolCalls(intermediate, pending);
838
+ assert.equal(merged.length, 2);
839
+ assert.equal((merged[0] as any).id, "tool-1");
840
+ assert.equal((merged[1] as any).id, "tool-2");
841
+ });
842
+
843
+ test("mergePendingToolCalls is idempotent across duplicate ids", () => {
844
+ const intermediate: AssistantMessage["content"] = [toolCall("tool-1")];
845
+ const pending: AssistantMessage["content"] = [toolCall("tool-1"), toolCall("tool-2")];
846
+ const merged = mergePendingToolCalls(intermediate, pending);
847
+ assert.equal(merged.length, 2);
848
+ assert.deepEqual(
849
+ merged.map((b) => (b as any).id),
850
+ ["tool-1", "tool-2"],
851
+ );
852
+ });
853
+
854
+ test("mergePendingToolCalls ignores non-toolCall blocks from pending", () => {
855
+ const intermediate: AssistantMessage["content"] = [];
856
+ const pending: AssistantMessage["content"] = [
857
+ { type: "text", text: "hello" },
858
+ { type: "thinking", thinking: "pondering" },
859
+ toolCall("tool-1"),
860
+ ];
861
+ const merged = mergePendingToolCalls(intermediate, pending);
862
+ assert.equal(merged.length, 1);
863
+ assert.equal((merged[0] as any).id, "tool-1");
864
+ });
865
+ });
866
+
867
+ // ---------------------------------------------------------------------------
868
+ // F10 — permission mode is configurable
869
+ // ---------------------------------------------------------------------------
870
+
871
+ describe("stream-adapter — permission mode (F10)", () => {
872
+ // Earlier tests in this file set GSD_WORKFLOW_MCP_* env vars and restore
873
+ // them by reassigning from `prev.*`. When `prev.*` was undefined, node
874
+ // coerces the assignment to the literal string "undefined", which then
875
+ // fails JSON.parse inside buildWorkflowMcpServers. Clear the relevant
876
+ // slots before each permission-mode test so buildSdkOptions doesn't throw.
877
+ function clearWorkflowMcpEnv(): void {
878
+ for (const key of [
879
+ "GSD_WORKFLOW_MCP_COMMAND",
880
+ "GSD_WORKFLOW_MCP_NAME",
881
+ "GSD_WORKFLOW_MCP_ARGS",
882
+ "GSD_WORKFLOW_MCP_ENV",
883
+ "GSD_WORKFLOW_MCP_CWD",
884
+ ]) {
885
+ if (process.env[key] === undefined || process.env[key] === "undefined") {
886
+ delete process.env[key];
887
+ }
888
+ }
889
+ }
890
+
891
+ test("buildSdkOptions defaults to bypassPermissions for backwards compatibility", () => {
892
+ clearWorkflowMcpEnv();
893
+ const opts = buildSdkOptions("claude-sonnet-4-6", "test");
894
+ assert.equal(opts.permissionMode, "bypassPermissions");
895
+ assert.equal(opts.allowDangerouslySkipPermissions, true);
896
+ });
897
+
898
+ test("buildSdkOptions respects explicit acceptEdits override", () => {
899
+ clearWorkflowMcpEnv();
900
+ const opts = buildSdkOptions("claude-sonnet-4-6", "test", { permissionMode: "acceptEdits" });
901
+ assert.equal(opts.permissionMode, "acceptEdits");
902
+ assert.equal(
903
+ opts.allowDangerouslySkipPermissions,
904
+ false,
905
+ "allowDangerouslySkipPermissions must be false for non-bypass modes",
906
+ );
907
+ });
908
+
909
+ test("resolveClaudePermissionMode honours the GSD_CLAUDE_CODE_PERMISSION_MODE env override", async () => {
910
+ const env = { GSD_CLAUDE_CODE_PERMISSION_MODE: "acceptEdits" } as NodeJS.ProcessEnv;
911
+ const mode = await resolveClaudePermissionMode(env);
912
+ assert.equal(mode, "acceptEdits");
913
+ });
914
+
915
+ test("resolveClaudePermissionMode rejects unknown override values (fallback path)", async () => {
916
+ const env = { GSD_CLAUDE_CODE_PERMISSION_MODE: "nonsense" } as NodeJS.ProcessEnv;
917
+ const mode = await resolveClaudePermissionMode(env);
918
+ // Unknown override falls back to auto-detect → either bypass or acceptEdits
919
+ assert.ok(
920
+ mode === "bypassPermissions" || mode === "acceptEdits",
921
+ `expected bypass or acceptEdits, got ${mode}`,
922
+ );
923
+ });
924
+ });
925
+
683
926
  describe("stream-adapter — Windows Claude path lookup (#3770)", () => {
684
927
  test("getClaudeLookupCommand uses where on Windows", () => {
685
928
  assert.equal(getClaudeLookupCommand("win32"), "where claude");