@opengsd/gsd-pi 1.9.0 → 1.10.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 (300) hide show
  1. package/README.md +10 -10
  2. package/dist/headless.js +2 -1
  3. package/dist/resources/.managed-resources-content-hash +1 -1
  4. package/dist/resources/GSD-WORKFLOW.md +2 -2
  5. package/dist/resources/extensions/gsd/artifact-verification.js +3 -4
  6. package/dist/resources/extensions/gsd/auto/loop.js +24 -4
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +19 -1
  8. package/dist/resources/extensions/gsd/auto/unit-phase.js +37 -5
  9. package/dist/resources/extensions/gsd/auto-artifact-paths.js +14 -8
  10. package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
  11. package/dist/resources/extensions/gsd/auto-prompts.js +102 -21
  12. package/dist/resources/extensions/gsd/auto-recovery.js +10 -5
  13. package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -5
  14. package/dist/resources/extensions/gsd/auto-start.js +29 -9
  15. package/dist/resources/extensions/gsd/auto-tool-tracking.js +40 -0
  16. package/dist/resources/extensions/gsd/auto-worktree-cleanup.js +4 -0
  17. package/dist/resources/extensions/gsd/auto-worktree-sync.js +7 -3
  18. package/dist/resources/extensions/gsd/auto-worktree-teardown.js +1 -1
  19. package/dist/resources/extensions/gsd/auto.js +15 -5
  20. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -0
  21. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +33 -7
  22. package/dist/resources/extensions/gsd/bootstrap/system-context.js +10 -3
  23. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -24
  24. package/dist/resources/extensions/gsd/commands-verdict.js +10 -9
  25. package/dist/resources/extensions/gsd/compat/compat-marker.js +80 -5
  26. package/dist/resources/extensions/gsd/constants.js +12 -13
  27. package/dist/resources/extensions/gsd/context-masker.js +97 -21
  28. package/dist/resources/extensions/gsd/db/engine.js +5 -2
  29. package/dist/resources/extensions/gsd/docs/preferences-reference.md +22 -0
  30. package/dist/resources/extensions/gsd/doctor-engine-checks.js +24 -8
  31. package/dist/resources/extensions/gsd/error-classifier.js +12 -2
  32. package/dist/resources/extensions/gsd/exec-sandbox.js +1 -0
  33. package/dist/resources/extensions/gsd/flat-phase-migration.js +12 -0
  34. package/dist/resources/extensions/gsd/git-service.js +19 -7
  35. package/dist/resources/extensions/gsd/gsd-db.js +75 -31
  36. package/dist/resources/extensions/gsd/guided-flow.js +11 -5
  37. package/dist/resources/extensions/gsd/markdown-renderer.js +5 -21
  38. package/dist/resources/extensions/gsd/md-importer.js +5 -6
  39. package/dist/resources/extensions/gsd/model-cost-table.js +2 -0
  40. package/dist/resources/extensions/gsd/model-router.js +11 -0
  41. package/dist/resources/extensions/gsd/paths.js +66 -15
  42. package/dist/resources/extensions/gsd/planning-subagent-policy.js +37 -0
  43. package/dist/resources/extensions/gsd/planning-subagent-registry.js +25 -0
  44. package/dist/resources/extensions/gsd/preferences-types.js +5 -0
  45. package/dist/resources/extensions/gsd/preferences-validation.js +48 -1
  46. package/dist/resources/extensions/gsd/preferences.js +17 -0
  47. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
  48. package/dist/resources/extensions/gsd/provider-payload-policy.js +16 -5
  49. package/dist/resources/extensions/gsd/safety/destructive-guard.js +1 -1
  50. package/dist/resources/extensions/gsd/skill-activation.js +17 -3
  51. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.js +98 -2
  52. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.js +4 -1
  53. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +5 -2
  54. package/dist/resources/extensions/gsd/stop-notice.js +9 -0
  55. package/dist/resources/extensions/gsd/templates/PREFERENCES.md +5 -0
  56. package/dist/resources/extensions/gsd/tools/complete-task.js +12 -5
  57. package/dist/resources/extensions/gsd/tools/reopen-milestone.js +14 -5
  58. package/dist/resources/extensions/gsd/tools/reopen-slice.js +15 -5
  59. package/dist/resources/extensions/gsd/tools/reopen-task.js +16 -10
  60. package/dist/resources/extensions/gsd/unit-context-composer.js +28 -5
  61. package/dist/resources/extensions/gsd/workflow-projections.js +2 -3
  62. package/dist/resources/extensions/gsd/workspace.js +2 -1
  63. package/dist/resources/extensions/gsd/worktree-state-projection.js +20 -18
  64. package/dist/resources/extensions/shared/interview-ui.js +43 -6
  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.html +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/index.html +1 -1
  86. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  93. package/dist/web/standalone/.next/server/chunks/1991.js +2 -2
  94. package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
  95. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  96. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  98. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  99. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  100. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  101. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  102. package/integrations/hermes/pyproject.toml +1 -1
  103. package/package.json +7 -6
  104. package/packages/cloud-mcp-gateway/package.json +2 -2
  105. package/packages/contracts/package.json +1 -1
  106. package/packages/daemon/package.json +4 -4
  107. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts.map +1 -1
  108. package/packages/gsd-agent-core/dist/compaction/compaction.js +36 -4
  109. package/packages/gsd-agent-core/dist/compaction/compaction.js.map +1 -1
  110. package/packages/gsd-agent-core/dist/sdk.d.ts.map +1 -1
  111. package/packages/gsd-agent-core/dist/sdk.js +1 -0
  112. package/packages/gsd-agent-core/dist/sdk.js.map +1 -1
  113. package/packages/gsd-agent-core/package.json +5 -5
  114. package/packages/gsd-agent-modes/package.json +7 -7
  115. package/packages/gsd-cloud/README.md +9 -3
  116. package/packages/gsd-cloud/dist/background-cli.test.d.ts +2 -0
  117. package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +1 -0
  118. package/packages/gsd-cloud/dist/background-cli.test.js +248 -0
  119. package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -0
  120. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
  121. package/packages/gsd-cloud/dist/cli.js +121 -15
  122. package/packages/gsd-cloud/dist/cli.js.map +1 -1
  123. package/packages/gsd-cloud/dist/cloud-config.d.ts +1 -1
  124. package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -1
  125. package/packages/gsd-cloud/dist/cloud-config.js +7 -1
  126. package/packages/gsd-cloud/dist/cloud-config.js.map +1 -1
  127. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +1 -3
  128. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
  129. package/packages/gsd-cloud/dist/cloud-runtime.js +9 -7
  130. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
  131. package/packages/gsd-cloud/dist/runtime-process.d.ts +27 -0
  132. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -0
  133. package/packages/gsd-cloud/dist/runtime-process.js +272 -0
  134. package/packages/gsd-cloud/dist/runtime-process.js.map +1 -0
  135. package/packages/gsd-cloud/dist/runtime-process.test.d.ts +2 -0
  136. package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +1 -0
  137. package/packages/gsd-cloud/dist/runtime-process.test.js +97 -0
  138. package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -0
  139. package/packages/gsd-cloud/package.json +2 -2
  140. package/packages/mcp-server/README.md +7 -2
  141. package/packages/mcp-server/dist/cli-runner.d.ts +3 -0
  142. package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -1
  143. package/packages/mcp-server/dist/cli-runner.js +119 -33
  144. package/packages/mcp-server/dist/cli-runner.js.map +1 -1
  145. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  146. package/packages/mcp-server/dist/server.js +5 -5
  147. package/packages/mcp-server/dist/server.js.map +1 -1
  148. package/packages/mcp-server/dist/workflow-tools.d.ts +4 -3
  149. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  150. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  151. package/packages/mcp-server/package.json +4 -4
  152. package/packages/native/package.json +1 -1
  153. package/packages/pi-agent-core/dist/agent.d.ts +5 -1
  154. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  155. package/packages/pi-agent-core/dist/agent.js +2 -0
  156. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  157. package/packages/pi-agent-core/package.json +1 -1
  158. package/packages/pi-ai/dist/models.generated.d.ts +76 -0
  159. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  160. package/packages/pi-ai/dist/models.generated.js +70 -0
  161. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  162. package/packages/pi-ai/dist/providers/anthropic.js +6 -1
  163. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  164. package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
  165. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  166. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  167. package/packages/pi-ai/dist/providers/openai-completions.js +103 -0
  168. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  169. package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
  170. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  171. package/packages/pi-ai/package.json +1 -1
  172. package/packages/pi-coding-agent/dist/core/capability-patches.js +1 -1
  173. package/packages/pi-coding-agent/dist/core/capability-patches.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
  175. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  176. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +11 -1
  177. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  178. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  179. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  180. package/packages/pi-coding-agent/dist/core/skills.d.ts +5 -0
  181. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  182. package/packages/pi-coding-agent/dist/core/skills.js +54 -0
  183. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  184. package/packages/pi-coding-agent/package.json +7 -7
  185. package/packages/pi-tui/package.json +2 -2
  186. package/packages/rpc-client/package.json +2 -2
  187. package/pkg/package.json +1 -1
  188. package/src/resources/GSD-WORKFLOW.md +2 -2
  189. package/src/resources/extensions/gsd/artifact-verification.ts +3 -8
  190. package/src/resources/extensions/gsd/auto/loop.ts +48 -4
  191. package/src/resources/extensions/gsd/auto/run-unit.ts +20 -1
  192. package/src/resources/extensions/gsd/auto/unit-phase.ts +41 -5
  193. package/src/resources/extensions/gsd/auto-artifact-paths.ts +14 -7
  194. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  195. package/src/resources/extensions/gsd/auto-prompts.ts +118 -19
  196. package/src/resources/extensions/gsd/auto-recovery.ts +10 -8
  197. package/src/resources/extensions/gsd/auto-runtime-state.ts +12 -7
  198. package/src/resources/extensions/gsd/auto-start.ts +31 -8
  199. package/src/resources/extensions/gsd/auto-tool-tracking.ts +56 -0
  200. package/src/resources/extensions/gsd/auto-worktree-cleanup.ts +4 -0
  201. package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
  202. package/src/resources/extensions/gsd/auto-worktree-teardown.ts +1 -1
  203. package/src/resources/extensions/gsd/auto.ts +20 -7
  204. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +1 -0
  205. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +41 -12
  206. package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -3
  207. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +7 -29
  208. package/src/resources/extensions/gsd/commands-verdict.ts +16 -9
  209. package/src/resources/extensions/gsd/compat/compat-marker.ts +99 -5
  210. package/src/resources/extensions/gsd/constants.ts +14 -13
  211. package/src/resources/extensions/gsd/context-masker.ts +98 -2
  212. package/src/resources/extensions/gsd/db/engine.ts +5 -2
  213. package/src/resources/extensions/gsd/docs/preferences-reference.md +22 -0
  214. package/src/resources/extensions/gsd/doctor-engine-checks.ts +30 -7
  215. package/src/resources/extensions/gsd/error-classifier.ts +13 -2
  216. package/src/resources/extensions/gsd/exec-sandbox.ts +1 -0
  217. package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -0
  218. package/src/resources/extensions/gsd/git-service.ts +19 -6
  219. package/src/resources/extensions/gsd/gsd-db.ts +85 -33
  220. package/src/resources/extensions/gsd/guided-flow.ts +11 -4
  221. package/src/resources/extensions/gsd/markdown-renderer.ts +5 -23
  222. package/src/resources/extensions/gsd/md-importer.ts +5 -5
  223. package/src/resources/extensions/gsd/model-cost-table.ts +2 -0
  224. package/src/resources/extensions/gsd/model-router.ts +12 -0
  225. package/src/resources/extensions/gsd/paths.ts +73 -15
  226. package/src/resources/extensions/gsd/planning-subagent-policy.ts +58 -0
  227. package/src/resources/extensions/gsd/planning-subagent-registry.ts +31 -0
  228. package/src/resources/extensions/gsd/preferences-types.ts +16 -0
  229. package/src/resources/extensions/gsd/preferences-validation.ts +62 -0
  230. package/src/resources/extensions/gsd/preferences.ts +25 -0
  231. package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
  232. package/src/resources/extensions/gsd/provider-payload-policy.ts +18 -4
  233. package/src/resources/extensions/gsd/safety/destructive-guard.ts +1 -1
  234. package/src/resources/extensions/gsd/skill-activation.ts +17 -3
  235. package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +132 -2
  236. package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +4 -1
  237. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +4 -2
  238. package/src/resources/extensions/gsd/stop-notice.ts +11 -0
  239. package/src/resources/extensions/gsd/templates/PREFERENCES.md +5 -0
  240. package/src/resources/extensions/gsd/tests/auto-artifact-paths.test.ts +7 -7
  241. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +264 -0
  242. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +39 -0
  243. package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +21 -1
  244. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +74 -1
  245. package/src/resources/extensions/gsd/tests/before-provider-context-management.test.ts +55 -0
  246. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +10 -0
  247. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +25 -0
  248. package/src/resources/extensions/gsd/tests/complete-task.test.ts +76 -1
  249. package/src/resources/extensions/gsd/tests/context-masker.test.ts +204 -8
  250. package/src/resources/extensions/gsd/tests/db-engine-cjs-compat.test.ts +12 -0
  251. package/src/resources/extensions/gsd/tests/db-tools-alias-suppression.test.ts +62 -0
  252. package/src/resources/extensions/gsd/tests/destructive-confirmation.test.ts +33 -0
  253. package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +14 -0
  254. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +83 -0
  255. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +33 -1
  256. package/src/resources/extensions/gsd/tests/external-markdown-edit.test.ts +38 -1
  257. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
  258. package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +20 -1
  259. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
  260. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +22 -0
  261. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -0
  262. package/src/resources/extensions/gsd/tests/integration/paths.test.ts +22 -4
  263. package/src/resources/extensions/gsd/tests/integration-branch-meta-location.test.ts +62 -0
  264. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +5 -5
  265. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +9 -1
  266. package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
  267. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +7 -5
  268. package/src/resources/extensions/gsd/tests/preferences.test.ts +84 -0
  269. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +4 -0
  270. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +87 -0
  271. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +121 -1
  272. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +39 -0
  273. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +105 -0
  274. package/src/resources/extensions/gsd/tests/service-tier.test.ts +4 -0
  275. package/src/resources/extensions/gsd/tests/single-writer-v3-tool-surface.test.ts +33 -7
  276. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +20 -16
  277. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
  278. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +21 -1
  279. package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +13 -7
  280. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +14 -0
  281. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
  282. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +8 -0
  283. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +39 -0
  284. package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +7 -0
  285. package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +4 -4
  286. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
  287. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +60 -0
  288. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +34 -0
  289. package/src/resources/extensions/gsd/tools/complete-task.ts +20 -5
  290. package/src/resources/extensions/gsd/tools/reopen-milestone.ts +24 -5
  291. package/src/resources/extensions/gsd/tools/reopen-slice.ts +25 -5
  292. package/src/resources/extensions/gsd/tools/reopen-task.ts +23 -9
  293. package/src/resources/extensions/gsd/unit-context-composer.ts +34 -8
  294. package/src/resources/extensions/gsd/workflow-projections.ts +2 -3
  295. package/src/resources/extensions/gsd/workspace.ts +2 -1
  296. package/src/resources/extensions/gsd/worktree-state-projection.ts +21 -24
  297. package/src/resources/extensions/shared/interview-ui.ts +47 -8
  298. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +29 -0
  299. /package/dist/web/standalone/.next/static/{BE5XoklToI6lZjRzpXih8 → EAhnhYMt5UVOiG9-iPDjc}/_buildManifest.js +0 -0
  300. /package/dist/web/standalone/.next/static/{BE5XoklToI6lZjRzpXih8 → EAhnhYMt5UVOiG9-iPDjc}/_ssgManifest.js +0 -0
@@ -1,3 +1,5 @@
1
+ import { GSD_CONTEXT_MESSAGE_SENTINEL } from "./constants.js";
2
+
1
3
  /**
2
4
  * Observation masking for GSD auto-mode sessions.
3
5
  *
@@ -75,6 +77,26 @@ function findTurnBoundary(messages: MaskableMessage[], keepRecentTurns: number):
75
77
  return 0;
76
78
  }
77
79
 
80
+ function countRealUserTurns(messages: MaskableMessage[]): number {
81
+ let count = 0;
82
+ for (const m of messages) {
83
+ if (m.role === "user" && !isBashResultUserMessage(m)) count++;
84
+ }
85
+ return count;
86
+ }
87
+
88
+ /**
89
+ * Quantizes how many recent turns stay unmasked so the boundary only moves
90
+ * forward once per full block of `keepRecentTurns` new turns, instead of
91
+ * every turn. Keeps the masked prefix byte-stable within a block, which is
92
+ * what lets the LLM provider's prompt cache survive across turns.
93
+ */
94
+ function quantizedKeepTurns(totalTurns: number, keepRecentTurns: number): number {
95
+ if (keepRecentTurns <= 0 || totalTurns < 2 * keepRecentTurns) return totalTurns;
96
+ const maskedTurns = Math.floor((totalTurns - keepRecentTurns) / keepRecentTurns) * keepRecentTurns;
97
+ return totalTurns - maskedTurns;
98
+ }
99
+
78
100
  /**
79
101
  * Detect user messages that originated from bashExecution.
80
102
  * After convertToLlm, these are {role: "user", content: [{type:"text", text:"Ran `cmd`\n..."}]}.
@@ -95,7 +117,8 @@ function isMaskableMessage(m: MaskableMessage): boolean {
95
117
 
96
118
  export function createObservationMask(keepRecentTurns: number = 8) {
97
119
  return (messages: MaskableMessage[]): MaskableMessage[] => {
98
- const boundary = findTurnBoundary(messages, keepRecentTurns);
120
+ const totalTurns = countRealUserTurns(messages);
121
+ const boundary = findTurnBoundary(messages, quantizedKeepTurns(totalTurns, keepRecentTurns));
99
122
  if (boundary === 0) return messages;
100
123
 
101
124
  return messages.map((m, i) => {
@@ -126,6 +149,14 @@ function findResponsesTurnBoundary(items: ResponsesInputItem[], keepRecentTurns:
126
149
  return 0;
127
150
  }
128
151
 
152
+ function countResponsesRealUserTurns(items: ResponsesInputItem[]): number {
153
+ let count = 0;
154
+ for (const item of items) {
155
+ if (item.role === "user" && !isResponsesBashResultUserItem(item)) count++;
156
+ }
157
+ return count;
158
+ }
159
+
129
160
  /**
130
161
  * Observation masking for OpenAI/Codex Responses API payloads.
131
162
  *
@@ -135,7 +166,8 @@ function findResponsesTurnBoundary(items: ResponsesInputItem[], keepRecentTurns:
135
166
  */
136
167
  export function createResponsesInputObservationMask(keepRecentTurns: number = 8) {
137
168
  return (items: ResponsesInputItem[]): ResponsesInputItem[] => {
138
- const boundary = findResponsesTurnBoundary(items, keepRecentTurns);
169
+ const totalTurns = countResponsesRealUserTurns(items);
170
+ const boundary = findResponsesTurnBoundary(items, quantizedKeepTurns(totalTurns, keepRecentTurns));
139
171
  if (boundary === 0) return items;
140
172
 
141
173
  return items.map((item, i) => {
@@ -219,3 +251,67 @@ export function truncateResponsesInputResultItems(items: ResponsesInputItem[], m
219
251
  return item;
220
252
  });
221
253
  }
254
+
255
+ // GSD injects at most one context message per turn (memory/guided/forensics —
256
+ // see buildContextMessage in bootstrap/system-context.ts), marked with
257
+ // GSD_CONTEXT_MESSAGE_SENTINEL. convertToLlm strips the distinguishing
258
+ // customType before the payload reaches this hook, so detection is by content
259
+ // prefix instead. Pre-sentinel session history lacks the sentinel, so we also
260
+ // match the stable *bracketed* GSD block labels those injections begin with,
261
+ // letting resumed sessions dedupe older memory/guided blocks.
262
+ //
263
+ // Only GSD-specific bracketed markers belong here — never generic prose. A
264
+ // message is dropped from the payload when it matches, so a natural-language
265
+ // prefix (e.g. the forensics prompt "Debug GSD itself.") could silently delete
266
+ // a real user message that happens to start the same way. Legacy forensics
267
+ // injections without memory are therefore left un-deduped by design; those
268
+ // with memory still match via the "[GSD Context Metadata]" wrapper.
269
+ const LEGACY_GSD_CONTEXT_INJECTION_PREFIXES = [
270
+ "[GSD Context Metadata]",
271
+ "[MEMORY — Critical and prompt-relevant memories from the GSD memory store]",
272
+ "[GSD Guided Execute Context]",
273
+ ] as const;
274
+
275
+ function isGsdContextInjectionText(text: string | undefined): boolean {
276
+ if (typeof text !== "string") return false;
277
+ if (text.startsWith(GSD_CONTEXT_MESSAGE_SENTINEL)) return true;
278
+ return LEGACY_GSD_CONTEXT_INJECTION_PREFIXES.some((prefix) => text.startsWith(prefix));
279
+ }
280
+
281
+ function isGsdContextInjectionMessage(m: MaskableMessage): boolean {
282
+ if (m.role !== "user") return false;
283
+ return isGsdContextInjectionText(firstTextFromContent(m.content));
284
+ }
285
+
286
+ /**
287
+ * Removes every GSD context-injection user message except the latest one.
288
+ * Each turn re-injects a near-identical memory/guided/forensics block; left
289
+ * in place they duplicate verbatim across an N-turn session. Removal (not
290
+ * masking) — an empty placeholder still costs tokens and shifts message
291
+ * positions, breaking cache byte-stability. Pure function: stored history is
292
+ * never mutated, only the outgoing payload array.
293
+ */
294
+ export function filterSupersededContextInjections(messages: MaskableMessage[]): MaskableMessage[] {
295
+ let lastIndex = -1;
296
+ for (let i = 0; i < messages.length; i++) {
297
+ if (isGsdContextInjectionMessage(messages[i])) lastIndex = i;
298
+ }
299
+ if (lastIndex === -1) return messages;
300
+ return messages.filter((m, i) => i === lastIndex || !isGsdContextInjectionMessage(m));
301
+ }
302
+
303
+ function isResponsesGsdContextInjectionItem(item: ResponsesInputItem): boolean {
304
+ if (item.role !== "user") return false;
305
+ return isGsdContextInjectionText(firstTextFromContent(item.content));
306
+ }
307
+
308
+ export function filterSupersededResponsesContextInjections(
309
+ items: ResponsesInputItem[],
310
+ ): ResponsesInputItem[] {
311
+ let lastIndex = -1;
312
+ for (let i = 0; i < items.length; i++) {
313
+ if (isResponsesGsdContextInjectionItem(items[i])) lastIndex = i;
314
+ }
315
+ if (lastIndex === -1) return items;
316
+ return items.filter((item, i) => i === lastIndex || !isResponsesGsdContextInjectionItem(item));
317
+ }
@@ -10,7 +10,7 @@
10
10
  // writer layer.
11
11
  import { createRequire } from "node:module";
12
12
  import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
13
- import { dirname, join } from "node:path";
13
+ import { dirname, join, resolve } from "node:path";
14
14
  import { GSDError, GSD_STALE_STATE } from "../errors.js";
15
15
  import type { GsdWorkspace, MilestoneScope } from "../workspace.js";
16
16
  import { logError, logWarning } from "../workflow-logger.js";
@@ -71,7 +71,10 @@ let _gsdRequire: ReturnType<typeof createRequire> | null | undefined;
71
71
  function getGsdRequire(): ReturnType<typeof createRequire> | null {
72
72
  if (_gsdRequire !== undefined) return _gsdRequire;
73
73
  try {
74
- _gsdRequire = createRequire(import.meta.url);
74
+ // Next.js may emit this module into a CommonJS chunk. Avoid ESM-only module
75
+ // metadata syntax here; it is a hard parse error there.
76
+ const packageRoot = process.env.GSD_WEB_PACKAGE_ROOT || process.env.GSD_PKG_ROOT || process.cwd();
77
+ _gsdRequire = createRequire(resolve(packageRoot, "package.json"));
75
78
  } catch {
76
79
  _gsdRequire = null;
77
80
  }
@@ -357,6 +357,28 @@ In `"parent"` mode, slice/task `targetRepositories` default to the declared chil
357
357
  - `"replace"` requires `prompt`.
358
358
  - `"skip"` is valid with no additional fields.
359
359
 
360
+ - `planning_subagents`: object — project-local widening for read-only planning subagent dispatch. Supported unit keys are `plan-milestone` and `plan-slice`; each has:
361
+ - `allowed`: string[] — additional planning-safe agents for that unit.
362
+
363
+ Agents must already be classified by GSD as read-only planning specialists: `mnemo`, `scout`, `planner`, `reviewer`, `security`, or `tester`. The write gate still blocks source writes outside `.gsd/**`, unrestricted bash, stale subagent calls without agent identities, agents outside the global read-only registry, and agents not listed for the active unit.
364
+
365
+ Example:
366
+
367
+ ```yaml
368
+ planning_subagents:
369
+ plan-milestone:
370
+ allowed:
371
+ - scout
372
+ - planner
373
+ - security
374
+ plan-slice:
375
+ allowed:
376
+ - scout
377
+ - planner
378
+ - reviewer
379
+ - security
380
+ ```
381
+
360
382
  **Known unit types for `before`/`after`:** `research-milestone`, `plan-milestone`, `research-slice`, `plan-slice`, `execute-task`, `complete-slice`, `replan-slice`, `reassess-roadmap`, `run-uat`.
361
383
 
362
384
  - `experimental`: opt-in experimental features. All features here are **off by default** — you must explicitly set each one to `true` to enable it. Features in this block may change or be removed without a deprecation cycle while in experimental status. Keys:
@@ -15,13 +15,13 @@ import {
15
15
  import { MEMORIES_FTS_REBUILT_KEY } from "./db-memory-fts-schema.js";
16
16
  import { isAfter, latestExplicitReopenAt } from "./milestone-reopen-events.js";
17
17
  import {
18
- buildTaskFileName,
19
18
  gsdProjectionRoot,
20
19
  gsdRoot,
21
20
  resolveGsdPathContract,
22
21
  resolveMilestoneFile,
23
22
  resolveMilestonePath,
24
23
  resolveSliceFile,
24
+ resolveTaskFile,
25
25
  } from "./paths.js";
26
26
  import { deriveState } from "./state.js";
27
27
  import { isClosedStatus } from "./status-guards.js";
@@ -95,6 +95,25 @@ function reportCheckboxDbStatusDivergence(
95
95
  });
96
96
  }
97
97
 
98
+ function bareDuplicateMilestoneId(milestoneId: string): string | null {
99
+ const match = milestoneId.match(/^(M\d{3})-[a-z0-9]{6}$/i);
100
+ return match?.[1]?.toUpperCase() ?? null;
101
+ }
102
+
103
+ function checkboxDbStatusMilestoneIds(basePath: string, milestoneIds: string[]): string[] {
104
+ if (!hasFlatPhaseLayout(basePath)) return milestoneIds;
105
+
106
+ // Flat-phase directories are keyed by phase number. A restored
107
+ // M003-xxxxxx row alongside M003 is a stale duplicate for the same
108
+ // phases/03-* projection, so checking both can compare DB status against two
109
+ // competing PLAN files and report a persistent false divergence.
110
+ const allIds = new Set(milestoneIds.map((id) => id.toUpperCase()));
111
+ return milestoneIds.filter((milestoneId) => {
112
+ const bareId = bareDuplicateMilestoneId(milestoneId);
113
+ return !bareId || !allIds.has(bareId);
114
+ });
115
+ }
116
+
98
117
  function checkProjectionCheckboxDbStatus(basePath: string, milestoneIds: string[], issues: DoctorIssue[]): void {
99
118
  for (const milestoneId of milestoneIds) {
100
119
  const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
@@ -208,10 +227,8 @@ function resolveFlatPhaseArtifactDiskPath(basePath: string, row: ArtifactRow): s
208
227
  if (!artifactType) return null;
209
228
 
210
229
  if (row.slice_id && row.task_id) {
211
- const phaseDir = resolveMilestonePath(basePath, row.milestone_id);
212
- if (!phaseDir || !isFlatPhaseDiskPath(basePath, phaseDir)) return null;
213
- const candidate = join(phaseDir, buildTaskFileName(row.task_id, artifactType));
214
- return existsSync(candidate) ? candidate : null;
230
+ const candidate = resolveTaskFile(basePath, row.milestone_id, row.slice_id, row.task_id, artifactType);
231
+ return candidate && isFlatPhaseDiskPath(basePath, candidate) ? candidate : null;
215
232
  }
216
233
 
217
234
  const candidate = row.slice_id
@@ -436,7 +453,9 @@ function staleMilestonesArtifactRowFixable(basePath: string, row: ArtifactRow, a
436
453
 
437
454
  function stalePhasesArtifactRowFixable(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
438
455
  const issuePath = artifactPathRelativeToGsd(row.path);
439
- return issuePath.startsWith(`${LAYOUT_SEGMENTS.level1}/`) && hasPresentMilestonesReplacement(basePath, row, artifactRows);
456
+ return issuePath.startsWith(`${LAYOUT_SEGMENTS.level1}/`) &&
457
+ (hasPresentFlatPhaseReplacement(basePath, row, artifactRows) ||
458
+ hasPresentMilestonesReplacement(basePath, row, artifactRows));
440
459
  }
441
460
 
442
461
  function staleArtifactRowFixable(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
@@ -809,7 +828,11 @@ export async function checkEngineHealth(
809
828
  // block doctor.
810
829
  try {
811
830
  if (isDbAvailable()) {
812
- checkProjectionCheckboxDbStatus(basePath, getAllMilestones().map((milestone) => milestone.id), issues);
831
+ checkProjectionCheckboxDbStatus(
832
+ basePath,
833
+ checkboxDbStatusMilestoneIds(basePath, getAllMilestones().map((milestone) => milestone.id)),
834
+ issues,
835
+ );
813
836
  }
814
837
  } catch {
815
838
  // Non-fatal: checkbox-vs-DB divergence check must never block doctor
@@ -65,8 +65,12 @@ const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|web
65
65
  // transient errors so auto-mode can retry with reduced budget or fall back to a larger-context model.
66
66
  // See: https://github.com/open-gsd/gsd-pi/issues/4528
67
67
  const SERVER_RE = /internal(?: server)?[ _-]?error|server[ _-]?error|500|502|503|overloaded|server_error|api_error|service.?unavailable|context (?:window|length) exceed|context window exceed/i;
68
+ // Provider adapters throw this when an upstream stop reason reaches Pi before
69
+ // the adapter knows how to map it. Keep the match scoped so plain "unknown"
70
+ // runtime errors still pause for inspection instead of being retried forever.
71
+ const PROVIDER_STOP_REASON_RE = /unhandled stop reason:\s*(?:abort|unknown)\b/i;
68
72
  // ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
69
- const CONNECTION_RE = /terminated|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
73
+ const CONNECTION_RE = /terminated|\babort\b|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
70
74
  // Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
71
75
  // This eliminates the need to enumerate every error message variant individually.
72
76
  const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
@@ -103,7 +107,7 @@ const UNSUPPORTED_MODEL_SCOPE_RE = /\b(?:account|plan|tier|subscription)\b/i;
103
107
  * 3. Network (ECONNRESET, ETIMEDOUT, socket hang up, fetch failed, dns)
104
108
  * 4. Stream truncation (malformed JSON from mid-stream cut)
105
109
  * 5. Server (500/502/503, overloaded, server_error)
106
- * 6. Connection (terminated, ECONNREFUSED, EPIPE, other side closed)
110
+ * 6. Connection (terminated, abort/unknown stop reason, ECONNREFUSED, EPIPE, other side closed)
107
111
  * 7. Model error (400 invalid argument / payload rejected for this model)
108
112
  * 8. Unknown
109
113
  */
@@ -163,6 +167,13 @@ export function classifyError(errorMsg: string, retryAfterMs?: number): ErrorCla
163
167
  return { kind: "rate-limit", retryAfterMs: delayMs };
164
168
  }
165
169
 
170
+ // Provider stop-reason adapter gaps are usually request/stream failures from
171
+ // the current model. Treat the scoped adapter message as fallback-eligible,
172
+ // but keep generic unknown errors on the indefinite pause path.
173
+ if (PROVIDER_STOP_REASON_RE.test(errorMsg)) {
174
+ return { kind: "connection", retryAfterMs: retryAfterMs ?? 15_000 };
175
+ }
176
+
166
177
  // 3. Network errors — same-model retry candidate
167
178
  if (NETWORK_RE.test(errorMsg)) {
168
179
  // Exclude if also matches permanent signals (already handled above for
@@ -111,6 +111,7 @@ export const EXEC_DEFAULTS = {
111
111
  "LOGNAME",
112
112
  "TMPDIR",
113
113
  "NODE_OPTIONS",
114
+ "NODE_PATH",
114
115
  "PYTHONPATH",
115
116
  "PYTHONIOENCODING",
116
117
  ] as const,
@@ -155,6 +155,19 @@ function isInsideFlatProjectionBackup(backupDir: string, src: string): boolean {
155
155
  return src === phaseBackupDir || src.startsWith(`${phaseBackupDir}/`) || src.startsWith(`${phaseBackupDir}\\`);
156
156
  }
157
157
 
158
+ function removeFlatProjectionBackup(backupDir: string): void {
159
+ const phaseBackupDir = join(backupDir, "__phases");
160
+ if (!existsSync(phaseBackupDir)) return;
161
+ try {
162
+ removePathWithRetries(phaseBackupDir);
163
+ } catch (err) {
164
+ logWarning(
165
+ "migration",
166
+ `flat-phase migration succeeded but could not remove temporary ${LAYOUT_SEGMENTS.level1}/ backup: ${(err as Error).message}`,
167
+ );
168
+ }
169
+ }
170
+
158
171
  function restoreFlatProjectionFromBackup(basePath: string, backupDir: string): void {
159
172
  const phaseBackupDir = join(backupDir, "__phases");
160
173
  if (!existsSync(phaseBackupDir)) return;
@@ -494,4 +507,5 @@ export async function migrateToFlatPhase(basePath: string): Promise<void> {
494
507
  `flat-phase migration succeeded but could not remove ${LEGACY_MIGRATING_SEGMENT}: ${(err as Error).message}`,
495
508
  );
496
509
  }
510
+ removeFlatProjectionBackup(backupDir);
497
511
  }
@@ -385,10 +385,16 @@ const runtimeFilesCleanedUpRepos = new Set<string>();
385
385
  // ─── Integration Branch Metadata ───────────────────────────────────────────
386
386
 
387
387
  /**
388
- * Path to the milestone metadata file that stores the integration branch.
389
- * Format: .gsd/milestones/<MID>/<MID>-META.json
388
+ * Path to the milestone integration-branch metadata file.
389
+ * Format: .gsd/<MID>-META.json (deliberately OUTSIDE milestones/<MID>/ so it
390
+ * cannot poison isLegacyMilestonesLayout — see ADR-045).
390
391
  */
391
- function milestoneMetaPath(basePath: string, milestoneId: string): string {
392
+ export function milestoneMetaPath(basePath: string, milestoneId: string): string {
393
+ return join(gsdRoot(basePath), `${milestoneId}-META.json`);
394
+ }
395
+
396
+ /** Legacy location (pre-ADR-045). Read-only fallback for existing on-disk trees. */
397
+ function legacyMilestoneMetaPath(basePath: string, milestoneId: string): string {
392
398
  return join(gsdRoot(basePath), "milestones", milestoneId, `${milestoneId}-META.json`);
393
399
  }
394
400
 
@@ -398,8 +404,12 @@ function milestoneMetaPath(basePath: string, milestoneId: string): string {
398
404
  */
399
405
  export function readIntegrationBranch(basePath: string, milestoneId: string): string | null {
400
406
  try {
401
- const metaFile = milestoneMetaPath(basePath, milestoneId);
402
- if (!existsSync(metaFile)) return null;
407
+ let metaFile = milestoneMetaPath(basePath, milestoneId);
408
+ if (!existsSync(metaFile)) {
409
+ const legacy = legacyMilestoneMetaPath(basePath, milestoneId);
410
+ if (!existsSync(legacy)) return null;
411
+ metaFile = legacy;
412
+ }
403
413
  const data = JSON.parse(readFileSync(metaFile, "utf-8"));
404
414
  const branch = data?.integrationBranch;
405
415
  if (typeof branch === "string" && branch.trim() !== "" && VALID_BRANCH_NAME.test(branch)) {
@@ -451,7 +461,10 @@ export function writeIntegrationBranch(
451
461
  if (existingBranch === branch) return;
452
462
 
453
463
  const metaFile = milestoneMetaPath(basePath, milestoneId);
454
- mkdirSync(join(gsdRoot(basePath), "milestones", milestoneId), { recursive: true });
464
+ // Ensure the flat .gsd/ root exists (never milestones/<MID>/ that would
465
+ // poison layout detection, ADR-045). In production .gsd/ already exists; this
466
+ // guards fresh trees so the write below can't ENOENT.
467
+ mkdirSync(gsdRoot(basePath), { recursive: true });
455
468
 
456
469
  // Merge with existing metadata if present
457
470
  let existing: Record<string, unknown> = {};
@@ -67,6 +67,13 @@ export type { SliceRow, TaskRow } from "./db-task-slice-rows.js";
67
67
 
68
68
  import { TERMINAL_STATUS_SQL } from "./db/sql-constants.js";
69
69
  import { applyStatusTransition } from "./db/writers/status.js";
70
+ import {
71
+ LAYOUT_SEGMENTS,
72
+ derivePhaseSlug,
73
+ milestoneIdToPhaseNum,
74
+ milestoneIdUniqueSuffix,
75
+ phaseDirName,
76
+ } from "./layout-policy.js";
70
77
 
71
78
  export function insertDecision(d: Omit<Decision, "seq">): void {
72
79
  if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
@@ -206,6 +213,47 @@ export function insertArtifact(a: {
206
213
  });
207
214
  }
208
215
 
216
+ function canonicalPhaseDirNameForDb(milestoneId: string, title: string): string {
217
+ const phaseNum = milestoneIdToPhaseNum(milestoneId);
218
+ const slug = derivePhaseSlug(title || milestoneId);
219
+ const suffix = milestoneIdUniqueSuffix(milestoneId);
220
+ return phaseDirName(phaseNum, suffix ? `${suffix}-${slug}` : slug);
221
+ }
222
+
223
+ function reconcileMilestonePhaseArtifactPaths(milestoneId: string, title: string): void {
224
+ const db = getDbOrNull()!;
225
+ const phaseNumPrefix = String(milestoneIdToPhaseNum(milestoneId)).padStart(2, "0");
226
+ const canonicalDir = canonicalPhaseDirNameForDb(milestoneId, title);
227
+ const staleRows = db.prepare(
228
+ `SELECT path
229
+ FROM artifacts
230
+ WHERE milestone_id = :milestone_id
231
+ AND path LIKE :phase_prefix`,
232
+ ).all({
233
+ ":milestone_id": milestoneId,
234
+ ":phase_prefix": `${LAYOUT_SEGMENTS.level1}/${phaseNumPrefix}-%/%`,
235
+ }) as Array<{ path: string }>;
236
+
237
+ const updatePath = db.prepare("UPDATE artifacts SET path = :new_path WHERE path = :old_path");
238
+ const deletePath = db.prepare("DELETE FROM artifacts WHERE path = :path");
239
+ const existingPath = db.prepare("SELECT 1 AS present FROM artifacts WHERE path = :path");
240
+
241
+ for (const row of staleRows) {
242
+ const parts = row.path.split("/");
243
+ if (parts.length < 3) continue;
244
+ if (parts[0] !== LAYOUT_SEGMENTS.level1) continue;
245
+ if (parts[1] === canonicalDir) continue;
246
+ if (!parts[1]?.startsWith(`${phaseNumPrefix}-`)) continue;
247
+
248
+ const newPath = [LAYOUT_SEGMENTS.level1, canonicalDir, ...parts.slice(2)].join("/");
249
+ if (existingPath.get({ ":path": newPath })) {
250
+ deletePath.run({ ":path": row.path });
251
+ continue;
252
+ }
253
+ updatePath.run({ ":new_path": newPath, ":old_path": row.path });
254
+ }
255
+ }
256
+
209
257
  export interface MilestonePlanningRecord {
210
258
  vision: string;
211
259
  successCriteria: string[];
@@ -287,39 +335,43 @@ export function insertMilestone(m: {
287
335
 
288
336
  export function upsertMilestonePlanning(milestoneId: string, planning: Partial<MilestonePlanningRecord> & { title?: string; status?: string; depends_on?: string[] }): void {
289
337
  if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
290
- getDbOrNull()!.prepare(
291
- `UPDATE milestones SET
292
- title = COALESCE(NULLIF(:title, ''), title),
293
- status = COALESCE(NULLIF(:status, ''), status),
294
- depends_on = COALESCE(:depends_on, depends_on),
295
- vision = COALESCE(:vision, vision),
296
- success_criteria = COALESCE(:success_criteria, success_criteria),
297
- key_risks = COALESCE(:key_risks, key_risks),
298
- proof_strategy = COALESCE(:proof_strategy, proof_strategy),
299
- verification_contract = COALESCE(:verification_contract, verification_contract),
300
- verification_integration = COALESCE(:verification_integration, verification_integration),
301
- verification_operational = COALESCE(:verification_operational, verification_operational),
302
- verification_uat = COALESCE(:verification_uat, verification_uat),
303
- definition_of_done = COALESCE(:definition_of_done, definition_of_done),
304
- requirement_coverage = COALESCE(:requirement_coverage, requirement_coverage),
305
- boundary_map_markdown = COALESCE(:boundary_map_markdown, boundary_map_markdown)
306
- WHERE id = :id`,
307
- ).run({
308
- ":id": milestoneId,
309
- ":title": planning.title ?? "",
310
- ":status": planning.status ?? "",
311
- ":depends_on": planning.depends_on ? JSON.stringify(planning.depends_on) : null,
312
- ":vision": planning.vision ?? null,
313
- ":success_criteria": planning.successCriteria ? JSON.stringify(planning.successCriteria) : null,
314
- ":key_risks": planning.keyRisks ? JSON.stringify(planning.keyRisks) : null,
315
- ":proof_strategy": planning.proofStrategy ? JSON.stringify(planning.proofStrategy) : null,
316
- ":verification_contract": planning.verificationContract ?? null,
317
- ":verification_integration": planning.verificationIntegration ?? null,
318
- ":verification_operational": planning.verificationOperational ?? null,
319
- ":verification_uat": planning.verificationUat ?? null,
320
- ":definition_of_done": planning.definitionOfDone ? JSON.stringify(planning.definitionOfDone) : null,
321
- ":requirement_coverage": planning.requirementCoverage ?? null,
322
- ":boundary_map_markdown": planning.boundaryMapMarkdown ?? null,
338
+ transaction(() => {
339
+ getDbOrNull()!.prepare(
340
+ `UPDATE milestones SET
341
+ title = COALESCE(NULLIF(:title, ''), title),
342
+ status = COALESCE(NULLIF(:status, ''), status),
343
+ depends_on = COALESCE(:depends_on, depends_on),
344
+ vision = COALESCE(:vision, vision),
345
+ success_criteria = COALESCE(:success_criteria, success_criteria),
346
+ key_risks = COALESCE(:key_risks, key_risks),
347
+ proof_strategy = COALESCE(:proof_strategy, proof_strategy),
348
+ verification_contract = COALESCE(:verification_contract, verification_contract),
349
+ verification_integration = COALESCE(:verification_integration, verification_integration),
350
+ verification_operational = COALESCE(:verification_operational, verification_operational),
351
+ verification_uat = COALESCE(:verification_uat, verification_uat),
352
+ definition_of_done = COALESCE(:definition_of_done, definition_of_done),
353
+ requirement_coverage = COALESCE(:requirement_coverage, requirement_coverage),
354
+ boundary_map_markdown = COALESCE(:boundary_map_markdown, boundary_map_markdown)
355
+ WHERE id = :id`,
356
+ ).run({
357
+ ":id": milestoneId,
358
+ ":title": planning.title ?? "",
359
+ ":status": planning.status ?? "",
360
+ ":depends_on": planning.depends_on ? JSON.stringify(planning.depends_on) : null,
361
+ ":vision": planning.vision ?? null,
362
+ ":success_criteria": planning.successCriteria ? JSON.stringify(planning.successCriteria) : null,
363
+ ":key_risks": planning.keyRisks ? JSON.stringify(planning.keyRisks) : null,
364
+ ":proof_strategy": planning.proofStrategy ? JSON.stringify(planning.proofStrategy) : null,
365
+ ":verification_contract": planning.verificationContract ?? null,
366
+ ":verification_integration": planning.verificationIntegration ?? null,
367
+ ":verification_operational": planning.verificationOperational ?? null,
368
+ ":verification_uat": planning.verificationUat ?? null,
369
+ ":definition_of_done": planning.definitionOfDone ? JSON.stringify(planning.definitionOfDone) : null,
370
+ ":requirement_coverage": planning.requirementCoverage ?? null,
371
+ ":boundary_map_markdown": planning.boundaryMapMarkdown ?? null,
372
+ });
373
+ const finalTitle = planning.title?.trim();
374
+ if (finalTitle) reconcileMilestonePhaseArtifactPaths(milestoneId, finalTitle);
323
375
  });
324
376
  }
325
377
 
@@ -26,6 +26,7 @@ import {
26
26
  buildPlanMilestonePrompt,
27
27
  buildPlanSlicePrompt,
28
28
  buildSkillActivationBlock,
29
+ capPreamble,
29
30
  } from "./auto-prompts.js";
30
31
  import { deriveState, isGhostMilestone } from "./state.js";
31
32
  import { invalidateAllCaches } from "./cache.js";
@@ -1199,7 +1200,9 @@ function resolveDiscussSliceBasePath(basePath: string, milestoneId: string): str
1199
1200
  * slice summaries so the agent can ask grounded UX/behaviour questions
1200
1201
  * without wasting a turn reading files.
1201
1202
  */
1202
- async function buildDiscussSlicePrompt(
1203
+ // Exported for tests (`prompt-budget-enforcement.test.ts`) — verifies the
1204
+ // inlined-context cap. Not part of the public flow surface otherwise.
1205
+ export async function buildDiscussSlicePrompt(
1203
1206
  mid: string,
1204
1207
  sid: string,
1205
1208
  sTitle: string,
@@ -1262,8 +1265,12 @@ async function buildDiscussSlicePrompt(
1262
1265
  }
1263
1266
  }
1264
1267
 
1268
+ // Cap the inlined block: it grows unbounded with each completed slice's full
1269
+ // SUMMARY. `capPreamble` truncates at `### ` section boundaries — roadmap and
1270
+ // milestone context come first (survive), completed-slice summaries come last
1271
+ // (dropped first) when the executor's window can't hold everything.
1265
1272
  const inlinedContext = inlined.length > 0
1266
- ? `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`
1273
+ ? capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`)
1267
1274
  : `## Inlined Context\n\n_(no context files found yet — go in blind and ask broad questions)_`;
1268
1275
 
1269
1276
  const sliceDirPath = `.gsd/milestones/${mid}/slices/${sid}`;
@@ -1275,7 +1282,7 @@ async function buildDiscussSlicePrompt(
1275
1282
  : "";
1276
1283
 
1277
1284
  const inlinedTemplates = inlineTemplate("slice-context", "Slice Context");
1278
- return loadPrompt("guided-discuss-slice", {
1285
+ return prependLanguageDirective(base, loadPrompt("guided-discuss-slice", {
1279
1286
  milestoneId: mid,
1280
1287
  sliceId: sid,
1281
1288
  sliceTitle: sTitle,
@@ -1286,7 +1293,7 @@ async function buildDiscussSlicePrompt(
1286
1293
  inlinedTemplates,
1287
1294
  structuredQuestionsAvailable: options?.structuredQuestionsAvailable ?? "false",
1288
1295
  commitInstruction: buildDocsCommitInstruction(`docs(${mid}/${sid}): slice context from discuss`),
1289
- });
1296
+ }));
1290
1297
  }
1291
1298
 
1292
1299
  /**
@@ -12,7 +12,7 @@
12
12
  import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from "node:fs";
13
13
  import { logWarning } from "./workflow-logger.js";
14
14
  import { isClosedStatus } from "./status-guards.js";
15
- import { dirname, join, relative } from "node:path";
15
+ import { dirname, join } from "node:path";
16
16
  import {
17
17
  getAllMilestones,
18
18
  getMilestone,
@@ -40,6 +40,7 @@ import {
40
40
  resolveTasksDir,
41
41
  targetMilestoneFile,
42
42
  targetSliceFile,
43
+ targetTaskFile,
43
44
  gsdProjectionRoot,
44
45
  gsdRoot,
45
46
  buildTaskFileName,
@@ -49,7 +50,7 @@ import { saveFile, clearParseCache, registerCacheClearCallback } from "./files.j
49
50
  import { parseRoadmap, parsePlan } from "./parsers-legacy.js";
50
51
  import { invalidateStateCache } from "./state.js";
51
52
  import { clearPathCache, milestonesDir, legacyMilestonesDir, isLegacyMilestonesLayout, resolveMilestonePath, relSliceFile, canonicalPhaseDirName } from "./paths.js";
52
- import { readCompatMarker, writeCompatMarker, computeProjectionSha } from "./compat/compat-marker.js";
53
+ import { readCompatMarker, writeCompatMarker, computeProjectionSha, deriveCompatProjectionKey } from "./compat/compat-marker.js";
53
54
  import type { RiskLevel } from "./types.js";
54
55
  import {
55
56
  phaseDirName,
@@ -108,13 +109,7 @@ function flushProjectionWritesToMarker(): void {
108
109
  */
109
110
  function toArtifactPath(absPath: string, basePath: string): string {
110
111
  const projectionRoot = gsdProjectionRoot(basePath);
111
- const projectionRel = relative(projectionRoot, absPath);
112
- const root = projectionRel && !projectionRel.startsWith("..") && !projectionRel.startsWith("/")
113
- ? projectionRoot
114
- : gsdRoot(basePath);
115
- const rel = relative(root, absPath);
116
- // Normalize to forward slashes for consistent DB keys
117
- return rel.replace(/\\/g, "/");
112
+ return deriveCompatProjectionKey(absPath, [projectionRoot, gsdRoot(basePath)]);
118
113
  }
119
114
 
120
115
  /**
@@ -756,20 +751,7 @@ export async function renderTaskSummary(
756
751
  return false; // No summary to render — skip silently
757
752
  }
758
753
 
759
- // Resolve the tasks directory, creating path if needed
760
- const slicePath = resolveSlicePath(basePath, milestoneId, sliceId);
761
- if (!slicePath) {
762
- process.stderr.write(
763
- `markdown-renderer: cannot resolve slice path for ${milestoneId}/${sliceId}\n`,
764
- );
765
- return false;
766
- }
767
-
768
- // Use the tasks/ subdir when it exists (legacy layout), otherwise use the
769
- // slice path directly (flat-phase: task summaries live alongside plan files).
770
- const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId) ?? slicePath;
771
- const fileName = buildTaskFileName(taskId, "SUMMARY");
772
- const absPath = join(tasksDir, fileName);
754
+ const absPath = targetTaskFile(basePath, milestoneId, sliceId, taskId, "SUMMARY", getMilestone(milestoneId)?.title);
773
755
  const artifactPath = toArtifactPath(absPath, basePath);
774
756
 
775
757
  await writeAndStore(absPath, artifactPath, task.full_summary_md, {