@opengsd/gsd-pi 1.8.1 → 1.9.0

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 (339) hide show
  1. package/README.md +10 -8
  2. package/dist/cli.js +5 -1
  3. package/dist/headless-events.d.ts +2 -1
  4. package/dist/headless-events.js +11 -3
  5. package/dist/headless-milestone-readiness.d.ts +29 -0
  6. package/dist/headless-milestone-readiness.js +161 -0
  7. package/dist/headless.js +25 -2
  8. package/dist/print-mode-exit.d.ts +12 -0
  9. package/dist/print-mode-exit.js +14 -0
  10. package/dist/resources/.managed-resources-content-hash +1 -1
  11. package/dist/resources/GSD-WORKFLOW.md +16 -47
  12. package/dist/resources/extensions/browser-tools/capture.js +2 -77
  13. package/dist/resources/extensions/browser-tools/screenshot-constraints.js +83 -0
  14. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +17 -2
  15. package/dist/resources/extensions/claude-code-cli/turn-assembler.js +30 -2
  16. package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
  17. package/dist/resources/extensions/gsd/auto-dispatch.js +74 -19
  18. package/dist/resources/extensions/gsd/auto-post-unit.js +4 -0
  19. package/dist/resources/extensions/gsd/auto-prompts.js +35 -1
  20. package/dist/resources/extensions/gsd/auto.js +5 -1
  21. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +78 -4
  22. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +84 -26
  23. package/dist/resources/extensions/gsd/command-feedback.js +16 -12
  24. package/dist/resources/extensions/gsd/compat/compat-marker.js +19 -2
  25. package/dist/resources/extensions/gsd/db/command-queue.js +7 -2
  26. package/dist/resources/extensions/gsd/db/engine.js +51 -16
  27. package/dist/resources/extensions/gsd/db/writers/cascades.js +5 -1
  28. package/dist/resources/extensions/gsd/db/writers/status.js +13 -3
  29. package/dist/resources/extensions/gsd/db-base-schema.js +28 -0
  30. package/dist/resources/extensions/gsd/db-migration-steps.js +30 -0
  31. package/dist/resources/extensions/gsd/db-schema-metadata.js +4 -1
  32. package/dist/resources/extensions/gsd/docs/preferences-reference.md +4 -4
  33. package/dist/resources/extensions/gsd/flat-phase-migration.js +156 -21
  34. package/dist/resources/extensions/gsd/gsd-db.js +187 -45
  35. package/dist/resources/extensions/gsd/guided-flow.js +29 -10
  36. package/dist/resources/extensions/gsd/markdown-renderer.js +16 -2
  37. package/dist/resources/extensions/gsd/md-importer.js +102 -14
  38. package/dist/resources/extensions/gsd/migrate/command.js +19 -162
  39. package/dist/resources/extensions/gsd/migrate/execution.js +98 -0
  40. package/dist/resources/extensions/gsd/migrate/plan.js +49 -0
  41. package/dist/resources/extensions/gsd/migrate/planning-writer.js +12 -17
  42. package/dist/resources/extensions/gsd/migrate/presentation.js +64 -0
  43. package/dist/resources/extensions/gsd/parallel-eligibility.js +5 -4
  44. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +28 -43
  45. package/dist/resources/extensions/gsd/paths.js +4 -1
  46. package/dist/resources/extensions/gsd/phase-anchor.js +7 -2
  47. package/dist/resources/extensions/gsd/post-execution-checks.js +4 -4
  48. package/dist/resources/extensions/gsd/prompts/refine-slice.md +2 -2
  49. package/dist/resources/extensions/gsd/safety/evidence-collector.js +26 -1
  50. package/dist/resources/extensions/gsd/safety/evidence-cross-ref.js +30 -5
  51. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.js +27 -13
  52. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.js +13 -3
  53. package/dist/resources/extensions/gsd/stop-notice.js +15 -1
  54. package/dist/resources/extensions/gsd/sync-lock.js +13 -7
  55. package/dist/resources/extensions/gsd/tools/complete-task.js +82 -1
  56. package/dist/resources/extensions/gsd/tools/plan-task.js +13 -15
  57. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +18 -15
  58. package/dist/resources/extensions/gsd/tools/replan-task.js +128 -0
  59. package/dist/resources/extensions/gsd/tools/rework-brief.js +74 -0
  60. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +81 -0
  61. package/dist/resources/extensions/gsd/unit-registry.js +4 -0
  62. package/dist/resources/extensions/gsd/uok/plan-v2.js +19 -3
  63. package/dist/resources/extensions/gsd/worktree-state-projection.js +6 -0
  64. package/dist/resources/extensions/mac-tools/index.js +31 -9
  65. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  66. package/dist/web/standalone/.next/BUILD_ID +1 -1
  67. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  68. package/dist/web/standalone/.next/build-manifest.json +2 -2
  69. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.rsc +2 -2
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  87. package/dist/web/standalone/.next/server/app/index.html +1 -1
  88. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  89. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  91. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  93. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  94. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  96. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  97. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  99. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  100. package/dist/web/standalone/.next/static/css/3b1ceb7abfb9a230.css +1 -0
  101. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  102. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  103. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  104. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  105. package/dist/web/standalone/package.json +0 -1
  106. package/dist/web-mode.js +31 -3
  107. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  108. package/integrations/hermes/pyproject.toml +1 -1
  109. package/package.json +12 -10
  110. package/packages/cloud-mcp-gateway/package.json +2 -2
  111. package/packages/contracts/dist/workflow.d.ts +14 -0
  112. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  113. package/packages/contracts/dist/workflow.js +16 -0
  114. package/packages/contracts/dist/workflow.js.map +1 -1
  115. package/packages/contracts/package.json +1 -1
  116. package/packages/daemon/dist/cloud-config.d.ts +1 -1
  117. package/packages/daemon/dist/cloud-config.d.ts.map +1 -1
  118. package/packages/daemon/dist/cloud-config.js +5 -1
  119. package/packages/daemon/dist/cloud-config.js.map +1 -1
  120. package/packages/daemon/dist/cloud-config.test.js +51 -1
  121. package/packages/daemon/dist/cloud-config.test.js.map +1 -1
  122. package/packages/daemon/dist/local-tool-executor.d.ts.map +1 -1
  123. package/packages/daemon/dist/local-tool-executor.js +2 -0
  124. package/packages/daemon/dist/local-tool-executor.js.map +1 -1
  125. package/packages/daemon/package.json +6 -6
  126. package/packages/gsd-agent-core/package.json +5 -5
  127. package/packages/gsd-agent-modes/package.json +7 -7
  128. package/packages/gsd-cloud/dist/cli.d.ts +5 -0
  129. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -0
  130. package/packages/gsd-cloud/dist/cli.js +160 -0
  131. package/packages/gsd-cloud/dist/cli.js.map +1 -0
  132. package/packages/gsd-cloud/dist/cloud-config.d.ts +19 -0
  133. package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -0
  134. package/packages/gsd-cloud/dist/cloud-config.js +231 -0
  135. package/packages/gsd-cloud/dist/cloud-config.js.map +1 -0
  136. package/packages/gsd-cloud/dist/cloud-config.test.d.ts +2 -0
  137. package/packages/gsd-cloud/dist/cloud-config.test.d.ts.map +1 -0
  138. package/packages/gsd-cloud/dist/cloud-config.test.js +118 -0
  139. package/packages/gsd-cloud/dist/cloud-config.test.js.map +1 -0
  140. package/packages/gsd-cloud/dist/cloud-gateway-lookup.test.d.ts +2 -0
  141. package/packages/gsd-cloud/dist/cloud-gateway-lookup.test.d.ts.map +1 -0
  142. package/packages/gsd-cloud/dist/cloud-gateway-lookup.test.js +37 -0
  143. package/packages/gsd-cloud/dist/cloud-gateway-lookup.test.js.map +1 -0
  144. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +35 -0
  145. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -0
  146. package/packages/gsd-cloud/dist/cloud-runtime.js +258 -0
  147. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -0
  148. package/packages/gsd-cloud/dist/cloud-runtime.test.d.ts +2 -0
  149. package/packages/gsd-cloud/dist/cloud-runtime.test.d.ts.map +1 -0
  150. package/packages/gsd-cloud/dist/cloud-runtime.test.js +88 -0
  151. package/packages/gsd-cloud/dist/cloud-runtime.test.js.map +1 -0
  152. package/packages/gsd-cloud/dist/cloud-token.d.ts +3 -0
  153. package/packages/gsd-cloud/dist/cloud-token.d.ts.map +1 -0
  154. package/packages/gsd-cloud/dist/cloud-token.js +37 -0
  155. package/packages/gsd-cloud/dist/cloud-token.js.map +1 -0
  156. package/packages/gsd-cloud/dist/config.d.ts +17 -0
  157. package/packages/gsd-cloud/dist/config.d.ts.map +1 -0
  158. package/packages/gsd-cloud/dist/config.js +146 -0
  159. package/packages/gsd-cloud/dist/config.js.map +1 -0
  160. package/packages/gsd-cloud/dist/device-flow.d.ts +24 -0
  161. package/packages/gsd-cloud/dist/device-flow.d.ts.map +1 -0
  162. package/packages/gsd-cloud/dist/device-flow.js +128 -0
  163. package/packages/gsd-cloud/dist/device-flow.js.map +1 -0
  164. package/packages/gsd-cloud/dist/device-flow.test.d.ts +2 -0
  165. package/packages/gsd-cloud/dist/device-flow.test.d.ts.map +1 -0
  166. package/packages/gsd-cloud/dist/device-flow.test.js +155 -0
  167. package/packages/gsd-cloud/dist/device-flow.test.js.map +1 -0
  168. package/packages/gsd-cloud/dist/executors/claude-executor.d.ts +6 -0
  169. package/packages/gsd-cloud/dist/executors/claude-executor.d.ts.map +1 -0
  170. package/packages/gsd-cloud/dist/executors/claude-executor.js +15 -0
  171. package/packages/gsd-cloud/dist/executors/claude-executor.js.map +1 -0
  172. package/packages/gsd-cloud/dist/executors/codex-executor.d.ts +6 -0
  173. package/packages/gsd-cloud/dist/executors/codex-executor.d.ts.map +1 -0
  174. package/packages/gsd-cloud/dist/executors/codex-executor.js +15 -0
  175. package/packages/gsd-cloud/dist/executors/codex-executor.js.map +1 -0
  176. package/packages/gsd-cloud/dist/executors/executor.d.ts +28 -0
  177. package/packages/gsd-cloud/dist/executors/executor.d.ts.map +1 -0
  178. package/packages/gsd-cloud/dist/executors/executor.js +11 -0
  179. package/packages/gsd-cloud/dist/executors/executor.js.map +1 -0
  180. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.d.ts +34 -0
  181. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.d.ts.map +1 -0
  182. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.js +179 -0
  183. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.js.map +1 -0
  184. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.test.d.ts +2 -0
  185. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.test.d.ts.map +1 -0
  186. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.test.js +44 -0
  187. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.test.js.map +1 -0
  188. package/packages/gsd-cloud/dist/executors/index.d.ts +17 -0
  189. package/packages/gsd-cloud/dist/executors/index.d.ts.map +1 -0
  190. package/packages/gsd-cloud/dist/executors/index.js +29 -0
  191. package/packages/gsd-cloud/dist/executors/index.js.map +1 -0
  192. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.d.ts +36 -0
  193. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.d.ts.map +1 -0
  194. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.js +176 -0
  195. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.js.map +1 -0
  196. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.test.d.ts +2 -0
  197. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.test.d.ts.map +1 -0
  198. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.test.js +34 -0
  199. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.test.js.map +1 -0
  200. package/packages/gsd-cloud/dist/index.d.ts +7 -0
  201. package/packages/gsd-cloud/dist/index.d.ts.map +1 -0
  202. package/packages/gsd-cloud/dist/index.js +8 -0
  203. package/packages/gsd-cloud/dist/index.js.map +1 -0
  204. package/packages/gsd-cloud/dist/inject-gateway.d.ts +16 -0
  205. package/packages/gsd-cloud/dist/inject-gateway.d.ts.map +1 -0
  206. package/packages/gsd-cloud/dist/inject-gateway.js +29 -0
  207. package/packages/gsd-cloud/dist/inject-gateway.js.map +1 -0
  208. package/packages/gsd-cloud/dist/inject-gateway.test.d.ts +2 -0
  209. package/packages/gsd-cloud/dist/inject-gateway.test.d.ts.map +1 -0
  210. package/packages/gsd-cloud/dist/inject-gateway.test.js +41 -0
  211. package/packages/gsd-cloud/dist/inject-gateway.test.js.map +1 -0
  212. package/packages/gsd-cloud/dist/logger.d.ts +25 -0
  213. package/packages/gsd-cloud/dist/logger.d.ts.map +1 -0
  214. package/packages/gsd-cloud/dist/logger.js +72 -0
  215. package/packages/gsd-cloud/dist/logger.js.map +1 -0
  216. package/packages/gsd-cloud/dist/types.d.ts +51 -0
  217. package/packages/gsd-cloud/dist/types.d.ts.map +1 -0
  218. package/packages/gsd-cloud/dist/types.js +9 -0
  219. package/packages/gsd-cloud/dist/types.js.map +1 -0
  220. package/packages/gsd-cloud/package.json +3 -3
  221. package/packages/mcp-server/README.md +8 -0
  222. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  223. package/packages/mcp-server/dist/workflow-tools.js +64 -2
  224. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  225. package/packages/mcp-server/package.json +4 -4
  226. package/packages/native/package.json +1 -1
  227. package/packages/pi-agent-core/package.json +1 -1
  228. package/packages/pi-ai/dist/models.generated.d.ts +60 -0
  229. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  230. package/packages/pi-ai/dist/models.generated.js +54 -0
  231. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  232. package/packages/pi-ai/dist/providers/amazon-bedrock.js +1 -0
  233. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  234. package/packages/pi-ai/dist/providers/anthropic.js +19 -0
  235. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  236. package/packages/pi-ai/package.json +1 -1
  237. package/packages/pi-coding-agent/package.json +7 -7
  238. package/packages/pi-tui/package.json +2 -2
  239. package/packages/rpc-client/package.json +2 -2
  240. package/pkg/package.json +1 -1
  241. package/src/resources/GSD-WORKFLOW.md +16 -47
  242. package/src/resources/extensions/browser-tools/capture.ts +2 -86
  243. package/src/resources/extensions/browser-tools/screenshot-constraints.ts +94 -0
  244. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +18 -1
  245. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +108 -0
  246. package/src/resources/extensions/claude-code-cli/turn-assembler.ts +30 -2
  247. package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
  248. package/src/resources/extensions/gsd/auto-dispatch.ts +86 -19
  249. package/src/resources/extensions/gsd/auto-post-unit.ts +4 -0
  250. package/src/resources/extensions/gsd/auto-prompts.ts +38 -1
  251. package/src/resources/extensions/gsd/auto.ts +5 -1
  252. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +91 -4
  253. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -24
  254. package/src/resources/extensions/gsd/command-feedback.ts +19 -12
  255. package/src/resources/extensions/gsd/compat/compat-marker.ts +21 -4
  256. package/src/resources/extensions/gsd/db/command-queue.ts +7 -1
  257. package/src/resources/extensions/gsd/db/engine.ts +53 -17
  258. package/src/resources/extensions/gsd/db/writers/cascades.ts +5 -1
  259. package/src/resources/extensions/gsd/db/writers/status.ts +25 -8
  260. package/src/resources/extensions/gsd/db-base-schema.ts +30 -0
  261. package/src/resources/extensions/gsd/db-migration-steps.ts +32 -0
  262. package/src/resources/extensions/gsd/db-schema-metadata.ts +4 -1
  263. package/src/resources/extensions/gsd/docs/preferences-reference.md +4 -4
  264. package/src/resources/extensions/gsd/flat-phase-migration.ts +160 -19
  265. package/src/resources/extensions/gsd/gsd-db.ts +255 -52
  266. package/src/resources/extensions/gsd/guided-flow.ts +39 -10
  267. package/src/resources/extensions/gsd/markdown-renderer.ts +23 -7
  268. package/src/resources/extensions/gsd/md-importer.ts +109 -11
  269. package/src/resources/extensions/gsd/migrate/command.ts +21 -227
  270. package/src/resources/extensions/gsd/migrate/execution.ts +136 -0
  271. package/src/resources/extensions/gsd/migrate/plan.ts +101 -0
  272. package/src/resources/extensions/gsd/migrate/planning-writer.ts +12 -20
  273. package/src/resources/extensions/gsd/migrate/presentation.ts +83 -0
  274. package/src/resources/extensions/gsd/parallel-eligibility.ts +5 -4
  275. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +37 -40
  276. package/src/resources/extensions/gsd/paths.ts +4 -1
  277. package/src/resources/extensions/gsd/phase-anchor.ts +8 -2
  278. package/src/resources/extensions/gsd/post-execution-checks.ts +4 -4
  279. package/src/resources/extensions/gsd/prompts/refine-slice.md +2 -2
  280. package/src/resources/extensions/gsd/safety/evidence-collector.ts +28 -1
  281. package/src/resources/extensions/gsd/safety/evidence-cross-ref.ts +31 -5
  282. package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +30 -13
  283. package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +16 -3
  284. package/src/resources/extensions/gsd/state-reconciliation/types.ts +2 -2
  285. package/src/resources/extensions/gsd/stop-notice.ts +18 -1
  286. package/src/resources/extensions/gsd/sync-lock.ts +14 -6
  287. package/src/resources/extensions/gsd/tests/closeout-git-deferral.test.ts +96 -1
  288. package/src/resources/extensions/gsd/tests/command-feedback.test.ts +12 -0
  289. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +104 -0
  290. package/src/resources/extensions/gsd/tests/complete-task.test.ts +286 -0
  291. package/src/resources/extensions/gsd/tests/db-engine-migrate-guards.test.ts +186 -0
  292. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +29 -0
  293. package/src/resources/extensions/gsd/tests/discuss-routing-fixes.test.ts +123 -0
  294. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +50 -0
  295. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +49 -0
  296. package/src/resources/extensions/gsd/tests/evidence-cross-ref.test.ts +81 -0
  297. package/src/resources/extensions/gsd/tests/evidence-xref-gsd-exec.test.ts +22 -0
  298. package/src/resources/extensions/gsd/tests/external-edit-scoped-import.test.ts +262 -0
  299. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +202 -0
  300. package/src/resources/extensions/gsd/tests/gate-storage.test.ts +29 -0
  301. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +25 -0
  302. package/src/resources/extensions/gsd/tests/import-slice-milestone-completion-preserve.test.ts +116 -0
  303. package/src/resources/extensions/gsd/tests/migrate-plan.test.ts +68 -0
  304. package/src/resources/extensions/gsd/tests/migrate-presentation.test.ts +98 -0
  305. package/src/resources/extensions/gsd/tests/migrate-safety-audit.test.ts +1 -1
  306. package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +55 -0
  307. package/src/resources/extensions/gsd/tests/parallel-eligibility-file-overlap.test.ts +146 -0
  308. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +59 -0
  309. package/src/resources/extensions/gsd/tests/phase-anchor.test.ts +32 -4
  310. package/src/resources/extensions/gsd/tests/plan-task.test.ts +26 -23
  311. package/src/resources/extensions/gsd/tests/planning-writer.test.ts +29 -0
  312. package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +22 -0
  313. package/src/resources/extensions/gsd/tests/reassess-handler.test.ts +50 -0
  314. package/src/resources/extensions/gsd/tests/replan-task-handler.test.ts +103 -0
  315. package/src/resources/extensions/gsd/tests/rework-brief.test.ts +197 -0
  316. package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +21 -0
  317. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +17 -0
  318. package/src/resources/extensions/gsd/tests/sync-lock.test.ts +19 -0
  319. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +34 -0
  320. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +93 -1
  321. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -0
  322. package/src/resources/extensions/gsd/tests/workflow-migration-gates.test.ts +139 -0
  323. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +36 -0
  324. package/src/resources/extensions/gsd/tests/write-gate-seam.test.ts +52 -0
  325. package/src/resources/extensions/gsd/tools/complete-task.ts +120 -0
  326. package/src/resources/extensions/gsd/tools/plan-task.ts +14 -16
  327. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +18 -17
  328. package/src/resources/extensions/gsd/tools/replan-task.ts +160 -0
  329. package/src/resources/extensions/gsd/tools/rework-brief.ts +91 -0
  330. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +92 -0
  331. package/src/resources/extensions/gsd/types.ts +11 -0
  332. package/src/resources/extensions/gsd/unit-registry.ts +4 -0
  333. package/src/resources/extensions/gsd/uok/plan-v2.ts +20 -3
  334. package/src/resources/extensions/gsd/worktree-state-projection.ts +12 -0
  335. package/src/resources/extensions/mac-tools/index.ts +52 -9
  336. package/src/resources/extensions/mac-tools/tests/mac-screenshot-payload.test.cjs +70 -0
  337. package/dist/web/standalone/.next/static/css/18c2fdf261bfc1b7.css +0 -1
  338. /package/dist/web/standalone/.next/static/{UaXRrOYxBpG9QBINKhZkg → BE5XoklToI6lZjRzpXih8}/_buildManifest.js +0 -0
  339. /package/dist/web/standalone/.next/static/{UaXRrOYxBpG9QBINKhZkg → BE5XoklToI6lZjRzpXih8}/_ssgManifest.js +0 -0
@@ -6,58 +6,14 @@
6
6
  */
7
7
 
8
8
  import type { Frame, Page } from "playwright";
9
-
10
- // sharp is an optional native dependency. Load it lazily so that the extension
11
- // can still be loaded on platforms where sharp is unavailable (e.g. bunx on
12
- // Raspberry Pi). constrainScreenshot falls back to returning the raw buffer
13
- // when sharp is not installed, which means screenshots won't be resized but
14
- // the tool remains functional.
15
- let _sharp: typeof import("sharp") | null | undefined;
16
- async function getSharp(): Promise<typeof import("sharp") | null> {
17
- if (_sharp !== undefined) return _sharp;
18
- try {
19
- _sharp = (await import("sharp")).default;
20
- } catch {
21
- _sharp = null;
22
- }
23
- return _sharp;
24
- }
25
-
26
- /**
27
- * Test-only seam: override the cached sharp module. Pass `null` to simulate
28
- * an environment where the sharp native dep is unavailable; pass `undefined`
29
- * to clear the cache and let the next getSharp() call re-import. See
30
- * tests/capture-sharp-optional.test.cjs.
31
- */
32
- export function __setSharpForTesting(
33
- value: typeof import("sharp") | null | undefined,
34
- ): void {
35
- _sharp = value;
36
- }
9
+ import { constrainScreenshot } from "./screenshot-constraints.js";
10
+ export { constrainScreenshot, __setSharpForTesting } from "./screenshot-constraints.js";
37
11
  import type { CompactPageState, CompactSelectorState } from "./state.js";
38
12
  import { formatCompactStateSummary } from "./utils.js";
39
13
 
40
- // Anthropic vision: 1568px is the recommended optimal width. Height is capped
41
- // generously at 8000px so tall full-page screenshots remain readable rather
42
- // than being squished into a square constraint.
43
- //
44
14
  // Override via environment variables:
45
- // SCREENSHOT_MAX_WIDTH=0 → uncap width (use raw resolution)
46
- // SCREENSHOT_MAX_HEIGHT=0 → uncap height
47
15
  // SCREENSHOT_FORMAT=png → lossless PNG for all viewport/fullpage screenshots
48
16
  // SCREENSHOT_QUALITY=100 → max JPEG quality (1-100, default 80)
49
- const MAX_SCREENSHOT_WIDTH = parseScreenshotDimension(process.env.SCREENSHOT_MAX_WIDTH, 1568);
50
- const MAX_SCREENSHOT_HEIGHT = parseScreenshotDimension(process.env.SCREENSHOT_MAX_HEIGHT, 8000);
51
-
52
- /** Parse a dimension env var: positive int = that value, 0 = Infinity (uncapped), absent/invalid = default. */
53
- function parseScreenshotDimension(value: string | undefined, fallback: number): number {
54
- if (value === undefined || value === "") return fallback;
55
- const n = parseInt(value, 10);
56
- if (isNaN(n) || n < 0) return fallback;
57
- if (n === 0) return Infinity;
58
- return n;
59
- }
60
-
61
17
  /** Return the user-configured screenshot format override, or null for default behavior. */
62
18
  export function getScreenshotFormatOverride(): "png" | "jpeg" | null {
63
19
  const fmt = process.env.SCREENSHOT_FORMAT?.toLowerCase();
@@ -176,46 +132,6 @@ export async function postActionSummary(p: Page, target?: Page | Frame): Promise
176
132
  }
177
133
  }
178
134
 
179
- // ---------------------------------------------------------------------------
180
- // Screenshot helpers
181
- // ---------------------------------------------------------------------------
182
-
183
- /**
184
- * Constrain screenshot dimensions for the Anthropic vision API.
185
- * Width is capped at 1568px (optimal) and height at 8000px, each
186
- * independently, using `fit: "inside"` so aspect ratio is preserved.
187
- * Small images are never upscaled.
188
- *
189
- * `page` parameter is retained for ToolDeps signature stability (D008)
190
- * but is no longer used — all processing is server-side via sharp.
191
- */
192
- export async function constrainScreenshot(
193
- _page: Page,
194
- buffer: Buffer,
195
- mimeType: string,
196
- quality: number,
197
- ): Promise<Buffer> {
198
- const sharp = await getSharp();
199
- if (!sharp) return buffer;
200
-
201
- const meta = await sharp(buffer).metadata();
202
- const width = meta.width;
203
- const height = meta.height;
204
-
205
- if (width === undefined || height === undefined) return buffer;
206
- if (width <= MAX_SCREENSHOT_WIDTH && height <= MAX_SCREENSHOT_HEIGHT) return buffer;
207
-
208
- const resizer = sharp(buffer).resize(MAX_SCREENSHOT_WIDTH, MAX_SCREENSHOT_HEIGHT, {
209
- fit: "inside",
210
- withoutEnlargement: true,
211
- });
212
-
213
- if (mimeType === "image/png") {
214
- return Buffer.from(await resizer.png().toBuffer());
215
- }
216
- return Buffer.from(await resizer.jpeg({ quality }).toBuffer());
217
- }
218
-
219
135
  /** Capture a JPEG screenshot for error debugging. Returns base64 or null. */
220
136
  export async function captureErrorScreenshot(p: Page | null): Promise<{ data: string; mimeType: string } | null> {
221
137
  if (!p) return null;
@@ -0,0 +1,94 @@
1
+ import type { Page } from "playwright";
2
+
3
+ // sharp is an optional native dependency. Load it lazily so that screenshot
4
+ // emitters still work on platforms where sharp is unavailable (e.g. bunx on
5
+ // Raspberry Pi). constrainScreenshot falls back to returning the raw buffer
6
+ // when sharp is not installed.
7
+ let _sharp: typeof import("sharp") | null | undefined;
8
+ async function getSharp(): Promise<typeof import("sharp") | null> {
9
+ if (_sharp !== undefined) return _sharp;
10
+ try {
11
+ _sharp = (await import("sharp")).default;
12
+ } catch {
13
+ _sharp = null;
14
+ }
15
+ return _sharp;
16
+ }
17
+
18
+ /**
19
+ * Test-only seam: override the cached sharp module. Pass `null` to simulate
20
+ * an environment where the sharp native dep is unavailable; pass `undefined`
21
+ * to clear the cache and let the next getSharp() call re-import. See
22
+ * tests/capture-sharp-optional.test.cjs.
23
+ */
24
+ export function __setSharpForTesting(
25
+ value: typeof import("sharp") | null | undefined,
26
+ ): void {
27
+ _sharp = value;
28
+ }
29
+
30
+ // Anthropic vision: 1568px is the recommended optimal width. Height is capped
31
+ // generously at 8000px so tall full-page screenshots remain readable rather
32
+ // than being squished into a square constraint.
33
+ //
34
+ // Override via environment variables:
35
+ // SCREENSHOT_MAX_WIDTH=0 -> uncap width (use raw resolution)
36
+ // SCREENSHOT_MAX_HEIGHT=0 -> uncap height
37
+ export const MAX_SCREENSHOT_WIDTH = parseScreenshotDimension(process.env.SCREENSHOT_MAX_WIDTH, 1568);
38
+ export const MAX_SCREENSHOT_HEIGHT = parseScreenshotDimension(process.env.SCREENSHOT_MAX_HEIGHT, 8000);
39
+
40
+ /** Parse a dimension env var: positive int = that value, 0 = Infinity (uncapped), absent/invalid = default. */
41
+ function parseScreenshotDimension(value: string | undefined, fallback: number): number {
42
+ if (value === undefined || value === "") return fallback;
43
+ const n = parseInt(value, 10);
44
+ if (isNaN(n) || n < 0) return fallback;
45
+ if (n === 0) return Infinity;
46
+ return n;
47
+ }
48
+
49
+ export async function getScreenshotBufferDimensions(
50
+ buffer: Buffer,
51
+ ): Promise<{ width: number; height: number } | null> {
52
+ const sharp = await getSharp();
53
+ if (!sharp) return null;
54
+
55
+ const meta = await sharp(buffer).metadata();
56
+ if (meta.width === undefined || meta.height === undefined) return null;
57
+ return { width: meta.width, height: meta.height };
58
+ }
59
+
60
+ /**
61
+ * Constrain screenshot dimensions for the Anthropic vision API.
62
+ * Width is capped at 1568px (optimal) and height at 8000px, each
63
+ * independently, using `fit: "inside"` so aspect ratio is preserved.
64
+ * Small images are never upscaled.
65
+ *
66
+ * `page` parameter is retained for ToolDeps signature stability (D008)
67
+ * but is no longer used — all processing is server-side via sharp.
68
+ */
69
+ export async function constrainScreenshot(
70
+ _page: Page | null,
71
+ buffer: Buffer,
72
+ mimeType: string,
73
+ quality: number,
74
+ ): Promise<Buffer> {
75
+ const sharp = await getSharp();
76
+ if (!sharp) return buffer;
77
+
78
+ const dimensions = await getScreenshotBufferDimensions(buffer);
79
+ const width = dimensions?.width;
80
+ const height = dimensions?.height;
81
+
82
+ if (width === undefined || height === undefined) return buffer;
83
+ if (width <= MAX_SCREENSHOT_WIDTH && height <= MAX_SCREENSHOT_HEIGHT) return buffer;
84
+
85
+ const resizer = sharp(buffer).resize(MAX_SCREENSHOT_WIDTH, MAX_SCREENSHOT_HEIGHT, {
86
+ fit: "inside",
87
+ withoutEnlargement: true,
88
+ });
89
+
90
+ if (mimeType === "image/png") {
91
+ return Buffer.from(await resizer.png().toBuffer());
92
+ }
93
+ return Buffer.from(await resizer.jpeg({ quality }).toBuffer());
94
+ }
@@ -22,6 +22,7 @@ import type {
22
22
  import type { ExtensionUIContext } from "@gsd/pi-coding-agent";
23
23
  import { EventStream } from "@gsd/pi-ai";
24
24
  import { execSync } from "node:child_process";
25
+ import { createHash } from "node:crypto";
25
26
  import { existsSync, readFileSync } from "node:fs";
26
27
  import { homedir } from "node:os";
27
28
  import { createRequire } from "node:module";
@@ -583,6 +584,7 @@ function inferMimeTypeFromDataUri(value: string): string | null {
583
584
  /** Collect all base64 image blocks from user messages in the context for inclusion in the SDK prompt. */
584
585
  export function extractImageBlocksFromContext(context: Context): SDKInputImageBlock[] {
585
586
  const imageBlocks: SDKInputImageBlock[] = [];
587
+ const seenImageKeys = new Set<string>();
586
588
 
587
589
  for (const msg of context.messages) {
588
590
  if (msg.role !== "user" || !Array.isArray(msg.content)) continue;
@@ -597,12 +599,17 @@ export function extractImageBlocksFromContext(context: Context): SDKInputImageBl
597
599
  : inferMimeTypeFromDataUri(block.data);
598
600
  if (!mimeType) continue;
599
601
 
602
+ const data = stripDataUriPrefix(block.data);
603
+ const imageKey = `${mimeType}\0${createHash("sha1").update(data).digest("hex")}`;
604
+ if (seenImageKeys.has(imageKey)) continue;
605
+ seenImageKeys.add(imageKey);
606
+
600
607
  imageBlocks.push({
601
608
  type: "image",
602
609
  source: {
603
610
  type: "base64",
604
611
  media_type: mimeType,
605
- data: stripDataUriPrefix(block.data),
612
+ data,
606
613
  },
607
614
  });
608
615
  }
@@ -2252,6 +2259,7 @@ export function streamViaClaudeCode(
2252
2259
  interface SdkAttemptMessageState {
2253
2260
  builder: PartialMessageBuilder | null;
2254
2261
  intermediateToolBlocks: AssistantMessage["content"];
2262
+ intermediateTextBlocks: AssistantMessage["content"];
2255
2263
  toolResultsById: Map<string, ExternalToolResultPayload>;
2256
2264
  toolCompletionTargetsById: Map<string, { partial: AssistantMessage; contentIndex: number }>;
2257
2265
  emittedExternalToolResultIds: Set<string>;
@@ -2261,6 +2269,7 @@ function createSdkAttemptMessageState(): SdkAttemptMessageState {
2261
2269
  return {
2262
2270
  builder: null,
2263
2271
  intermediateToolBlocks: [],
2272
+ intermediateTextBlocks: [],
2264
2273
  toolResultsById: new Map<string, ExternalToolResultPayload>(),
2265
2274
  toolCompletionTargetsById: new Map<string, { partial: AssistantMessage; contentIndex: number }>(),
2266
2275
  emittedExternalToolResultIds: new Set<string>(),
@@ -2402,6 +2411,7 @@ async function pumpSdkMessages(
2402
2411
  let {
2403
2412
  builder,
2404
2413
  intermediateToolBlocks,
2414
+ intermediateTextBlocks,
2405
2415
  toolResultsById,
2406
2416
  toolCompletionTargetsById,
2407
2417
  emittedExternalToolResultIds,
@@ -2542,8 +2552,14 @@ async function pumpSdkMessages(
2542
2552
  for (const [contentIndex, block] of builder.message.content.entries()) {
2543
2553
  if (block.type === "text" && block.text) {
2544
2554
  lastTextContent = block.text;
2555
+ // Accumulate completed prose in order — a multi-question
2556
+ // turn commits [prose][elicitation] segments across several
2557
+ // synthetic-user boundaries, and overwriting a single
2558
+ // scalar would drop every explanation but the last.
2559
+ intermediateTextBlocks.push({ type: "text", text: block.text });
2545
2560
  } else if (block.type === "thinking" && block.thinking) {
2546
2561
  lastThinkingContent = block.thinking;
2562
+ intermediateTextBlocks.push({ type: "thinking", thinking: block.thinking });
2547
2563
  } else if (block.type === "toolCall" || block.type === "serverToolUse") {
2548
2564
  // Collect tool blocks for externalToolExecution rendering
2549
2565
  intermediateToolBlocks.push(block);
@@ -2641,6 +2657,7 @@ async function pumpSdkMessages(
2641
2657
  const result = msg as SDKResultMessage;
2642
2658
  const finalContent = buildFinalAssistantContent({
2643
2659
  intermediateToolBlocks,
2660
+ intermediateTextBlocks,
2644
2661
  pendingContent: builder?.message.content,
2645
2662
  toolResultsById,
2646
2663
  lastThinkingContent,
@@ -363,6 +363,53 @@ describe("stream-adapter — image prompt forwarding (#4183)", () => {
363
363
  ]);
364
364
  });
365
365
 
366
+ test("extractImageBlocksFromContext deduplicates byte-identical image parts", () => {
367
+ const context: Context = {
368
+ messages: [
369
+ {
370
+ role: "user",
371
+ content: [
372
+ {
373
+ type: "image",
374
+ data: "data:image/png;base64,abc123",
375
+ mimeType: "image/png",
376
+ },
377
+ {
378
+ type: "image",
379
+ data: "abc123",
380
+ mimeType: "image/png",
381
+ },
382
+ {
383
+ type: "image",
384
+ data: "def456",
385
+ mimeType: "image/png",
386
+ },
387
+ ],
388
+ } as Message,
389
+ ],
390
+ };
391
+
392
+ const imageBlocks = extractImageBlocksFromContext(context);
393
+ assert.deepEqual(imageBlocks, [
394
+ {
395
+ type: "image",
396
+ source: {
397
+ type: "base64",
398
+ media_type: "image/png",
399
+ data: "abc123",
400
+ },
401
+ },
402
+ {
403
+ type: "image",
404
+ source: {
405
+ type: "base64",
406
+ media_type: "image/png",
407
+ data: "def456",
408
+ },
409
+ },
410
+ ]);
411
+ });
412
+
366
413
  test("buildSdkQueryPrompt returns plain string when no images exist in context", () => {
367
414
  const context: Context = {
368
415
  messages: [{ role: "user", content: "hello" } as Message],
@@ -1365,6 +1412,67 @@ describe("stream-adapter — Claude Code external tool results", () => {
1365
1412
  });
1366
1413
  assert.deepEqual(finalContent[1], { type: "text", text: "Read complete." });
1367
1414
  });
1415
+
1416
+ test("buildFinalAssistantContent keeps every prose segment across successive elicitation boundaries (#1284)", () => {
1417
+ // A multi-question /gsd discuss turn: [prose A][Q1][prose B][Q2][prose C].
1418
+ // Prose A and B are committed at synthetic-user boundaries (intermediate),
1419
+ // prose C is still in the builder (pending) when `result` arrives.
1420
+ const finalContent = buildFinalAssistantContent({
1421
+ intermediateToolBlocks: [
1422
+ { type: "toolCall", id: "q1", name: "ask_user_questions", arguments: {} } as any,
1423
+ { type: "toolCall", id: "q2", name: "ask_user_questions", arguments: {} } as any,
1424
+ ],
1425
+ intermediateTextBlocks: [
1426
+ { type: "text", text: "prose A" },
1427
+ { type: "text", text: "prose B" },
1428
+ ],
1429
+ pendingContent: [{ type: "text", text: "prose C" }],
1430
+ toolResultsById: new Map(),
1431
+ });
1432
+
1433
+ const textBlocks = finalContent.filter((b) => b.type === "text").map((b) => (b as any).text);
1434
+ assert.deepEqual(textBlocks, ["prose A", "prose B", "prose C"]);
1435
+ });
1436
+
1437
+ test("buildFinalAssistantContent does not duplicate the last prose when the turn ends on an elicitation (#1284)", () => {
1438
+ // [prose A][Q1][prose B][Q2] — builder was reset at the final boundary, so
1439
+ // pendingContent is empty and lastTextContent still holds "prose B".
1440
+ const finalContent = buildFinalAssistantContent({
1441
+ intermediateToolBlocks: [
1442
+ { type: "toolCall", id: "q1", name: "ask_user_questions", arguments: {} } as any,
1443
+ { type: "toolCall", id: "q2", name: "ask_user_questions", arguments: {} } as any,
1444
+ ],
1445
+ intermediateTextBlocks: [
1446
+ { type: "text", text: "prose A" },
1447
+ { type: "text", text: "prose B" },
1448
+ ],
1449
+ toolResultsById: new Map(),
1450
+ lastTextContent: "prose B",
1451
+ });
1452
+
1453
+ const textBlocks = finalContent.filter((b) => b.type === "text").map((b) => (b as any).text);
1454
+ assert.deepEqual(textBlocks, ["prose A", "prose B"]);
1455
+ });
1456
+
1457
+ test("buildFinalAssistantContent emits scalar fallback prose/thinking not captured in the accumulator", () => {
1458
+ // A non-streaming `assistant` SDK message sets lastText/lastThinkingContent
1459
+ // only (never pushed to intermediateTextBlocks). With pendingContent empty
1460
+ // and intermediate blocks present, the fallback must still be emitted.
1461
+ const finalContent = buildFinalAssistantContent({
1462
+ intermediateToolBlocks: [
1463
+ { type: "toolCall", id: "q1", name: "ask_user_questions", arguments: {} } as any,
1464
+ ],
1465
+ intermediateTextBlocks: [{ type: "text", text: "prose A" }],
1466
+ toolResultsById: new Map(),
1467
+ lastThinkingContent: "final thinking",
1468
+ lastTextContent: "final prose",
1469
+ });
1470
+
1471
+ const textBlocks = finalContent.filter((b) => b.type === "text").map((b) => (b as any).text);
1472
+ const thinkingBlocks = finalContent.filter((b) => b.type === "thinking").map((b) => (b as any).thinking);
1473
+ assert.deepEqual(textBlocks, ["prose A", "final prose"]);
1474
+ assert.deepEqual(thinkingBlocks, ["final thinking"]);
1475
+ });
1368
1476
  });
1369
1477
 
1370
1478
  describe("claude-code-cli — Claude Fable 5 Opus-tier support", () => {
@@ -244,6 +244,7 @@ export function shouldSuppressDuplicateToolUnavailableBlock(
244
244
  */
245
245
  export function buildFinalAssistantContent(params: {
246
246
  intermediateToolBlocks: AssistantMessage["content"];
247
+ intermediateTextBlocks?: AssistantMessage["content"];
247
248
  pendingContent?: AssistantMessage["content"];
248
249
  toolResultsById: ReadonlyMap<string, ExternalToolResultPayload>;
249
250
  lastThinkingContent?: string;
@@ -259,6 +260,15 @@ export function buildFinalAssistantContent(params: {
259
260
  const finalContent: AssistantMessage["content"] = mergedToolBlocks.filter(
260
261
  (block) => !shouldSuppressDuplicateToolUnavailableBlock(block, mergedToolBlocks),
261
262
  );
263
+ // Emit prose/thinking captured at earlier turn-boundaries (each successive
264
+ // `ask_user_questions` elicitation completes a turn) before the final
265
+ // segment. Capturing these in an ordered accumulator — symmetric with
266
+ // `intermediateToolBlocks` — is what keeps intermediate explanations from
267
+ // being collapsed to a single overwritten scalar and silently dropped.
268
+ const intermediateTextBlocks = params.intermediateTextBlocks ?? [];
269
+ for (const block of intermediateTextBlocks) {
270
+ finalContent.push(block);
271
+ }
262
272
  if (params.pendingContent && params.pendingContent.length > 0) {
263
273
  for (const block of params.pendingContent) {
264
274
  if (block.type === "text" || block.type === "thinking") {
@@ -266,11 +276,29 @@ export function buildFinalAssistantContent(params: {
266
276
  }
267
277
  }
268
278
  } else {
279
+ // No builder content survived to this turn boundary, so fall back to the
280
+ // last scalar thinking/text captured off the stream. When the turn ends
281
+ // on an elicitation these scalars just mirror the final intermediate
282
+ // block (they are overwritten at each synthetic-user boundary), so only
283
+ // emit them when they carry content not already accumulated — otherwise
284
+ // a non-streaming `assistant` message's prose/thinking is silently lost.
285
+ const lastBlockOfType = (type: "text" | "thinking") => {
286
+ for (let i = intermediateTextBlocks.length - 1; i >= 0; i--) {
287
+ if (intermediateTextBlocks[i].type === type) return intermediateTextBlocks[i];
288
+ }
289
+ return undefined;
290
+ };
269
291
  if (params.lastThinkingContent) {
270
- finalContent.push({ type: "thinking", thinking: params.lastThinkingContent });
292
+ const lastThinking = lastBlockOfType("thinking") as { thinking?: string } | undefined;
293
+ if (lastThinking?.thinking !== params.lastThinkingContent) {
294
+ finalContent.push({ type: "thinking", thinking: params.lastThinkingContent });
295
+ }
271
296
  }
272
297
  if (params.lastTextContent) {
273
- finalContent.push({ type: "text", text: params.lastTextContent });
298
+ const lastText = lastBlockOfType("text") as { text?: string } | undefined;
299
+ if (lastText?.text !== params.lastTextContent) {
300
+ finalContent.push({ type: "text", text: params.lastTextContent });
301
+ }
274
302
  }
275
303
  }
276
304
 
@@ -297,9 +297,9 @@ export function diagnoseExpectedArtifact(
297
297
  }
298
298
  return `${relSliceFile(base, mid, sid!, "RESEARCH")} (slice research)`;
299
299
  case "plan-slice":
300
- return `${relSliceFile(base, mid, sid!, "PLAN")} plus tasks/T##-PLAN.md files (slice plan and task plans)`;
300
+ return `${relSliceFile(base, mid, sid!, "PLAN")} with embedded task plans`;
301
301
  case "refine-slice":
302
- return `${relSliceFile(base, mid, sid!, "PLAN")} plus tasks/T##-PLAN.md files (refined slice plan and task plans)`;
302
+ return `${relSliceFile(base, mid, sid!, "PLAN")} with embedded refined task plans`;
303
303
  case "execute-task": {
304
304
  return `Task ${tid} marked [x] in ${relSliceFile(base, mid, sid!, "PLAN")} + summary written`;
305
305
  }
@@ -393,7 +393,8 @@ function resolveArtifactBasePath(
393
393
  ): string {
394
394
  if (
395
395
  session?.basePath &&
396
- session.currentMilestoneId === mid &&
396
+ session.currentMilestoneId &&
397
+ milestoneIdsDispatchCompatible(session.currentMilestoneId, mid) &&
397
398
  existsSync(session.basePath)
398
399
  ) {
399
400
  return session.basePath;
@@ -422,6 +423,28 @@ function normalizeMilestoneScope(value: string | null | undefined): string | nul
422
423
  return trimmed;
423
424
  }
424
425
 
426
+ function dispatchMilestoneIdentity(value: string | null | undefined): { baseId: string; hasSuffix: boolean } | null {
427
+ const normalized = normalizeMilestoneScope(value);
428
+ const match = normalized?.match(/^(M\d{3})(?:-[a-z0-9]{6})?$/);
429
+ if (!match) return null;
430
+ return { baseId: match[1]!, hasSuffix: normalized !== match[1] };
431
+ }
432
+
433
+ function isBareSuffixedMilestoneAlias(left: string, right: string): boolean {
434
+ const leftId = dispatchMilestoneIdentity(left);
435
+ const rightId = dispatchMilestoneIdentity(right);
436
+ return Boolean(
437
+ leftId &&
438
+ rightId &&
439
+ leftId.baseId === rightId.baseId &&
440
+ leftId.hasSuffix !== rightId.hasSuffix,
441
+ );
442
+ }
443
+
444
+ function milestoneIdsDispatchCompatible(left: string, right: string): boolean {
445
+ return left === right || isBareSuffixedMilestoneAlias(left, right);
446
+ }
447
+
425
448
  function resolveDispatchMilestoneScope(
426
449
  ctx: DispatchContext,
427
450
  ): { id: string; source: string } | null {
@@ -439,6 +462,37 @@ function resolveDispatchMilestoneScope(
439
462
  return null;
440
463
  }
441
464
 
465
+ function resolveEffectiveDispatchMilestoneId(
466
+ ctx: DispatchContext,
467
+ scopedMilestone: { id: string; source: string } | null,
468
+ ): string {
469
+ if (scopedMilestone && isBareSuffixedMilestoneAlias(ctx.mid, scopedMilestone.id)) {
470
+ return dispatchMilestoneIdentity(scopedMilestone.id)?.hasSuffix ? scopedMilestone.id : ctx.mid;
471
+ }
472
+
473
+ const activeMid = ctx.state.activeMilestone?.id;
474
+ if (activeMid && isBareSuffixedMilestoneAlias(ctx.mid, activeMid)) {
475
+ return dispatchMilestoneIdentity(activeMid)?.hasSuffix ? activeMid : ctx.mid;
476
+ }
477
+
478
+ return ctx.mid;
479
+ }
480
+
481
+ function withEffectiveDispatchMilestone(ctx: DispatchContext, effectiveMid: string): DispatchContext {
482
+ if (effectiveMid === ctx.mid) return ctx;
483
+ const activeMilestone = ctx.state.activeMilestone;
484
+ const state = activeMilestone && milestoneIdsDispatchCompatible(activeMilestone.id, effectiveMid)
485
+ ? {
486
+ ...ctx.state,
487
+ activeMilestone: {
488
+ ...activeMilestone,
489
+ id: effectiveMid,
490
+ },
491
+ }
492
+ : ctx.state;
493
+ return { ...ctx, mid: effectiveMid, state };
494
+ }
495
+
442
496
  function hasMilestonePassedDiscuss(basePath: string, mid: string): boolean {
443
497
  if (isDbAvailable()) {
444
498
  try {
@@ -732,14 +786,21 @@ export const DISPATCH_RULES: DispatchRule[] = [
732
786
  // Fire BEFORE the execution-entry phase rules so we redispatch to
733
787
  // `discuss-milestone` instead of hitting the plan-v2 gate.
734
788
  name: "execution-entry phase (no context) → discuss-milestone",
735
- match: async ({ state, mid, midTitle, basePath, prefs, structuredQuestionsAvailable }) => {
789
+ match: async ({ state, mid, midTitle, basePath, session, prefs, structuredQuestionsAvailable }) => {
736
790
  if (!EXECUTION_ENTRY_PHASES.has(state.phase)) return null;
737
791
  if (!MILESTONE_ID_RE.test(mid)) return null;
738
792
  if (isRegistryMilestoneComplete(state, mid)) return null;
739
- if (hasMilestonePassedDiscuss(basePath, mid)) return null;
793
+ // Resolve discuss/context artifacts against the active session worktree,
794
+ // mirroring the executing rules below. For a suffixed-worktree milestone
795
+ // the CONTEXT and slice plans live under the worktree, not the project
796
+ // root; checking the raw project-root basePath misreads the milestone as
797
+ // "never discussed" and re-dispatches discuss-milestone after task
798
+ // closeout instead of continuing execution (#1317).
799
+ const artifactBasePath = resolveArtifactBasePath(basePath, mid, session);
800
+ if (hasMilestonePassedDiscuss(artifactBasePath, mid)) return null;
740
801
  // Align with the plan-v2 gate's lookup semantics: whitespace-only counts
741
802
  // as missing, and an auto worktree may fall back to GSD_PROJECT_ROOT.
742
- if (hasFinalizedMilestoneContext(basePath, mid)) return null;
803
+ if (hasFinalizedMilestoneContext(artifactBasePath, mid)) return null;
743
804
  // H6 fix (#4973): non-deep auto-mode has no human to answer the
744
805
  // depth-verification question, so pre-marking avoids a write-gate
745
806
  // deadlock. Deep planning is still user-driven even inside auto-mode,
@@ -1095,10 +1156,13 @@ export const DISPATCH_RULES: DispatchRule[] = [
1095
1156
  },
1096
1157
  {
1097
1158
  name: "pre-planning (no context) → discuss-milestone",
1098
- match: async ({ state, mid, midTitle, basePath, prefs, structuredQuestionsAvailable }) => {
1159
+ match: async ({ state, mid, midTitle, basePath, prefs, session, structuredQuestionsAvailable }) => {
1099
1160
  if (state.phase !== "pre-planning") return null;
1100
1161
  if (isRegistryMilestoneComplete(state, mid)) return null;
1101
- const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
1162
+ const contextBasePath = resolveWorktreeProjectRoot(basePath, session?.originalBasePath);
1163
+ const contextFile =
1164
+ resolveMilestoneFile(basePath, mid, "CONTEXT") ??
1165
+ (contextBasePath !== basePath ? resolveMilestoneFile(contextBasePath, mid, "CONTEXT") : null);
1102
1166
  const hasContext = !!(contextFile && (await loadFile(contextFile)));
1103
1167
  if (hasContext) return null; // fall through to next rule
1104
1168
  if (prefs?.planning_depth === "deep") return null;
@@ -1908,36 +1972,39 @@ function applyLanguageDirectiveToDispatch(
1908
1972
  export async function resolveDispatch(
1909
1973
  ctx: DispatchContext,
1910
1974
  ): Promise<DispatchAction> {
1911
- if (ctx.mid && isDbAvailable()) {
1912
- const milestone = getMilestone(ctx.mid);
1975
+ const scopedMilestone = resolveDispatchMilestoneScope(ctx);
1976
+ const effectiveMid = resolveEffectiveDispatchMilestoneId(ctx, scopedMilestone);
1977
+ const dispatchCtx = withEffectiveDispatchMilestone(ctx, effectiveMid);
1978
+
1979
+ if (dispatchCtx.mid && isDbAvailable()) {
1980
+ const milestone = getMilestone(dispatchCtx.mid);
1913
1981
  if (milestone && isClosedStatus(milestone.status)) {
1914
1982
  return {
1915
1983
  action: "stop",
1916
1984
  reason:
1917
- `Milestone ${ctx.mid} is closed (status: ${milestone.status}); auto-mode will not reopen or recover it implicitly. ` +
1985
+ `Milestone ${dispatchCtx.mid} is closed (status: ${milestone.status}); auto-mode will not reopen or recover it implicitly. ` +
1918
1986
  "Use an explicit reopen command before planning or executing more work for this milestone.",
1919
1987
  level: "warning",
1920
1988
  };
1921
1989
  }
1922
1990
  }
1923
1991
 
1924
- const activeMid = ctx.state.activeMilestone?.id;
1925
- if (activeMid && ctx.mid !== activeMid) {
1992
+ const activeMid = dispatchCtx.state.activeMilestone?.id;
1993
+ if (activeMid && !milestoneIdsDispatchCompatible(dispatchCtx.mid, activeMid)) {
1926
1994
  return {
1927
1995
  action: "stop",
1928
1996
  reason:
1929
- `Dispatch milestone mismatch: context mid "${ctx.mid}" does not match active milestone "${activeMid}". ` +
1997
+ `Dispatch milestone mismatch: context mid "${dispatchCtx.mid}" does not match active milestone "${activeMid}". ` +
1930
1998
  "This usually means a project-level deep setup pseudo-id leaked into milestone dispatch; rerun /gsd auto after setup state is reconciled.",
1931
1999
  level: "warning",
1932
2000
  };
1933
2001
  }
1934
2002
 
1935
- const scopedMilestone = resolveDispatchMilestoneScope(ctx);
1936
- if (scopedMilestone && ctx.mid !== scopedMilestone.id) {
2003
+ if (scopedMilestone && !milestoneIdsDispatchCompatible(dispatchCtx.mid, scopedMilestone.id)) {
1937
2004
  return {
1938
2005
  action: "stop",
1939
2006
  reason:
1940
- `Dispatch milestone mismatch: context mid "${ctx.mid}" does not match ${scopedMilestone.source} "${scopedMilestone.id}". ` +
2007
+ `Dispatch milestone mismatch: context mid "${dispatchCtx.mid}" does not match ${scopedMilestone.source} "${scopedMilestone.id}". ` +
1941
2008
  "The active worktree/session and derived project state disagree; recover, park, or discard the stranded milestone before continuing.",
1942
2009
  level: "warning",
1943
2010
  };
@@ -1946,10 +2013,10 @@ export async function resolveDispatch(
1946
2013
  // Delegate to registry when available
1947
2014
  try {
1948
2015
  const registry = getRegistry();
1949
- const action = annotateBackgroundable(await registry.evaluateDispatch(ctx));
2016
+ const action = annotateBackgroundable(await registry.evaluateDispatch(dispatchCtx));
1950
2017
  if (
1951
2018
  action.action === "dispatch" &&
1952
- ctx.session?.exhaustedVerificationUnits?.has(`${action.unitType}:${action.unitId}`)
2019
+ dispatchCtx.session?.exhaustedVerificationUnits?.has(`${action.unitType}:${action.unitId}`)
1953
2020
  ) {
1954
2021
  return {
1955
2022
  action: "stop",
@@ -1964,13 +2031,13 @@ export async function resolveDispatch(
1964
2031
  }
1965
2032
 
1966
2033
  for (const rule of DISPATCH_RULES) {
1967
- const result = await rule.match(ctx);
2034
+ const result = await rule.match(dispatchCtx);
1968
2035
  if (result) {
1969
2036
  if (result.action !== "skip") result.matchedRule = rule.name;
1970
2037
  const action = annotateBackgroundable(result);
1971
2038
  if (
1972
2039
  action.action === "dispatch" &&
1973
- ctx.session?.exhaustedVerificationUnits?.has(`${action.unitType}:${action.unitId}`)
2040
+ dispatchCtx.session?.exhaustedVerificationUnits?.has(`${action.unitType}:${action.unitId}`)
1974
2041
  ) {
1975
2042
  return {
1976
2043
  action: "stop",