@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
@@ -116,7 +116,8 @@ import {
116
116
  } from "./auto-tool-tracking.js";
117
117
  import { closeoutUnit } from "./auto-unit-closeout.js";
118
118
  import { recoverTimedOutUnit } from "./auto-timeout-recovery.js";
119
- import { selectAndApplyModel, resolveModelId, clearToolBaseline } from "./auto-model-selection.js";
119
+ import { selectAndApplyModel, resolveModelId, clearToolBaseline, isModelUnavailable, applyThinkingLevelForModel } from "./auto-model-selection.js";
120
+ import { resolveModelWithFallbacksForUnit } from "./preferences-models.js";
120
121
  import { resetRoutingHistory, recordOutcome } from "./routing-history.js";
121
122
  import {
122
123
  checkPostUnitHooks,
@@ -127,6 +128,7 @@ import {
127
128
  runPreDispatchHooks,
128
129
  persistHookState,
129
130
  restoreHookState,
131
+ reconcileRestoredHookDispatch,
130
132
  clearPersistedHookState,
131
133
  } from "./post-unit-hooks.js";
132
134
  import { runGSDDoctor, rebuildState } from "./doctor.js";
@@ -615,11 +617,27 @@ export function startAutoDetached(
615
617
  milestoneLock?: string | null;
616
618
  },
617
619
  ): void {
618
- void withDetachedAutoKeepalive(startAuto(ctx, pi, base, verboseMode, options)).catch((err) => {
620
+ void withDetachedAutoKeepalive(startAuto(ctx, pi, base, verboseMode, options)).catch(async (err) => {
619
621
  const message = getErrorMessage(err);
620
622
  ctx.ui.notify(`Auto-start failed: ${message}`, "error");
621
623
  logWarning("engine", `auto start error: ${message}`, { file: "auto.ts" });
622
624
  debugLog("auto-start-failed", { error: message });
625
+ // Backstop cleanup (#1235): if startAuto threw after auto-mode was activated
626
+ // (e.g. an exception during bootstrap, before the loop's try/finally could
627
+ // run cleanupAfterLoopExit), s.active and the on-disk auto lock leak, so the
628
+ // re-entry guard silently no-ops every later /gsd auto until restart. Clear
629
+ // the leaked activation here so a failed start is always recoverable.
630
+ if (s.active) {
631
+ try {
632
+ await cleanupAfterLoopExit(ctx);
633
+ } catch (cleanupErr) {
634
+ logWarning(
635
+ "engine",
636
+ `auto start cleanup failed: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`,
637
+ { file: "auto.ts" },
638
+ );
639
+ }
640
+ }
623
641
  });
624
642
  }
625
643
 
@@ -2827,6 +2845,10 @@ export async function startAuto(
2827
2845
  "info",
2828
2846
  );
2829
2847
  restoreHookState(s.basePath);
2848
+ // A restored activeHook has no live dispatch (the sidecar queue is not
2849
+ // persisted); re-enqueue it so the hook runs instead of blocking the next
2850
+ // unrelated unit's close-out (#1246).
2851
+ reconcileRestoredHookDispatch(s.basePath, s.sidecarQueue);
2830
2852
  // Re-sync managed resources on resume so long-lived auto sessions pick up
2831
2853
  // bundled extension updates before resume-time verification/state logic runs.
2832
2854
  // GSD_PKG_ROOT is set by loader.ts and points to the gsd-pi package root.
@@ -2895,15 +2917,18 @@ export async function startAuto(
2895
2917
  debugLog("resume-orchestration-resume", { error: err instanceof Error ? err.message : String(err) });
2896
2918
  }
2897
2919
  startAutoCommandPolling(s.basePath);
2898
- await runAutoLoopWithUok({
2899
- ctx,
2900
- pi,
2901
- s,
2902
- deps: loopDeps,
2903
- runKernelLoop: runUokKernelLoop,
2904
- runLegacyLoop: runLegacyAutoLoop,
2905
- });
2906
- await cleanupAfterLoopExit(ctx);
2920
+ try {
2921
+ await runAutoLoopWithUok({
2922
+ ctx,
2923
+ pi,
2924
+ s,
2925
+ deps: loopDeps,
2926
+ runKernelLoop: runUokKernelLoop,
2927
+ runLegacyLoop: runLegacyAutoLoop,
2928
+ });
2929
+ } finally {
2930
+ await cleanupAfterLoopExit(ctx);
2931
+ }
2907
2932
  return;
2908
2933
  }
2909
2934
 
@@ -2931,7 +2956,15 @@ export async function startAuto(
2931
2956
  bootstrapDeps,
2932
2957
  freshStartAssessment,
2933
2958
  );
2934
- if (!ready) return;
2959
+ if (!ready) {
2960
+ // bootstrapAutoSession sets s.active = true (auto-start.ts) before several
2961
+ // post-activation bail-outs return false without a loop ever running (e.g.
2962
+ // the SQLite-unavailable gate). Without this, s.active and the auto lock
2963
+ // leak and every later /gsd auto silently no-ops via the re-entry guard
2964
+ // (#1235). Run the canonical cleanup so a failed bootstrap stays recoverable.
2965
+ if (s.active) await cleanupAfterLoopExit(ctx);
2966
+ return;
2967
+ }
2935
2968
 
2936
2969
  // Build scope after bootstrap has populated s.basePath / s.originalBasePath /
2937
2970
  // s.currentMilestoneId (including worktree setup inside bootstrapAutoSession).
@@ -2957,15 +2990,18 @@ export async function startAuto(
2957
2990
  startAutoCommandPolling(s.basePath);
2958
2991
 
2959
2992
  // Dispatch the first unit
2960
- await runAutoLoopWithUok({
2961
- ctx,
2962
- pi,
2963
- s,
2964
- deps: loopDeps,
2965
- runKernelLoop: runUokKernelLoop,
2966
- runLegacyLoop: runLegacyAutoLoop,
2967
- });
2968
- await cleanupAfterLoopExit(ctx);
2993
+ try {
2994
+ await runAutoLoopWithUok({
2995
+ ctx,
2996
+ pi,
2997
+ s,
2998
+ deps: loopDeps,
2999
+ runKernelLoop: runUokKernelLoop,
3000
+ runLegacyLoop: runLegacyAutoLoop,
3001
+ });
3002
+ } finally {
3003
+ await cleanupAfterLoopExit(ctx);
3004
+ }
2969
3005
  }
2970
3006
 
2971
3007
  // describeNextUnit is imported from auto-dashboard.ts and re-exported
@@ -3123,19 +3159,56 @@ export async function dispatchHookUnit(
3123
3159
  workspaceRoot: s.basePath,
3124
3160
  });
3125
3161
 
3126
- if (hookModel) {
3127
- const availableModels = ctx.modelRegistry.getAvailable();
3128
- const match = resolveModelId(hookModel, availableModels, ctx.model?.provider);
3129
- if (match) {
3162
+ // Resolve the hook's model honoring the configured `fallbacks[]` chain
3163
+ // (#1229). The manual trigger path bypasses selectAndApplyModel, so resolve
3164
+ // the primary→fallbacks order here and apply the first candidate available in
3165
+ // the registry — a blocked or unconfigured primary then transparently drops
3166
+ // to a fallback instead of silently reverting to the session model.
3167
+ const availableModels = ctx.modelRegistry.getAvailable();
3168
+ const availableModelIds = availableModels.map(
3169
+ (m: { provider: string; id: string }) => `${m.provider}/${m.id}`,
3170
+ );
3171
+ const hookModelConfig = resolveModelWithFallbacksForUnit(
3172
+ hookUnitType,
3173
+ targetBasePath,
3174
+ availableModelIds,
3175
+ );
3176
+ const modelCandidates = hookModelConfig
3177
+ ? [hookModelConfig.primary, ...hookModelConfig.fallbacks]
3178
+ : hookModel
3179
+ ? [hookModel]
3180
+ : [];
3181
+ if (modelCandidates.length > 0) {
3182
+ let applied = false;
3183
+ for (const candidate of modelCandidates) {
3184
+ const match = resolveModelId(candidate, availableModels, ctx.model?.provider);
3185
+ if (!match) continue;
3186
+ // Skip models the runtime has marked blocked or temporarily unavailable
3187
+ // (e.g. a primary that just tripped a provider limit) so the configured
3188
+ // fallbacks[] chain actually engages — parity with auto-mode's
3189
+ // selectAndApplyModel, which consults the same blocked-models store (#1229).
3190
+ if (isModelUnavailable(targetBasePath, match.provider, match.id)) continue;
3130
3191
  try {
3131
- await pi.setModel(match);
3192
+ if (await pi.setModel(match)) {
3193
+ // The manual trigger path bypasses selectAndApplyModel, so apply the
3194
+ // hook's per-field `thinking` (from `post_unit_hooks[].model`'s object
3195
+ // form) here against the just-set model rather than leaving the hook at
3196
+ // the session level (#1269). Absent → session level, unchanged.
3197
+ if (hookModelConfig?.thinking) {
3198
+ applyThinkingLevelForModel(pi, hookModelConfig.thinking, match, ctx);
3199
+ }
3200
+ applied = true;
3201
+ break;
3202
+ }
3132
3203
  } catch (err) {
3133
- /* non-fatal */
3134
- logWarning("dispatch", `hook model set failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
3204
+ /* non-fatal — try the next fallback */
3205
+ logWarning("dispatch", `hook model set failed for ${candidate}: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
3135
3206
  }
3136
- } else {
3207
+ }
3208
+ if (!applied) {
3137
3209
  ctx.ui.notify(
3138
- `Hook model "${hookModel}" not found in available models. Falling back to current session model. ` +
3210
+ `Hook model${modelCandidates.length > 1 ? "s" : ""} "${modelCandidates.join(", ")}" not available. ` +
3211
+ `Falling back to current session model. ` +
3139
3212
  `Ensure the model is defined in models.json and has auth configured.`,
3140
3213
  "warning",
3141
3214
  );
@@ -1725,7 +1725,7 @@ export function registerHooks(
1725
1725
  }
1726
1726
  }
1727
1727
  if (!event.isError) {
1728
- recordToolCallLoopMutation(toolName);
1728
+ recordToolCallLoopMutation(toolName, event.details);
1729
1729
  refreshSourceObservationAfterMutation(toolName, event.input, ctx);
1730
1730
  clearSourceObservationsAfterShell(toolName);
1731
1731
  }
@@ -14,13 +14,16 @@
14
14
  * to stop tooling for the rest of that turn and answer in text.
15
15
  *
16
16
  * The per-tool-name check (#783 Brief C) tracks call counts within a
17
- * turn regardless of args, decaying after successful state-mutating tools.
17
+ * turn regardless of args, decaying after successful state-progressing tools.
18
18
  * This catches improvisation
19
19
  * loops where the model attempts the same missing workflow tool through
20
20
  * varied surfaces (bash → `node -e` → CLI), each with a different
21
21
  * signature, so the identical-args streak never trips, while allowing
22
- * tool-heavy turns that are making file-mutation progress. Whichever guard
23
- * trips first blocks.
22
+ * tool-heavy turns that are making progress. Progress means a successful file
23
+ * mutation (edit/write) or a successful shell/exec call (bg_shell,
24
+ * gsd_uat_exec, …) — so a local model running a legitimate iterative
25
+ * debugging loop (restart server, npm install, curl an endpoint) is not
26
+ * blocked as a false loop (#1206). Whichever guard trips first blocks.
24
27
  *
25
28
  * Thresholds, exempt tools, and enable flags are user-tunable (#1198) via the
26
29
  * `tool_call_loop_guard` key in `.gsd/PREFERENCES.md` and `GSD_TOOL_LOOP_*`
@@ -45,6 +48,52 @@ const MAX_CONSECUTIVE_STRICT = 1;
45
48
 
46
49
  const STATE_MUTATING_TOOL_SET = new Set(["edit", "write", "multi_edit", "notebook_edit"]);
47
50
 
51
+ /**
52
+ * Successful shell/exec calls are state progression too (#1206), not just file
53
+ * mutations. A local model debugging a UAT scenario legitimately restarts
54
+ * servers, runs `npm install`, and curls endpoints via bg_shell / gsd_uat_exec
55
+ * with varied args — productive work that never touches a file, so Guard 2's
56
+ * per-tool cap would otherwise misfire as a false loop. Decaying on these
57
+ * successful calls keeps the guard from aborting a progressing turn.
58
+ *
59
+ * Only successful calls reach {@link recordToolCallLoopMutation}: the
60
+ * tool_result hook skips `isError` results, and the bash tool throws on
61
+ * non-zero exit, so a genuinely stuck improvisation loop of failing commands
62
+ * (#783) never decays and still trips the cap. `async_bash` is excluded
63
+ * because it only registers a background job; command success is reported
64
+ * later via `await_job`. `bg_shell` may return `isError: false` for failed
65
+ * `run`/`start` outcomes, so the hook passes `details` for validation.
66
+ */
67
+ const STATE_PROGRESSING_EXEC_TOOL_SET = new Set([
68
+ "bash",
69
+ "bg_shell",
70
+ "shell",
71
+ "powershell",
72
+ "gsd_exec",
73
+ "gsd_uat_exec",
74
+ ]);
75
+
76
+ function isSuccessfulBgShellLoopProgress(details: unknown): boolean {
77
+ if (!details || typeof details !== "object") return false;
78
+ const record = details as Record<string, unknown>;
79
+ switch (record.action) {
80
+ case "run":
81
+ return record.exitCode === 0 && record.timedOut !== true;
82
+ case "start":
83
+ case "restart": {
84
+ const process = record.process;
85
+ if (!process || typeof process !== "object") return false;
86
+ return (process as Record<string, unknown>).alive === true;
87
+ }
88
+ case "wait_for_ready":
89
+ return record.ready === true;
90
+ case "send_and_wait":
91
+ return record.matched === true;
92
+ default:
93
+ return true;
94
+ }
95
+ }
96
+
48
97
  /**
49
98
  * User-tunable configuration shape for the loop guard (#1198).
50
99
  *
@@ -295,10 +344,21 @@ export function checkToolCallLoop(
295
344
  return { block: false, count: consecutiveCount };
296
345
  }
297
346
 
298
- /** Record successful mutation progress so Guard 2 can decay on the next count. */
299
- export function recordToolCallLoopMutation(toolName: string): void {
347
+ /**
348
+ * Record successful state progress so Guard 2 can decay on the next count.
349
+ * File mutations (edit/write/…) and successful shell/exec calls (bash,
350
+ * bg_shell, gsd_uat_exec, …) both count as progress (#1092, #1206). The caller
351
+ * only invokes this for non-error results, so failing improvisation loops
352
+ * (#783) never decay and still trip the per-tool cap.
353
+ */
354
+ export function recordToolCallLoopMutation(toolName: string, details?: unknown): void {
300
355
  if (!enabled) return;
301
- if (!STATE_MUTATING_TOOL_SET.has(toolName)) return;
356
+ if (STATE_MUTATING_TOOL_SET.has(toolName)) {
357
+ mutationEpoch++;
358
+ return;
359
+ }
360
+ if (!STATE_PROGRESSING_EXEC_TOOL_SET.has(toolName)) return;
361
+ if (toolName === "bg_shell" && details !== undefined && !isSuccessfulBgShellLoopProgress(details)) return;
302
362
  mutationEpoch++;
303
363
  }
304
364
 
@@ -675,10 +675,16 @@ export function ensureCodebaseMapFresh(
675
675
 
676
676
  const metadata = parseCodebaseMapMetadata(existing);
677
677
  const existingDescriptions = parseCodebaseMap(existing);
678
+ // Compare against the truncated count: when the repository has more tracked
679
+ // files than maxFiles, the map only lists the first maxFiles entries, so a
680
+ // stored fileCount above the cap (e.g. from an older map that recorded the
681
+ // full count) still matches the current truncated listing. Comparing raw
682
+ // values would flag "file-count-changed" on every turn, rewriting the
683
+ // timestamp and invalidating the provider KV cache prefix.
678
684
  const staleReason =
679
685
  !metadata ? "missing-metadata"
680
686
  : metadata.fingerprint !== fingerprint ? "files-changed"
681
- : metadata.fileCount !== listed.files.length ? "file-count-changed"
687
+ : Math.min(metadata.fileCount, resolved.maxFiles) !== listed.files.length ? "file-count-changed"
682
688
  : metadata.truncated !== listed.truncated ? "truncation-changed"
683
689
  : undefined;
684
690
 
@@ -148,6 +148,53 @@ export function writeCompatMarker(basePath: string, marker: CompatMarker): void
148
148
  renameSync(tmp, path);
149
149
  }
150
150
 
151
+ /**
152
+ * Remove projection entries whose backing file no longer exists on disk.
153
+ *
154
+ * gsd-pi never deletes marker entries on its own: both drift detectors skip
155
+ * files missing from disk (`if (!existsSync(abs)) continue;`) and the write-time
156
+ * flush only ever adds or refreshes entries. So when a phase directory is
157
+ * renamed or removed (e.g. `phases/29-new-milestone-m029/` →
158
+ * `phases/29-frontend-code-debt-cleanup/`), the old projection paths linger in
159
+ * `.compat.json` forever as phantom entries pointing at directories that no
160
+ * longer exist (#1257). They never reconcile, and `.compat.json` keeps drifting
161
+ * from disk reality so `git status` stops being a reliable proxy for "did the
162
+ * engine touch my plans?".
163
+ *
164
+ * This prunes those orphaned entries across the `.gsd/` projections and the
165
+ * `.planning/` projections/passthrough maps. It is safe: a missing-file entry is
166
+ * inert (every detector already ignores it), and if the file is later
167
+ * re-projected the write-time flush / unseeded-file detection re-seeds an
168
+ * accurate baseline.
169
+ *
170
+ * Returns the number of entries removed; when nothing is orphaned the marker is
171
+ * left untouched (no needless write, no `lastProjectedAt` churn).
172
+ */
173
+ export function pruneOrphanedProjectionEntries(basePath: string): number {
174
+ if (!existsSync(compatMarkerPath(basePath))) return 0;
175
+
176
+ const marker = readCompatMarker(basePath);
177
+ let removed = 0;
178
+
179
+ const pruneMap = (map: Record<string, ProjectionEntry>, root: string): void => {
180
+ for (const relPath of Object.keys(map)) {
181
+ if (!existsSync(join(basePath, root, relPath))) {
182
+ delete map[relPath];
183
+ removed++;
184
+ }
185
+ }
186
+ };
187
+
188
+ pruneMap(marker.projections, ".gsd");
189
+ if (marker.planning) {
190
+ pruneMap(marker.planning.projections, ".planning");
191
+ pruneMap(marker.planning.passthrough, ".planning");
192
+ }
193
+
194
+ if (removed > 0) writeCompatMarker(basePath, marker);
195
+ return removed;
196
+ }
197
+
151
198
  function quarantine(basePath: string, raw: string): void {
152
199
  const path = compatMarkerPath(basePath);
153
200
  const badPath = `${path}.bad-${Date.now()}`;
@@ -150,7 +150,13 @@ function collectConfigSections(options?: CollectConfigOptions): ConfigSection[]
150
150
  if (prefs?.auto_supervisor) {
151
151
  const sup = resolveAutoSupervisorConfig();
152
152
  const supRows: ConfigSection["rows"] = [];
153
- if (sup.model) supRows.push({ label: "Model", value: sup.model });
153
+ if (sup.model) {
154
+ let modelVal = sup.model;
155
+ if (sup.modelFallbacks && sup.modelFallbacks.length > 0) {
156
+ modelVal += ` \u2192 ${sup.modelFallbacks.join(" \u2192 ")}`;
157
+ }
158
+ supRows.push({ label: "Model", value: modelVal });
159
+ }
154
160
  supRows.push({ label: "Soft timeout", value: `${sup.soft_timeout_minutes}m` });
155
161
  supRows.push({ label: "Idle timeout", value: `${sup.idle_timeout_minutes}m` });
156
162
  supRows.push({ label: "Stalled tool timeout", value: `${sup.stalled_tool_timeout_minutes}m` });
@@ -860,3 +860,39 @@ export function getDb(): DbAdapter {
860
860
  export function getDbOrNull(): DbAdapter | null {
861
861
  return currentDb;
862
862
  }
863
+
864
+ export interface DbWritableProbeResult {
865
+ ok: boolean;
866
+ detail?: string;
867
+ }
868
+
869
+ /**
870
+ * Confirm the open handle can actually write, not just that it opened.
871
+ *
872
+ * A schema-current database performs zero writes during open, so a handle that
873
+ * opened successfully but is not writable (read-only file, WAL/SHM permission
874
+ * mismatch, or a stale/moved handle → SQLITE_READONLY_DBMOVED) otherwise passes
875
+ * the open-only availability check and only fails much later at the first real
876
+ * write. The probe forces a genuine page write by re-writing the current
877
+ * `PRAGMA user_version` value back inside an IMMEDIATE transaction: a bare
878
+ * `BEGIN IMMEDIATE; ROLLBACK` is not sufficient because a moved handle does not
879
+ * fail until a page is actually dirtied. The value is unchanged, so the probe
880
+ * is idempotent, and the transaction is rolled back so nothing persists.
881
+ */
882
+ export function probeDbWritable(): DbWritableProbeResult {
883
+ const db = currentDb;
884
+ if (!db) return { ok: false, detail: "No database is open." };
885
+ try {
886
+ const row = db.prepare("PRAGMA user_version").get() as { user_version?: number } | undefined;
887
+ const current = typeof row?.user_version === "number" ? row.user_version : 0;
888
+ db.exec("BEGIN IMMEDIATE");
889
+ try {
890
+ db.exec(`PRAGMA user_version = ${current}`);
891
+ } finally {
892
+ db.exec("ROLLBACK");
893
+ }
894
+ return { ok: true };
895
+ } catch (err) {
896
+ return { ok: false, detail: (err as Error).message };
897
+ }
898
+ }
@@ -38,6 +38,17 @@ export type ReopenSliceOutcome =
38
38
  * completion timestamps cleared, in one commit. Folds the hand-rolled
39
39
  * transaction-plus-cascade previously in tools/reopen-slice.ts. Guards run
40
40
  * inside the transaction (TOCTOU-safe).
41
+ *
42
+ * A closed slice is always reopenable. An open slice is only reopenable when it
43
+ * carries a completed-task state desync (#1205): a UAT→planning fallback can
44
+ * leave the slice "pending" while *all* its tasks stay "complete", which
45
+ * deadlocks every recovery path (plan-slice/replan-slice/complete-task all
46
+ * reject the still-closed tasks, and slice_reopen historically rejected the
47
+ * still-open slice). Resetting those tasks to pending gives the planner the
48
+ * clean slate it needs. An open slice that still has any unfinished
49
+ * (pending/in-progress) task is a normal in-flight slice, not the desync, and
50
+ * stays rejected as "slice-not-complete" so a full reset can't wipe its
51
+ * legitimate progress.
41
52
  */
42
53
  export function reopenSliceCascade(milestoneId: string, sliceId: string): ReopenSliceOutcome {
43
54
  requireDb();
@@ -48,9 +59,20 @@ export function reopenSliceCascade(milestoneId: string, sliceId: string): Reopen
48
59
  if (isClosedStatus(milestone.status)) { outcome = { ok: false, reason: "milestone-closed", status: milestone.status }; return; }
49
60
  const slice = getSlice(milestoneId, sliceId);
50
61
  if (!slice) { outcome = { ok: false, reason: "slice-not-found" }; return; }
51
- if (!isClosedStatus(slice.status)) { outcome = { ok: false, reason: "slice-not-complete", status: slice.status }; return; }
52
62
 
53
63
  const tasks = getSliceTasks(milestoneId, sliceId);
64
+ // A closed slice is always reopenable. An open slice is only the #1205
65
+ // desync when EVERY task is already closed (a UAT→planning fallback left the
66
+ // slice open with all tasks still "complete"). Requiring *all* tasks closed
67
+ // (not merely one) means a normal in-flight slice — which keeps at least one
68
+ // pending/in-progress task — is rejected, so a full reset can't wipe its
69
+ // legitimate progress.
70
+ const allTasksClosed = tasks.length > 0 && tasks.every((t) => isClosedStatus(t.status));
71
+ if (!isClosedStatus(slice.status) && !allTasksClosed) {
72
+ outcome = { ok: false, reason: "slice-not-complete", status: slice.status };
73
+ return;
74
+ }
75
+
54
76
  getDbOrNull()!.prepare(
55
77
  `UPDATE slices SET status = 'in_progress', completed_at = NULL WHERE milestone_id = :mid AND id = :sid`,
56
78
  ).run({ ":mid": milestoneId, ":sid": sliceId });
@@ -25,6 +25,32 @@ export function normalizeDbRows(rows: unknown[]): Record<string, unknown>[] {
25
25
  return rows.map((row) => normalizeDbRow(row)!);
26
26
  }
27
27
 
28
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
29
+ if (value === null || typeof value !== "object") return false;
30
+ const proto = Object.getPrototypeOf(value);
31
+ return proto === Object.prototype || proto === null;
32
+ }
33
+
34
+ // node:sqlite accepts binding-object keys with or without the ':'/'@'/'$'
35
+ // prefix, but better-sqlite3 accepts only bare keys — strip the prefix so
36
+ // call sites written against node:sqlite also work on the fallback provider.
37
+ function normalizeBindParams(params: unknown[]): unknown[] {
38
+ return params.map((param) => {
39
+ if (!isPlainObject(param)) return param;
40
+ let changed = false;
41
+ const normalized: Record<string, unknown> = {};
42
+ for (const [key, value] of Object.entries(param)) {
43
+ if (key.length > 1 && (key[0] === ":" || key[0] === "@" || key[0] === "$")) {
44
+ normalized[key.slice(1)] = value;
45
+ changed = true;
46
+ } else {
47
+ normalized[key] = value;
48
+ }
49
+ }
50
+ return changed ? normalized : param;
51
+ });
52
+ }
53
+
28
54
  export function createDbAdapter(rawDb: unknown): DbAdapter {
29
55
  const db = rawDb as {
30
56
  exec(sql: string): void;
@@ -45,13 +71,13 @@ export function createDbAdapter(rawDb: unknown): DbAdapter {
45
71
  }): DbStatement {
46
72
  return {
47
73
  run(...params: unknown[]): unknown {
48
- return raw.run(...params);
74
+ return raw.run(...normalizeBindParams(params));
49
75
  },
50
76
  get(...params: unknown[]): Record<string, unknown> | undefined {
51
- return normalizeDbRow(raw.get(...params));
77
+ return normalizeDbRow(raw.get(...normalizeBindParams(params)));
52
78
  },
53
79
  all(...params: unknown[]): Record<string, unknown>[] {
54
- return normalizeDbRows(raw.all(...params));
80
+ return normalizeDbRows(raw.all(...normalizeBindParams(params)));
55
81
  },
56
82
  };
57
83
  }
@@ -138,7 +138,7 @@ Diagnostics record the file path, scope (global/project), severity (error/warnin
138
138
  - `off` — skill discovery is disabled entirely.
139
139
 
140
140
  - `auto_supervisor`: configures the auto-mode supervisor that monitors agent progress and enforces timeouts. Keys:
141
- - `model`: model ID to use for the supervisor process (defaults to the currently active model).
141
+ - `model`: model to use for the supervisor process (defaults to the currently active model). Accepts a bare model ID **or** the same `{ model, provider?, fallbacks? }` object form as `models.<phase>`, so it honors `fallbacks[]` on a transient provider trip.
142
142
  - `soft_timeout_minutes`: minutes before the supervisor issues a soft warning (default: 20).
143
143
  - `idle_timeout_minutes`: minutes of inactivity before the supervisor intervenes (default: 10).
144
144
  - `hard_timeout_minutes`: minutes before the supervisor forces termination (default: 30).
@@ -228,7 +228,7 @@ In `"parent"` mode, slice/task `targetRepositories` default to the declared chil
228
228
  - `enabled`: boolean — set `false` to force sequential task execution. Default: `true`.
229
229
  - `max_parallel`: number — maximum tasks to dispatch in one batch, range `1`-`8`. Default: `2`.
230
230
  - `isolation_mode`: `"same-tree"` — currently the only supported value.
231
- - `subagent_model`: string — optional model override for reactive task subagents. Falls back to the `models.subagent` routing when omitted.
231
+ - `subagent_model`: optional model override for reactive task subagents. Accepts a bare model ID **or** the `{ model, provider?, fallbacks? }` object form (parity with `models.<phase>`). Falls back to the `models.subagent` routing when omitted.
232
232
 
233
233
  - `remote_questions`: route interactive questions to Slack/Discord for headless auto-mode. Keys:
234
234
  - `channel`: `"slack"` or `"discord"` — channel type.
@@ -326,7 +326,7 @@ In `"parent"` mode, slice/task `targetRepositories` default to the declared chil
326
326
  - `after`: string[] — unit types that trigger this hook (e.g., `["execute-task"]`).
327
327
  - `prompt`: string — prompt sent to the LLM. Supports `{milestoneId}`, `{sliceId}`, `{taskId}` substitutions.
328
328
  - `max_cycles`: number — max times this hook fires per trigger (default: 1, max: 10).
329
- - `model`: string — optional model override.
329
+ - `model`: optional model override. Accepts a bare model ID **or** the same `{ model, provider?, fallbacks? }` object form as `models.<phase>`. With `fallbacks[]`, a transient trip of the primary provider falls back to the next entry instead of hard-failing the hook — which, for a `blocking` hook with `on_block: { action: pause }`, would otherwise pause the whole run.
330
330
  - `artifact`: string — expected output file name (relative to task/slice dir). Hook is skipped if file already exists (idempotent).
331
331
  - `criticality`: `"advisory"` or `"blocking"` — advisory preserves current best-effort behavior; blocking requires clean hook completion plus a valid outcome verdict before auto-mode advances. Default: `"advisory"`.
332
332
  - `retry_on`: string — if this file is produced instead of the artifact, re-run the trigger unit then re-run hooks.
@@ -108,15 +108,35 @@ function rollbackPartialMigration(
108
108
  `rollback: could not remove partial ${LAYOUT_SEGMENTS.level1}/: ${(removeErr as Error).message}`,
109
109
  );
110
110
  }
111
+ // Only the standalone .gsd-backups/migrate-<ts>/ copy is disposable once the
112
+ // restore succeeds. When resuming an interrupted migration, backupDir IS the
113
+ // preserved migrating tree (the sole surviving data source) and must never be
114
+ // removed here. Deleting the backup after a successful rollback keeps the gate
115
+ // from leaking one .gsd-backups/migrate-<ts>/ per session_start.
116
+ const isDisposableBackup = Boolean(backupDir) && backupDir !== migratingPath;
117
+ const cleanupBackup = (): void => {
118
+ if (!isDisposableBackup || !existsSync(backupDir)) return;
119
+ try {
120
+ removePathWithRetries(backupDir);
121
+ } catch (cleanupErr) {
122
+ logWarning(
123
+ "migration",
124
+ `rollback: could not clean backup ${backupDir}: ${(cleanupErr as Error).message}`,
125
+ );
126
+ }
127
+ };
128
+
111
129
  // Restore milestones/ — prefer renaming the preserved migrating copy back.
112
130
  const milestonesPath = join(basePath, ".gsd", "milestones");
113
131
  try {
114
132
  if (migratingPath && existsSync(migratingPath) && !existsSync(milestonesPath)) {
115
133
  movePathWithCopyDeleteFallback(migratingPath, milestonesPath);
134
+ cleanupBackup();
116
135
  return;
117
136
  }
118
137
  if (existsSync(backupDir)) {
119
138
  cpSync(backupDir, milestonesPath, { recursive: true });
139
+ cleanupBackup();
120
140
  }
121
141
  if (migratingPath && existsSync(migratingPath)) {
122
142
  removePathWithRetries(migratingPath);
@@ -454,6 +454,14 @@ export function insertTask(t: {
454
454
  fullSummaryMd?: string;
455
455
  sequence?: number;
456
456
  planning?: Partial<TaskPlanningRecord>;
457
+ // #1222 (metadata half): when re-importing from markdown, the caller only
458
+ // knows the plan/status — not the execution prose that gsd_task_complete
459
+ // wrote to the DB. Setting this preserves the existing row's execution
460
+ // columns (and completed_at) whenever the incoming value is empty/default,
461
+ // so a re-import cannot silently blank a completed task's summary metadata
462
+ // or refresh its completion timestamp. Off by default: callers that own the
463
+ // execution metadata (gsd_task_complete, planning) keep overwrite semantics.
464
+ preserveCompletionMetadata?: boolean;
457
465
  }): void {
458
466
  if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
459
467
  getDbOrNull()!.prepare(
@@ -473,17 +481,17 @@ export function insertTask(t: {
473
481
  ON CONFLICT(milestone_id, slice_id, id) DO UPDATE SET
474
482
  title = CASE WHEN NULLIF(:title, '') IS NOT NULL THEN :title ELSE tasks.title END,
475
483
  status = :status,
476
- one_liner = :one_liner,
477
- narrative = :narrative,
478
- verification_result = :verification_result,
479
- duration = :duration,
480
- completed_at = :completed_at,
481
- blocker_discovered = :blocker_discovered,
482
- deviations = :deviations,
483
- known_issues = :known_issues,
484
- key_files = :key_files,
485
- key_decisions = :key_decisions,
486
- full_summary_md = :full_summary_md,
484
+ one_liner = CASE WHEN :preserve_completion = 1 AND NULLIF(:one_liner, '') IS NULL THEN tasks.one_liner ELSE :one_liner END,
485
+ narrative = CASE WHEN :preserve_completion = 1 AND NULLIF(:narrative, '') IS NULL THEN tasks.narrative ELSE :narrative END,
486
+ verification_result = CASE WHEN :preserve_completion = 1 AND NULLIF(:verification_result, '') IS NULL THEN tasks.verification_result ELSE :verification_result END,
487
+ duration = CASE WHEN :preserve_completion = 1 AND NULLIF(:duration, '') IS NULL THEN tasks.duration ELSE :duration END,
488
+ completed_at = CASE WHEN :preserve_completion = 1 AND tasks.completed_at IS NOT NULL THEN tasks.completed_at ELSE :completed_at END,
489
+ blocker_discovered = CASE WHEN :preserve_completion = 1 AND :blocker_discovered = 0 THEN tasks.blocker_discovered ELSE :blocker_discovered END,
490
+ deviations = CASE WHEN :preserve_completion = 1 AND NULLIF(:deviations, '') IS NULL THEN tasks.deviations ELSE :deviations END,
491
+ known_issues = CASE WHEN :preserve_completion = 1 AND NULLIF(:known_issues, '') IS NULL THEN tasks.known_issues ELSE :known_issues END,
492
+ key_files = CASE WHEN :preserve_completion = 1 AND NULLIF(:key_files, '[]') IS NULL THEN tasks.key_files ELSE :key_files END,
493
+ key_decisions = CASE WHEN :preserve_completion = 1 AND NULLIF(:key_decisions, '[]') IS NULL THEN tasks.key_decisions ELSE :key_decisions END,
494
+ full_summary_md = CASE WHEN :preserve_completion = 1 AND NULLIF(:full_summary_md, '') IS NULL THEN tasks.full_summary_md ELSE :full_summary_md END,
487
495
  description = CASE WHEN NULLIF(:description, '') IS NOT NULL THEN :description ELSE tasks.description END,
488
496
  estimate = CASE WHEN NULLIF(:estimate, '') IS NOT NULL THEN :estimate ELSE tasks.estimate END,
489
497
  files = CASE WHEN NULLIF(:files, '[]') IS NOT NULL THEN :files ELSE tasks.files END,
@@ -523,6 +531,7 @@ export function insertTask(t: {
523
531
  ":observability_impact": t.planning?.observabilityImpact ?? "",
524
532
  ":full_plan_md": t.planning?.fullPlanMd ?? "",
525
533
  ":sequence": t.sequence ?? 0,
534
+ ":preserve_completion": t.preserveCompletionMetadata && (t.status === "complete" || t.status === "done") ? 1 : 0,
526
535
  ":target_repositories": JSON.stringify(t.planning?.targetRepositories ?? []),
527
536
  ":raw_target_repositories":
528
537
  t.planning && "targetRepositories" in t.planning