@opengsd/gsd-pi 1.5.0-dev.553dc518 → 1.5.0-dev.c4c8885f

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 (302) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/ask-user-questions.js +1 -1
  3. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +114 -36
  4. package/dist/resources/extensions/gsd/activity-log.js +1 -1
  5. package/dist/resources/extensions/gsd/auto/orchestrator.js +16 -10
  6. package/dist/resources/extensions/gsd/auto-artifact-paths.js +14 -5
  7. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +7 -2
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +69 -17
  9. package/dist/resources/extensions/gsd/auto-model-selection.js +28 -1
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +4 -3
  11. package/dist/resources/extensions/gsd/auto-prompts.js +29 -117
  12. package/dist/resources/extensions/gsd/auto-recovery.js +50 -6
  13. package/dist/resources/extensions/gsd/auto-start.js +54 -31
  14. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +12 -2
  15. package/dist/resources/extensions/gsd/auto-timers.js +9 -2
  16. package/dist/resources/extensions/gsd/auto.js +97 -31
  17. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +1 -1
  18. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +67 -6
  19. package/dist/resources/extensions/gsd/codebase-generator.js +7 -1
  20. package/dist/resources/extensions/gsd/compat/compat-marker.js +44 -0
  21. package/dist/resources/extensions/gsd/config-overlay.js +7 -2
  22. package/dist/resources/extensions/gsd/db/engine.js +33 -0
  23. package/dist/resources/extensions/gsd/db/writers/cascades.js +20 -2
  24. package/dist/resources/extensions/gsd/db-adapter.js +30 -3
  25. package/dist/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  26. package/dist/resources/extensions/gsd/flat-phase-migration.js +18 -0
  27. package/dist/resources/extensions/gsd/gsd-db.js +12 -11
  28. package/dist/resources/extensions/gsd/guided-flow.js +16 -7
  29. package/dist/resources/extensions/gsd/journal.js +1 -1
  30. package/dist/resources/extensions/gsd/markdown-renderer.js +34 -91
  31. package/dist/resources/extensions/gsd/md-importer.js +89 -23
  32. package/dist/resources/extensions/gsd/metrics.js +2 -2
  33. package/dist/resources/extensions/gsd/migration-auto-check.js +30 -3
  34. package/dist/resources/extensions/gsd/paths.js +87 -52
  35. package/dist/resources/extensions/gsd/post-unit-hooks.js +27 -0
  36. package/dist/resources/extensions/gsd/preferences-models.js +66 -15
  37. package/dist/resources/extensions/gsd/preferences-skills.js +10 -16
  38. package/dist/resources/extensions/gsd/preferences-validation.js +66 -10
  39. package/dist/resources/extensions/gsd/preferences.js +20 -0
  40. package/dist/resources/extensions/gsd/rule-registry.js +106 -10
  41. package/dist/resources/extensions/gsd/skill-discovery.js +36 -23
  42. package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +1 -10
  43. package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +3 -24
  44. package/dist/resources/extensions/gsd/state-reconciliation/index.js +10 -0
  45. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -5
  46. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +16 -10
  47. package/dist/resources/extensions/gsd/tools/reopen-slice.js +5 -0
  48. package/dist/resources/extensions/gsd/tools/reopen-task.js +13 -5
  49. package/dist/resources/extensions/gsd/tools/validate-milestone.js +3 -8
  50. package/dist/resources/extensions/gsd/uat-dispatch.js +94 -0
  51. package/dist/resources/extensions/gsd/uok/audit-toggle.js +31 -2
  52. package/dist/resources/extensions/gsd/uok/audit.js +3 -0
  53. package/dist/resources/extensions/gsd/uok/gate-runner.js +22 -30
  54. package/dist/resources/extensions/gsd/uok/gitops.js +18 -15
  55. package/dist/resources/extensions/gsd/uok/kernel.js +120 -46
  56. package/dist/resources/extensions/gsd/uok/model-policy.js +17 -14
  57. package/dist/resources/extensions/gsd/workflow-event-ledger.js +1 -1
  58. package/dist/resources/extensions/gsd/workflow-logger.js +1 -1
  59. package/dist/resources/extensions/gsd/workflow-projections.js +9 -26
  60. package/dist/resources/extensions/gsd/workspace.js +5 -14
  61. package/dist/resources/extensions/shared/gsd-browser-cli.js +29 -0
  62. package/dist/resources/extensions/shared/interview-ui.js +62 -9
  63. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  64. package/dist/web/standalone/.next/BUILD_ID +1 -1
  65. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  66. package/dist/web/standalone/.next/build-manifest.json +2 -2
  67. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  68. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  95. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  96. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  97. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  98. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  99. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  100. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
  101. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  102. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  103. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  104. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  105. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  106. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  107. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  108. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  109. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  110. package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
  111. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  112. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  113. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  115. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  116. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  117. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  118. package/dist/web/standalone/.next/server/app/index.html +1 -1
  119. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  120. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  121. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  122. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  123. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  124. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  125. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  126. package/dist/web/standalone/.next/server/chunks/{1128.js → 1991.js} +1 -1
  127. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  128. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  129. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  130. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  131. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  132. package/dist/worktree-cli-create.d.ts +10 -0
  133. package/dist/worktree-cli-create.js +11 -0
  134. package/dist/worktree-cli-format.d.ts +12 -0
  135. package/dist/worktree-cli-format.js +38 -0
  136. package/dist/worktree-cli-plan.d.ts +16 -0
  137. package/dist/worktree-cli-plan.js +19 -0
  138. package/dist/worktree-cli-session.d.ts +13 -0
  139. package/dist/worktree-cli-session.js +18 -0
  140. package/dist/worktree-cli-status.d.ts +35 -0
  141. package/dist/worktree-cli-status.js +54 -0
  142. package/dist/worktree-cli.d.ts +1 -16
  143. package/dist/worktree-cli.js +28 -122
  144. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  145. package/integrations/hermes/pyproject.toml +1 -1
  146. package/package.json +1 -1
  147. package/packages/cloud-mcp-gateway/package.json +2 -2
  148. package/packages/contracts/package.json +1 -1
  149. package/packages/daemon/dist/orchestrator-agent.d.ts +21 -0
  150. package/packages/daemon/dist/orchestrator-agent.d.ts.map +1 -0
  151. package/packages/daemon/dist/orchestrator-agent.js +113 -0
  152. package/packages/daemon/dist/orchestrator-agent.js.map +1 -0
  153. package/packages/daemon/dist/orchestrator-tools.d.ts +12 -0
  154. package/packages/daemon/dist/orchestrator-tools.d.ts.map +1 -0
  155. package/packages/daemon/dist/orchestrator-tools.js +137 -0
  156. package/packages/daemon/dist/orchestrator-tools.js.map +1 -0
  157. package/packages/daemon/dist/orchestrator.d.ts +4 -56
  158. package/packages/daemon/dist/orchestrator.d.ts.map +1 -1
  159. package/packages/daemon/dist/orchestrator.js +13 -303
  160. package/packages/daemon/dist/orchestrator.js.map +1 -1
  161. package/packages/daemon/dist/orchestrator.test.js +11 -1
  162. package/packages/daemon/dist/orchestrator.test.js.map +1 -1
  163. package/packages/daemon/package.json +4 -4
  164. package/packages/gsd-agent-core/package.json +5 -5
  165. package/packages/gsd-agent-modes/package.json +7 -7
  166. package/packages/mcp-server/package.json +4 -4
  167. package/packages/native/package.json +1 -1
  168. package/packages/pi-agent-core/package.json +1 -1
  169. package/packages/pi-ai/package.json +1 -1
  170. package/packages/pi-coding-agent/dist/core/package-manager.d.ts +9 -0
  171. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  172. package/packages/pi-coding-agent/dist/core/package-manager.js +74 -53
  173. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +6 -0
  175. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  176. package/packages/pi-coding-agent/dist/core/resource-loader.js +30 -2
  177. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  178. package/packages/pi-coding-agent/dist/core/skill-directories.d.ts +66 -0
  179. package/packages/pi-coding-agent/dist/core/skill-directories.d.ts.map +1 -0
  180. package/packages/pi-coding-agent/dist/core/skill-directories.js +138 -0
  181. package/packages/pi-coding-agent/dist/core/skill-directories.js.map +1 -0
  182. package/packages/pi-coding-agent/dist/core/skills.d.ts +4 -1
  183. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  184. package/packages/pi-coding-agent/dist/core/skills.js +4 -1
  185. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  186. package/packages/pi-coding-agent/dist/index.d.ts +1 -0
  187. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  188. package/packages/pi-coding-agent/dist/index.js +1 -0
  189. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  190. package/packages/pi-coding-agent/package.json +7 -7
  191. package/packages/pi-tui/package.json +2 -2
  192. package/packages/rpc-client/package.json +2 -2
  193. package/pkg/package.json +1 -1
  194. package/src/resources/extensions/ask-user-questions.ts +1 -1
  195. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +146 -33
  196. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +25 -0
  197. package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +93 -0
  198. package/src/resources/extensions/gsd/activity-log.ts +1 -1
  199. package/src/resources/extensions/gsd/auto/orchestrator.ts +17 -11
  200. package/src/resources/extensions/gsd/auto-artifact-paths.ts +14 -4
  201. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +9 -2
  202. package/src/resources/extensions/gsd/auto-dispatch.ts +77 -16
  203. package/src/resources/extensions/gsd/auto-model-selection.ts +33 -1
  204. package/src/resources/extensions/gsd/auto-post-unit.ts +4 -2
  205. package/src/resources/extensions/gsd/auto-prompts.ts +53 -113
  206. package/src/resources/extensions/gsd/auto-recovery.ts +50 -6
  207. package/src/resources/extensions/gsd/auto-start.ts +58 -29
  208. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +12 -2
  209. package/src/resources/extensions/gsd/auto-timers.ts +9 -2
  210. package/src/resources/extensions/gsd/auto.ts +103 -30
  211. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +1 -1
  212. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +66 -6
  213. package/src/resources/extensions/gsd/codebase-generator.ts +7 -1
  214. package/src/resources/extensions/gsd/compat/compat-marker.ts +47 -0
  215. package/src/resources/extensions/gsd/config-overlay.ts +7 -1
  216. package/src/resources/extensions/gsd/db/engine.ts +36 -0
  217. package/src/resources/extensions/gsd/db/writers/cascades.ts +23 -1
  218. package/src/resources/extensions/gsd/db-adapter.ts +29 -3
  219. package/src/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  220. package/src/resources/extensions/gsd/flat-phase-migration.ts +20 -0
  221. package/src/resources/extensions/gsd/gsd-db.ts +20 -11
  222. package/src/resources/extensions/gsd/guided-flow.ts +19 -7
  223. package/src/resources/extensions/gsd/journal.ts +1 -1
  224. package/src/resources/extensions/gsd/markdown-renderer.ts +52 -103
  225. package/src/resources/extensions/gsd/md-importer.ts +93 -22
  226. package/src/resources/extensions/gsd/metrics.ts +2 -2
  227. package/src/resources/extensions/gsd/migration-auto-check.ts +32 -3
  228. package/src/resources/extensions/gsd/paths.ts +108 -51
  229. package/src/resources/extensions/gsd/post-unit-hooks.ts +32 -0
  230. package/src/resources/extensions/gsd/preferences-models.ts +74 -20
  231. package/src/resources/extensions/gsd/preferences-skills.ts +10 -16
  232. package/src/resources/extensions/gsd/preferences-types.ts +24 -1
  233. package/src/resources/extensions/gsd/preferences-validation.ts +77 -9
  234. package/src/resources/extensions/gsd/preferences.ts +20 -0
  235. package/src/resources/extensions/gsd/rule-registry.ts +111 -9
  236. package/src/resources/extensions/gsd/skill-discovery.ts +47 -23
  237. package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +1 -9
  238. package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +2 -22
  239. package/src/resources/extensions/gsd/state-reconciliation/index.ts +14 -0
  240. package/src/resources/extensions/gsd/tests/auto-artifact-paths.test.ts +169 -1
  241. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +41 -2
  242. package/src/resources/extensions/gsd/tests/auto-post-unit-artifact-diagnostic.test.ts +48 -1
  243. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +57 -0
  244. package/src/resources/extensions/gsd/tests/auto-start-project-milestone-reconcile.test.ts +29 -1
  245. package/src/resources/extensions/gsd/tests/auto-timeout-recovery-supervisor-inflight-guard.test.ts +159 -0
  246. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
  247. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +3 -1
  248. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +85 -0
  249. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +45 -0
  250. package/src/resources/extensions/gsd/tests/db-adapter.test.ts +39 -0
  251. package/src/resources/extensions/gsd/tests/db-writable-probe.test.ts +66 -0
  252. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +27 -0
  253. package/src/resources/extensions/gsd/tests/flat-phase-renderer.test.ts +22 -1
  254. package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +72 -32
  255. package/src/resources/extensions/gsd/tests/import-completion-lost-update.test.ts +196 -0
  256. package/src/resources/extensions/gsd/tests/integration/paths.test.ts +88 -1
  257. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +16 -15
  258. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +91 -0
  259. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +2 -1
  260. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
  261. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +269 -0
  262. package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +8 -1
  263. package/src/resources/extensions/gsd/tests/preferences.test.ts +19 -0
  264. package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +54 -0
  265. package/src/resources/extensions/gsd/tests/reassess-handler.test.ts +49 -1
  266. package/src/resources/extensions/gsd/tests/reopen-slice.test.ts +52 -0
  267. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +95 -1
  268. package/src/resources/extensions/gsd/tests/sibling-model-fallbacks.test.ts +189 -0
  269. package/src/resources/extensions/gsd/tests/single-field-model-thinking.test.ts +156 -0
  270. package/src/resources/extensions/gsd/tests/skill-discovery.test.ts +46 -0
  271. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +6 -1
  272. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +32 -0
  273. package/src/resources/extensions/gsd/tests/supervisor-model-swap-inflight-guard.test.ts +181 -0
  274. package/src/resources/extensions/gsd/tests/supervisor-model-swap-thinking-clamp.test.ts +122 -0
  275. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +93 -0
  276. package/src/resources/extensions/gsd/tests/uat-dispatch.test.ts +211 -0
  277. package/src/resources/extensions/gsd/tests/uok-gate-runner.test.ts +39 -0
  278. package/src/resources/extensions/gsd/tests/uok-kernel-path.test.ts +226 -2
  279. package/src/resources/extensions/gsd/tests/uok-model-policy.test.ts +6 -0
  280. package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +213 -2
  281. package/src/resources/extensions/gsd/tools/complete-milestone.ts +9 -5
  282. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +19 -8
  283. package/src/resources/extensions/gsd/tools/reopen-slice.ts +5 -0
  284. package/src/resources/extensions/gsd/tools/reopen-task.ts +13 -5
  285. package/src/resources/extensions/gsd/tools/validate-milestone.ts +13 -9
  286. package/src/resources/extensions/gsd/types.ts +23 -4
  287. package/src/resources/extensions/gsd/uat-dispatch.ts +176 -0
  288. package/src/resources/extensions/gsd/uok/audit-toggle.ts +32 -2
  289. package/src/resources/extensions/gsd/uok/audit.ts +2 -0
  290. package/src/resources/extensions/gsd/uok/gate-runner.ts +26 -36
  291. package/src/resources/extensions/gsd/uok/gitops.ts +21 -18
  292. package/src/resources/extensions/gsd/uok/kernel.ts +169 -48
  293. package/src/resources/extensions/gsd/uok/model-policy.ts +20 -17
  294. package/src/resources/extensions/gsd/workflow-event-ledger.ts +1 -1
  295. package/src/resources/extensions/gsd/workflow-logger.ts +1 -1
  296. package/src/resources/extensions/gsd/workflow-projections.ts +9 -24
  297. package/src/resources/extensions/gsd/workspace.ts +5 -21
  298. package/src/resources/extensions/shared/gsd-browser-cli.ts +29 -0
  299. package/src/resources/extensions/shared/interview-ui.ts +58 -11
  300. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +32 -0
  301. /package/dist/web/standalone/.next/static/{ZpFDazP-38PXBPiI_LdZf → NPMNi2HV8BuK3baQsEUhk}/_buildManifest.js +0 -0
  302. /package/dist/web/standalone/.next/static/{ZpFDazP-38PXBPiI_LdZf → NPMNi2HV8BuK3baQsEUhk}/_ssgManifest.js +0 -0
@@ -1033,6 +1033,26 @@ export function renderPreferencesForSystemPrompt(
1033
1033
  return lines.join("\n");
1034
1034
  }
1035
1035
 
1036
+ /**
1037
+ * Render a compact response-language directive for injection into dispatched
1038
+ * prompt *content* (discuss/QA and auto-execution unit prompts).
1039
+ *
1040
+ * The main session's system prompt already carries the full GSD Skill
1041
+ * Preferences block (including language) via `before_agent_start`, but
1042
+ * discuss/QA turns and auto-execution units are dispatched as separate
1043
+ * turns/sessions whose prompts are assembled here — without that block — so the
1044
+ * configured language never reaches them (#1210). Injecting this directive into
1045
+ * the dispatched prompt content makes the `language` preference effective across
1046
+ * both guided and auto workflows.
1047
+ *
1048
+ * Returns "" when no language is configured.
1049
+ */
1050
+ export function renderLanguageDirectiveForPrompt(preferences: GSDPreferences | undefined): string {
1051
+ if (!preferences?.language) return "";
1052
+ const safeLang = preferences.language.replace(/[\r\n]/g, " ").slice(0, 50);
1053
+ return `## Response Language\n\nAlways respond in ${safeLang} — questions, explanations, and summaries.`;
1054
+ }
1055
+
1036
1056
  // ─── Hook Resolution ──────────────────────────────────────────────────────────
1037
1057
 
1038
1058
  /**
@@ -22,8 +22,9 @@ import type {
22
22
  } from "./types.js";
23
23
  import { resolvePostUnitHooks, resolvePreDispatchHooks } from "./preferences.js";
24
24
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
25
- import { join } from "node:path";
25
+ import { dirname, join } from "node:path";
26
26
  import { parseUnitId } from "./unit-id.js";
27
+ import { resolveMilestonePath } from "./paths.js";
27
28
  import { queryJournal, type JournalEntry } from "./journal.js";
28
29
  import { readUnitRuntimeRecord, type UnitRuntimePhase } from "./unit-runtime.js";
29
30
  import { extractFrontmatterVerdict } from "./verdict-parser.js";
@@ -32,13 +33,45 @@ import { extractFrontmatterVerdict } from "./verdict-parser.js";
32
33
 
33
34
  export function resolveHookArtifactPath(basePath: string, unitId: string, artifactName: string): string {
34
35
  const { milestone, slice, task } = parseUnitId(unitId);
35
- if (task !== undefined && slice !== undefined) {
36
- return join(basePath, ".gsd", "milestones", milestone, "slices", slice, "tasks", `${task}-${artifactName}`);
36
+
37
+ // Prefer the active phase directory (flat-phase layout: .gsd/phases/<NN>-.../).
38
+ // Configured gate artifacts are canonical phase-level files (e.g.
39
+ // BROWSER-RUNTIME-EVIDENCE.md), so resolve the exact declared name first,
40
+ // then a task-prefixed variant for retry sentinels / rework briefs.
41
+ const candidates: string[] = [];
42
+ const phaseDir = resolveMilestonePath(basePath, milestone);
43
+ const isFlatPhaseDir = phaseDir !== null
44
+ && !(dirname(phaseDir).endsWith("/milestones") || dirname(phaseDir).endsWith("\\milestones"));
45
+ if (isFlatPhaseDir) {
46
+ candidates.push(join(phaseDir, artifactName));
47
+ if (task !== undefined) {
48
+ candidates.push(join(phaseDir, `${task}-${artifactName}`));
49
+ }
37
50
  }
38
- if (slice !== undefined) {
39
- return join(basePath, ".gsd", "milestones", milestone, "slices", slice, artifactName);
51
+
52
+ // Legacy nested layout fallbacks (.gsd/milestones/<M>/slices/<S>/tasks/...).
53
+ const legacyBase = join(basePath, ".gsd", "milestones", milestone);
54
+ let legacyDefault: string;
55
+ if (task !== undefined && slice !== undefined) {
56
+ legacyDefault = join(legacyBase, "slices", slice, "tasks", `${task}-${artifactName}`);
57
+ candidates.push(legacyDefault);
58
+ candidates.push(join(legacyBase, "slices", slice, "tasks", artifactName));
59
+ candidates.push(join(legacyBase, "slices", slice, artifactName));
60
+ } else if (slice !== undefined) {
61
+ legacyDefault = join(legacyBase, "slices", slice, artifactName);
62
+ candidates.push(legacyDefault);
63
+ } else {
64
+ legacyDefault = join(legacyBase, artifactName);
40
65
  }
41
- return join(basePath, ".gsd", "milestones", milestone, artifactName);
66
+ candidates.push(join(legacyBase, artifactName));
67
+
68
+ const existing = candidates.find((candidate) => existsSync(candidate));
69
+ if (existing) return existing;
70
+
71
+ // Nothing on disk yet: prefer the active phase path when the phase dir is
72
+ // known, otherwise fall back to the legacy default (preserves pre-flat-phase
73
+ // behavior for missing-artifact diagnostics).
74
+ return isFlatPhaseDir ? join(phaseDir!, artifactName) : legacyDefault;
42
75
  }
43
76
 
44
77
  // ─── Dispatch Rule Conversion ──────────────────────────────────────────────
@@ -126,6 +159,13 @@ export class RuleRegistry {
126
159
  forceRun?: boolean;
127
160
  }> = [];
128
161
  cycleCounts: Map<string, number> = new Map();
162
+ /**
163
+ * Cycle keys that have already been granted a one-shot re-dispatch after a
164
+ * lost/interrupted dispatch (hook charged a cycle but never produced its own
165
+ * unit-end). Bounds the dispatch-cycle refund to exactly one per gate so a
166
+ * hook that repeatedly fails to complete cannot loop forever.
167
+ */
168
+ redispatchedGateKeys: Set<string> = new Set();
129
169
  retryPending: boolean = false;
130
170
  retryTrigger: { unitType: string; unitId: string; retryArtifact?: string } | null = null;
131
171
  hookFailure: HookFailureState | null = null;
@@ -364,6 +404,14 @@ export class RuleRegistry {
364
404
  pendingRetry: false,
365
405
  };
366
406
 
407
+ return this._buildHookDispatch(config, triggerUnitId);
408
+ }
409
+
410
+ /** Construct the sidecar dispatch for a hook without mutating registry state. */
411
+ private _buildHookDispatch(
412
+ config: PostUnitHookConfig,
413
+ triggerUnitId: string,
414
+ ): HookDispatchResult {
367
415
  const { milestone: mid, slice: sid, task: tid } = parseUnitId(triggerUnitId);
368
416
  let prompt = config.prompt
369
417
  .replace(/\{milestoneId\}/g, mid ?? "")
@@ -375,12 +423,29 @@ export class RuleRegistry {
375
423
  return {
376
424
  hookName: config.name,
377
425
  prompt,
378
- model: config.model,
426
+ // Model selection (including fallbacks[]) is handled by
427
+ // resolveModelWithFallbacksForUnit for the `hook/<name>` unit type (#1229).
379
428
  unitType: `hook/${config.name}`,
380
429
  unitId: triggerUnitId,
381
430
  };
382
431
  }
383
432
 
433
+ /**
434
+ * Reconstruct the in-flight hook's dispatch from the restored `activeHook`
435
+ * state, without mutating the registry. Used to reconcile a persisted
436
+ * `activeHook` whose session-local dispatch was lost across a pause/resume
437
+ * or crash-recovery, so the hook actually runs instead of being charged
438
+ * against the next unrelated unit's close-out (#1246).
439
+ */
440
+ getPendingHookDispatch(basePath: string): HookDispatchResult | null {
441
+ if (!this.activeHook) return null;
442
+ const config = resolvePostUnitHooks(basePath).find(
443
+ h => h.name === this.activeHook!.hookName,
444
+ );
445
+ if (!config) return null;
446
+ return this._buildHookDispatch(config, this.activeHook.triggerUnitId);
447
+ }
448
+
384
449
  private _assessHookCompletion(
385
450
  basePath: string,
386
451
  hook: HookExecutionState,
@@ -503,6 +568,30 @@ export class RuleRegistry {
503
568
  observedCleanExecution: boolean,
504
569
  ): HookDispatchResult | null {
505
570
  if (!observedCleanExecution) {
571
+ // The hook was charged a cycle at dispatch but never produced its own
572
+ // unit-end — its dispatch was lost/interrupted (pause/resume, crash
573
+ // recovery). Refund that dispatch-consumed cycle exactly once so the gate
574
+ // is re-dispatched at least once instead of hard-blocking on a hook that
575
+ // never actually ran.
576
+ //
577
+ // This refund is intentionally UNCONDITIONAL on the first lost dispatch —
578
+ // it must NOT be gated on `currentCycle >= max_cycles`. A lost dispatch is
579
+ // a non-run: charging its cycle at dispatch time must not consume the
580
+ // hook's real-run budget. Gating the refund on "budget already exhausted"
581
+ // lets a lost dispatch below max_cycles silently eat a genuine cycle, so
582
+ // the hook gets one fewer real run than configured — reintroducing #1244
583
+ // for max_cycles >= 2 (see the "does not steal a real-run cycle" test).
584
+ // The redispatchedGateKeys flag bounds this to a single refund per gate,
585
+ // so repeated lost dispatches still terminate (total dispatches never
586
+ // exceed max_cycles + 1) and a hook that keeps failing blocks normally.
587
+ const cycleKey = hookCycleKey(config, hook);
588
+ if (!this.redispatchedGateKeys.has(cycleKey)) {
589
+ this.redispatchedGateKeys.add(cycleKey);
590
+ const currentCycle = this.cycleCounts.get(cycleKey);
591
+ if (typeof currentCycle === "number" && currentCycle > 0) {
592
+ this.cycleCounts.set(cycleKey, currentCycle - 1);
593
+ }
594
+ }
506
595
  return this._rerunGateOrBlock(config, hook, basePath, {
507
596
  reason: `hook/${config.name} did not complete cleanly before the trigger unit resumed`,
508
597
  });
@@ -839,6 +928,7 @@ export class RuleRegistry {
839
928
  this.activeHook = null;
840
929
  this.hookQueue = [];
841
930
  this.cycleCounts.clear();
931
+ this.redispatchedGateKeys.clear();
842
932
  this.retryPending = false;
843
933
  this.retryTrigger = null;
844
934
  this.hookFailure = null;
@@ -855,6 +945,7 @@ export class RuleRegistry {
855
945
  persistState(basePath: string): void {
856
946
  const state: PersistedHookState = {
857
947
  cycleCounts: Object.fromEntries(this.cycleCounts),
948
+ redispatchedGateKeys: Array.from(this.redispatchedGateKeys),
858
949
  activeHook: this.activeHook ? { ...this.activeHook } : null,
859
950
  hookQueue: this.hookQueue.map(entry => ({
860
951
  hookName: entry.config.name,
@@ -888,6 +979,14 @@ export class RuleRegistry {
888
979
  }
889
980
  }
890
981
  }
982
+ this.redispatchedGateKeys.clear();
983
+ if (Array.isArray(state.redispatchedGateKeys)) {
984
+ for (const key of state.redispatchedGateKeys) {
985
+ if (typeof key === "string") {
986
+ this.redispatchedGateKeys.add(key);
987
+ }
988
+ }
989
+ }
891
990
  this.activeHook = state.activeHook && typeof state.activeHook === "object"
892
991
  ? { ...state.activeHook }
893
992
  : null;
@@ -918,7 +1017,7 @@ export class RuleRegistry {
918
1017
  if (existsSync(filePath)) {
919
1018
  writeFileSync(
920
1019
  filePath,
921
- JSON.stringify({ cycleCounts: {}, activeHook: null, hookQueue: [], savedAt: new Date().toISOString() }, null, 2),
1020
+ JSON.stringify({ cycleCounts: {}, redispatchedGateKeys: [], activeHook: null, hookQueue: [], savedAt: new Date().toISOString() }, null, 2),
922
1021
  "utf-8",
923
1022
  );
924
1023
  }
@@ -1014,7 +1113,10 @@ export class RuleRegistry {
1014
1113
  return {
1015
1114
  hookName: hook.name,
1016
1115
  prompt,
1017
- model: hook.model,
1116
+ // Model selection (including fallbacks[]) is resolved by dispatchHookUnit
1117
+ // via resolveModelWithFallbacksForUnit for the `hook/<name>` unit type,
1118
+ // matching the auto-mode path. Emitting the primary-only model here would
1119
+ // discard the configured fallback chain (#1229).
1018
1120
  unitType: `hook/${hook.name}`,
1019
1121
  unitId,
1020
1122
  };
@@ -9,8 +9,8 @@
9
9
  */
10
10
 
11
11
  import { existsSync, readdirSync, readFileSync, type Dirent } from "node:fs";
12
- import { homedir } from "node:os";
13
12
  import { join } from "node:path";
13
+ import { getSkillDirectories } from "@gsd/pi-coding-agent";
14
14
  import { gsdHome } from "./gsd-home.js";
15
15
  import { getInstalledSkills, normalizeSkillName } from "./skills.js";
16
16
 
@@ -20,28 +20,47 @@ export interface DiscoveredSkill {
20
20
  location: string;
21
21
  }
22
22
 
23
- /** Snapshot of normalized skill names at auto-mode start */
24
- let baselineSkills: Set<string> | null = null;
23
+ /**
24
+ * Snapshot state. `searchDirs` is captured at snapshot time so every detection
25
+ * pass during the session scans the same project + user dirs (the bug fix:
26
+ * previously only user dirs were scanned, so skills installed into the project
27
+ * mid-session were silently missed).
28
+ */
29
+ interface SkillSnapshotState {
30
+ baselineNames: Set<string>;
31
+ searchDirs: string[];
32
+ }
33
+
34
+ let snapshot: SkillSnapshotState | null = null;
25
35
 
26
36
  /**
27
37
  * Snapshot the current installed skill catalog. Call at auto-mode start.
38
+ * `cwd` is the project base path; skills dropped into `<cwd>/.agents/skills/`,
39
+ * `<cwd>/.claude/skills/`, or `<cwd>/.gsd/skills/` mid-session are detected.
28
40
  */
29
- export function snapshotSkills(): void {
30
- baselineSkills = snapshotCurrentSkillNames();
41
+ export function snapshotSkills(options?: { cwd?: string }): void {
42
+ const searchDirs = getSkillDirectories({
43
+ cwd: options?.cwd ?? process.cwd(),
44
+ gsdHome: gsdHome(),
45
+ }).map((entry) => entry.path);
46
+ snapshot = {
47
+ baselineNames: snapshotCurrentSkillNames(searchDirs),
48
+ searchDirs,
49
+ };
31
50
  }
32
51
 
33
52
  /**
34
53
  * Clear the snapshot. Call when auto-mode stops.
35
54
  */
36
55
  export function clearSkillSnapshot(): void {
37
- baselineSkills = null;
56
+ snapshot = null;
38
57
  }
39
58
 
40
59
  /**
41
60
  * Check if a snapshot is active (auto-mode is running with discovery).
42
61
  */
43
62
  export function hasSkillSnapshot(): boolean {
44
- return baselineSkills !== null;
63
+ return snapshot !== null;
45
64
  }
46
65
 
47
66
  /**
@@ -49,12 +68,12 @@ export function hasSkillSnapshot(): boolean {
49
68
  * Returns skill metadata for any new skills found in the loader catalog or on disk.
50
69
  */
51
70
  export function detectNewSkills(): DiscoveredSkill[] {
52
- if (!baselineSkills) return [];
71
+ if (!snapshot) return [];
53
72
 
54
73
  const newSkills: DiscoveredSkill[] = [];
55
- for (const skill of getCurrentSkillsForDiscovery()) {
74
+ for (const skill of getCurrentSkillsForDiscovery(snapshot.searchDirs)) {
56
75
  const normalized = normalizeSkillName(skill.name);
57
- if (baselineSkills.has(normalized)) continue;
76
+ if (snapshot.baselineNames.has(normalized)) continue;
58
77
  newSkills.push(skill);
59
78
  }
60
79
 
@@ -84,7 +103,13 @@ export async function refreshCatalogForNewSkills(options?: {
84
103
  }
85
104
  }
86
105
 
87
- snapshotSkills();
106
+ // Re-snapshot, preserving the search dirs captured at auto-mode start.
107
+ if (snapshot) {
108
+ snapshot = {
109
+ baselineNames: snapshotCurrentSkillNames(),
110
+ searchDirs: snapshot.searchDirs,
111
+ };
112
+ }
88
113
  const names = newSkills.map((skill) => skill.name).join(", ");
89
114
  options?.notify?.(`GSD: loaded new skills: ${names}`, "info");
90
115
  return newSkills;
@@ -132,19 +157,18 @@ function escapeXml(value: string): string {
132
157
  });
133
158
  }
134
159
 
135
- function skillSearchDirs(): string[] {
136
- return [
137
- join(gsdHome(), "agent", "skills"),
138
- join(homedir(), ".agents", "skills"),
139
- join(homedir(), ".claude", "skills"),
140
- ];
160
+ function snapshotCurrentSkillNames(searchDirsOverride?: string[]): Set<string> {
161
+ // Uses explicit dirs, the snapshot's search dirs when active, or user-only dirs
162
+ // (no project context) when called before a snapshot is taken.
163
+ const searchDirs = searchDirsOverride ?? snapshot?.searchDirs ?? defaultSearchDirs();
164
+ return new Set(getCurrentSkillsForDiscovery(searchDirs).map(skill => normalizeSkillName(skill.name)));
141
165
  }
142
166
 
143
- function snapshotCurrentSkillNames(): Set<string> {
144
- return new Set(getCurrentSkillsForDiscovery().map(skill => normalizeSkillName(skill.name)));
167
+ function defaultSearchDirs(): string[] {
168
+ return getSkillDirectories({ cwd: process.cwd(), gsdHome: gsdHome() }).map((entry) => entry.path);
145
169
  }
146
170
 
147
- function getCurrentSkillsForDiscovery(): DiscoveredSkill[] {
171
+ function getCurrentSkillsForDiscovery(searchDirs: string[]): DiscoveredSkill[] {
148
172
  const skills = new Map<string, DiscoveredSkill>();
149
173
  for (const skill of getInstalledSkills()) {
150
174
  const normalized = normalizeSkillName(skill.name);
@@ -155,7 +179,7 @@ function getCurrentSkillsForDiscovery(): DiscoveredSkill[] {
155
179
  });
156
180
  }
157
181
 
158
- for (const skill of getDiskSkills()) {
182
+ for (const skill of getDiskSkills(searchDirs)) {
159
183
  const normalized = normalizeSkillName(skill.name);
160
184
  if (!skills.has(normalized)) skills.set(normalized, skill);
161
185
  }
@@ -163,9 +187,9 @@ function getCurrentSkillsForDiscovery(): DiscoveredSkill[] {
163
187
  return [...skills.values()];
164
188
  }
165
189
 
166
- function getDiskSkills(): DiscoveredSkill[] {
190
+ function getDiskSkills(searchDirs: string[]): DiscoveredSkill[] {
167
191
  const skills = new Map<string, DiscoveredSkill>();
168
- for (const dir of skillSearchDirs()) {
192
+ for (const dir of searchDirs) {
169
193
  if (!existsSync(dir)) continue;
170
194
  let entries: Dirent[];
171
195
  try {
@@ -213,21 +213,13 @@ function detectArtifactDbStatusDriftForMilestone(
213
213
 
214
214
  for (const task of tasks) {
215
215
  if (isClosedStatus(task.status)) continue;
216
- // Flat-phase: task summaries live in the phase dir as TID-SUMMARY.md
217
- let diskTaskSummary = resolveTaskFile(
216
+ const diskTaskSummary = resolveTaskFile(
218
217
  basePath,
219
218
  milestoneId,
220
219
  slice.id,
221
220
  task.id,
222
221
  "SUMMARY",
223
222
  );
224
- if (!diskTaskSummary) {
225
- const phaseDir = resolveMilestonePath(basePath, milestoneId);
226
- if (phaseDir) {
227
- const flatSummary = join(phaseDir, `${task.id}-SUMMARY.md`);
228
- if (existsSync(flatSummary)) diskTaskSummary = flatSummary;
229
- }
230
- }
231
223
  if (!diskTaskSummary || !existsSync(diskTaskSummary)) continue;
232
224
  addUniqueDrift(drifts, seen, {
233
225
  kind: "artifact-db-status-divergence",
@@ -20,11 +20,9 @@ import {
20
20
  } from "../../gsd-db.js";
21
21
  import {
22
22
  resolveMilestoneFile,
23
- resolveMilestonePath,
24
23
  resolveSliceFile,
25
24
  resolveTaskFile,
26
25
  } from "../../paths.js";
27
- import { join } from "node:path";
28
26
  import type { GSDState } from "../../types.js";
29
27
  import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
30
28
 
@@ -105,23 +103,13 @@ function collectMilestoneCompletionDrift(
105
103
  for (const task of getSliceTasks(mid, slice.id)) {
106
104
  if (!COMPLETE_STATUSES.has(task.status)) continue;
107
105
  if (task.completed_at !== null) continue;
108
- // Flat-phase: task summaries live in the phase dir as TID-SUMMARY.md.
109
- // Legacy: resolveTaskFile returns the tasks/TID/TID-SUMMARY.md path.
110
- let taskSummary = resolveTaskFile(
106
+ const taskSummary = resolveTaskFile(
111
107
  ctx.basePath,
112
108
  mid,
113
109
  slice.id,
114
110
  task.id,
115
111
  "SUMMARY",
116
112
  );
117
- // Flat-phase fallback: check the phase dir directly
118
- if (!taskSummary) {
119
- const phaseDir = resolveMilestonePath(ctx.basePath, mid);
120
- if (phaseDir) {
121
- const flatSummary = join(phaseDir, `${task.id}-SUMMARY.md`);
122
- if (existsSync(flatSummary)) taskSummary = flatSummary;
123
- }
124
- }
125
113
  if (taskSummary && existsSync(taskSummary)) {
126
114
  drifts.push({
127
115
  kind: "missing-completion-timestamp",
@@ -182,15 +170,7 @@ export function repairMissingCompletionTimestamp(
182
170
  task.completed_at !== null ||
183
171
  !COMPLETE_STATUSES.has(task.status)
184
172
  ) return;
185
- // Flat-phase: task summaries live in the phase dir as TID-SUMMARY.md
186
- let summary = resolveTaskFile(ctx.basePath, mid, sid, tid, "SUMMARY");
187
- if (!summary) {
188
- const phaseDir = resolveMilestonePath(ctx.basePath, mid);
189
- if (phaseDir) {
190
- const flatSummary = join(phaseDir, `${tid}-SUMMARY.md`);
191
- if (existsSync(flatSummary)) summary = flatSummary;
192
- }
193
- }
173
+ const summary = resolveTaskFile(ctx.basePath, mid, sid, tid, "SUMMARY");
194
174
  const ts = summary ? summaryMtimeIso(summary) : null;
195
175
  if (!ts) return;
196
176
  updateTaskStatus(mid, sid, tid, task.status, ts);
@@ -73,6 +73,20 @@ export async function reconcileBeforeDispatch(
73
73
  if (!deps.dryRun) {
74
74
  const { capturePlanningCompatIfNeeded } = await import("../compat/planning-compat.js");
75
75
  await capturePlanningCompatIfNeeded(basePath);
76
+
77
+ // Self-heal phantom projection entries (#1257): a renamed/removed phase
78
+ // directory leaves stale `.compat.json` projection paths that no detector
79
+ // ever prunes (they all skip missing files), so the marker drifts from disk
80
+ // reality permanently. Drop entries whose backing file is gone before the
81
+ // detect passes run. Gated on !dryRun to keep detection read-only.
82
+ const { pruneOrphanedProjectionEntries } = await import("../compat/compat-marker.js");
83
+ const prunedCount = pruneOrphanedProjectionEntries(basePath);
84
+ if (prunedCount > 0) {
85
+ logWarning(
86
+ "reconcile",
87
+ `pruned ${prunedCount} orphaned projection entr${prunedCount === 1 ? "y" : "ies"} from .compat.json (backing file no longer on disk)`,
88
+ );
89
+ }
76
90
  }
77
91
 
78
92
  for (let pass = 0; pass < MAX_PASSES; pass++) {
@@ -1,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { mkdtempSync, mkdirSync, realpathSync, rmSync, utimesSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
 
@@ -360,6 +360,174 @@ test("canonical worktree + project-root legacy dir produces legacy filename (#bu
360
360
  }
361
361
  });
362
362
 
363
+ // ── cursor[bot] 3523932401: bare milestone id must not match a team-suffix ────
364
+ //
365
+ // A bare milestone id (M001, no team-mode suffix) resolves its phase dir by
366
+ // scanning phases/NN-*. Before the guard (commit 8f45bf3),
367
+ // phaseDirMatchesMilestoneId returned true for ANY same-number dir, so a
368
+ // leftover team-mode projection (phases/01-<6char>-.../, from a prior
369
+ // team-suffixed run) matched the bare id too. When that stale dir carried a
370
+ // newer mtime, pickPreferredPhaseDir selected it over the real title dir and
371
+ // resolved the ROADMAP into a directory that has no ROADMAP — the file-not-found
372
+ // retry loop. The guard (slugLooksLikeTeamSuffixProjection) rejects
373
+ // team-suffix-looking slugs for bare ids in the primary pass; the suffixed
374
+ // projection stays available only as a fallback when the primary pass finds
375
+ // nothing (#1195, guarded by the second test below).
376
+
377
+ test("bare milestone id prefers the real title dir over a newer team-suffix projection (cursor 3523932401)", () => {
378
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-bare-teamsuffix-")));
379
+ try {
380
+ const phasesDir = join(root, ".gsd", "phases");
381
+ // Intended flat-phase dir for the bare id: 01-<title-slug>/ with the ROADMAP.
382
+ const intendedDir = join(phasesDir, "01-clean-continuation");
383
+ mkdirSync(intendedDir, { recursive: true });
384
+ writeFileSync(join(intendedDir, "01-ROADMAP.md"), "# roadmap\n");
385
+
386
+ // Leftover team-mode projection: 01-<6char>-.../ with NO ROADMAP, made
387
+ // NEWER so an mtime tiebreak would wrongly prefer it without the guard.
388
+ const staleDir = join(phasesDir, "01-re4q3k-old-team-projection");
389
+ mkdirSync(staleDir, { recursive: true });
390
+ const newer = new Date(Date.now() + 60_000);
391
+ const older = new Date(Date.now() - 60_000);
392
+ utimesSync(staleDir, newer, newer);
393
+ utimesSync(intendedDir, older, older);
394
+
395
+ _clearGsdRootCache();
396
+ clearPathCache();
397
+
398
+ const resolved = resolveExpectedArtifactPath("plan-milestone", "M001", root);
399
+ assert.equal(
400
+ resolved,
401
+ join(intendedDir, "01-ROADMAP.md"),
402
+ "bare id must resolve to the real title dir, not the newer team-suffix projection",
403
+ );
404
+ assert.notEqual(
405
+ resolved,
406
+ join(staleDir, "01-ROADMAP.md"),
407
+ "must NOT resolve into the team-suffix projection dir that has no ROADMAP",
408
+ );
409
+ } finally {
410
+ _clearGsdRootCache();
411
+ clearPathCache();
412
+ rmSync(root, { recursive: true, force: true });
413
+ }
414
+ });
415
+
416
+ test("bare milestone id still falls back to a lone team-suffix projection (#1195)", () => {
417
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-bare-teamsuffix-only-")));
418
+ try {
419
+ const phasesDir = join(root, ".gsd", "phases");
420
+ // The only on-disk dir for the bare id is a team-mode projection. With no
421
+ // non-suffixed candidate, the fallback pass must still resolve to it.
422
+ const onlyDir = join(phasesDir, "01-re4q3k-clean-continuation");
423
+ mkdirSync(onlyDir, { recursive: true });
424
+ writeFileSync(join(onlyDir, "01-ROADMAP.md"), "# roadmap\n");
425
+
426
+ _clearGsdRootCache();
427
+ clearPathCache();
428
+
429
+ const resolved = resolveExpectedArtifactPath("plan-milestone", "M001", root);
430
+ assert.equal(
431
+ resolved,
432
+ join(onlyDir, "01-ROADMAP.md"),
433
+ "with no non-suffixed dir, the team-suffix projection is the correct fallback",
434
+ );
435
+ } finally {
436
+ _clearGsdRootCache();
437
+ clearPathCache();
438
+ rmSync(root, { recursive: true, force: true });
439
+ }
440
+ });
441
+
442
+ // ── #1208: flat-phase execute-task summaries resolve at the phase root ────────
443
+ //
444
+ // In flat-phase layout task summaries are written beside the plan files
445
+ // (phases/<phase>/T##-SUMMARY.md), NOT under a tasks/ subdir. A tasks/ dir may
446
+ // still exist to hold auxiliary task-scoped gate artifacts (e.g. T01-VERIFY.json).
447
+ // Before the fix, the mere existence of that tasks/ dir redirected summary
448
+ // verification into tasks/T##-SUMMARY.md — which never exists — trapping auto-mode
449
+ // in a false verification retry that eventually paused after a successful task.
450
+
451
+ test("flat-phase execute-task summary resolves at phase root when no tasks/ dir exists (#1208)", () => {
452
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-flat-task-summary-")));
453
+ try {
454
+ const gsd = join(root, ".gsd");
455
+ const phaseDir = join(gsd, "phases", "49-end-to-end-mandates");
456
+ mkdirSync(phaseDir, { recursive: true });
457
+ writeFileSync(join(phaseDir, "49-03-PLAN.md"), "# plan\n- [x] T02\n");
458
+
459
+ _clearGsdRootCache();
460
+ clearPathCache();
461
+
462
+ assert.equal(
463
+ resolveExpectedArtifactPath("execute-task", "M049/S03/T02", root),
464
+ join(phaseDir, "T02-SUMMARY.md"),
465
+ );
466
+ } finally {
467
+ _clearGsdRootCache();
468
+ clearPathCache();
469
+ rmSync(root, { recursive: true, force: true });
470
+ }
471
+ });
472
+
473
+ test("flat-phase execute-task summary resolves at phase root even when tasks/ dir holds gate artifacts (#1208)", () => {
474
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-flat-task-tasksdir-")));
475
+ try {
476
+ const gsd = join(root, ".gsd");
477
+ const phaseDir = join(gsd, "phases", "49-end-to-end-mandates");
478
+ mkdirSync(phaseDir, { recursive: true });
479
+ writeFileSync(join(phaseDir, "49-03-PLAN.md"), "# plan\n- [x] T02\n");
480
+ // Auxiliary task-scoped gate artifact — creates the tasks/ dir but must not
481
+ // redirect the T02 summary into tasks/.
482
+ const tasksDir = join(phaseDir, "tasks");
483
+ mkdirSync(tasksDir, { recursive: true });
484
+ writeFileSync(join(tasksDir, "T01-VERIFY.json"), '{"ok":true}');
485
+
486
+ _clearGsdRootCache();
487
+ clearPathCache();
488
+
489
+ const resolved = resolveExpectedArtifactPath("execute-task", "M049/S03/T02", root);
490
+ assert.equal(
491
+ resolved,
492
+ join(phaseDir, "T02-SUMMARY.md"),
493
+ "flat-phase task summary must resolve at the phase root, not tasks/",
494
+ );
495
+ assert.notEqual(
496
+ resolved,
497
+ join(tasksDir, "T02-SUMMARY.md"),
498
+ "a tasks/ dir with gate artifacts must NOT redirect summary resolution into tasks/",
499
+ );
500
+ } finally {
501
+ _clearGsdRootCache();
502
+ clearPathCache();
503
+ rmSync(root, { recursive: true, force: true });
504
+ }
505
+ });
506
+
507
+ test("legacy execute-task summary still resolves under slices/<SID>/tasks/ (#1208 regression guard)", () => {
508
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-legacy-task-summary-")));
509
+ try {
510
+ const milestoneDir = join(root, ".gsd", "milestones", "M001");
511
+ const sliceDir = join(milestoneDir, "slices", "S01");
512
+ const tasksDir = join(sliceDir, "tasks");
513
+ mkdirSync(tasksDir, { recursive: true });
514
+ writeFileSync(join(milestoneDir, "M001-CONTEXT.md"), "# context\n");
515
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), "# plan\n");
516
+
517
+ _clearGsdRootCache();
518
+ clearPathCache();
519
+
520
+ assert.equal(
521
+ resolveExpectedArtifactPath("execute-task", "M001/S01/T02", root),
522
+ join(tasksDir, "T02-SUMMARY.md"),
523
+ );
524
+ } finally {
525
+ _clearGsdRootCache();
526
+ clearPathCache();
527
+ rmSync(root, { recursive: true, force: true });
528
+ }
529
+ });
530
+
363
531
  // ── resolveSliceResearchLocation / resolveExistingSliceResearchPath ───────────
364
532
  //
365
533
  // Added in the code-quality consolidation PR as the shared dual-path resolver