@opengsd/gsd-pi 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/README.md +11 -11
  2. package/dist/onboarding.js +11 -0
  3. package/dist/resources/.managed-resources-content-hash +1 -1
  4. package/dist/resources/GSD-WORKFLOW.md +20 -25
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +118 -53
  6. package/dist/resources/extensions/cursor-cli/index.js +16 -0
  7. package/dist/resources/extensions/cursor-cli/models.js +63 -0
  8. package/dist/resources/extensions/cursor-cli/package.json +11 -0
  9. package/dist/resources/extensions/cursor-cli/readiness.js +128 -0
  10. package/dist/resources/extensions/cursor-cli/stream-adapter.js +353 -0
  11. package/dist/resources/extensions/gsd/activity-log.js +4 -1
  12. package/dist/resources/extensions/gsd/artifact-verification.js +25 -9
  13. package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
  14. package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
  15. package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
  16. package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
  17. package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
  18. package/dist/resources/extensions/gsd/auto-dispatch.js +15 -5
  19. package/dist/resources/extensions/gsd/auto-model-selection.js +36 -18
  20. package/dist/resources/extensions/gsd/auto-prompts.js +76 -2
  21. package/dist/resources/extensions/gsd/auto-start.js +4 -3
  22. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +4 -0
  23. package/dist/resources/extensions/gsd/auto-worktree.js +38 -11
  24. package/dist/resources/extensions/gsd/auto.js +23 -3
  25. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -0
  26. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +114 -5
  27. package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
  28. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +10 -2
  29. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +14 -1
  30. package/dist/resources/extensions/gsd/codebase-generator.js +157 -19
  31. package/dist/resources/extensions/gsd/commands/handlers/core.js +40 -4
  32. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +153 -1
  33. package/dist/resources/extensions/gsd/commands-worktree.js +11 -2
  34. package/dist/resources/extensions/gsd/db/queries.js +39 -0
  35. package/dist/resources/extensions/gsd/discussion-handoff.js +8 -3
  36. package/dist/resources/extensions/gsd/docs/preferences-reference.md +8 -2
  37. package/dist/resources/extensions/gsd/doctor-engine-checks.js +206 -14
  38. package/dist/resources/extensions/gsd/doctor-providers.js +38 -10
  39. package/dist/resources/extensions/gsd/doctor.js +93 -3
  40. package/dist/resources/extensions/gsd/exec-sandbox.js +6 -2
  41. package/dist/resources/extensions/gsd/flat-phase-migration.js +74 -23
  42. package/dist/resources/extensions/gsd/git-service.js +14 -18
  43. package/dist/resources/extensions/gsd/gsd-db.js +52 -0
  44. package/dist/resources/extensions/gsd/guidance.js +1 -0
  45. package/dist/resources/extensions/gsd/guided-flow.js +40 -2
  46. package/dist/resources/extensions/gsd/key-manager.js +1 -0
  47. package/dist/resources/extensions/gsd/markdown-renderer.js +25 -8
  48. package/dist/resources/extensions/gsd/migrate-external.js +24 -7
  49. package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
  50. package/dist/resources/extensions/gsd/preferences-diagnostics.js +24 -1
  51. package/dist/resources/extensions/gsd/preferences-models.js +6 -4
  52. package/dist/resources/extensions/gsd/preferences-validation.js +25 -2
  53. package/dist/resources/extensions/gsd/preferences.js +29 -2
  54. package/dist/resources/extensions/gsd/prompts/plan-slice.md +4 -2
  55. package/dist/resources/extensions/gsd/redact-secrets.js +37 -0
  56. package/dist/resources/extensions/gsd/repo-identity.js +16 -0
  57. package/dist/resources/extensions/gsd/repository-registry.js +6 -1
  58. package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
  59. package/dist/resources/extensions/gsd/safe-fs.js +23 -2
  60. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +7 -9
  61. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +1 -0
  62. package/dist/resources/extensions/gsd/tools/plan-task.js +76 -8
  63. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -0
  64. package/dist/resources/extensions/gsd/unit-context-manifest.js +5 -4
  65. package/dist/resources/extensions/gsd/unit-registry.js +27 -2
  66. package/dist/resources/extensions/gsd/unit-runtime.js +21 -0
  67. package/dist/resources/extensions/gsd/workspace.js +26 -8
  68. package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
  69. package/dist/resources/extensions/gsd/worktree-manager.js +97 -4
  70. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  71. package/dist/web/standalone/.next/BUILD_ID +1 -1
  72. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  73. package/dist/web/standalone/.next/build-manifest.json +2 -2
  74. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/index.html +1 -1
  92. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  99. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  100. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  102. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  103. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  104. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  105. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  106. package/dist/wizard.d.ts +8 -0
  107. package/dist/wizard.js +8 -1
  108. package/integrations/hermes/CONTEXT.md +47 -0
  109. package/integrations/hermes/README.md +21 -0
  110. package/integrations/hermes/__init__.py +31 -0
  111. package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
  112. package/integrations/hermes/docs/setup.md +4 -0
  113. package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
  114. package/integrations/hermes/open_gsd_hermes/config.py +2 -0
  115. package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
  116. package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
  117. package/integrations/hermes/plugin.yaml +4 -0
  118. package/integrations/hermes/tests/test_config.py +6 -0
  119. package/integrations/hermes/tests/test_directory_loader.py +37 -0
  120. package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
  121. package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
  122. package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
  123. package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
  124. package/integrations/hermes/tests/test_milestone_client.py +447 -0
  125. package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
  126. package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
  127. package/package.json +8 -8
  128. package/packages/cloud-mcp-gateway/dist/auth-store.d.ts +1 -0
  129. package/packages/cloud-mcp-gateway/dist/auth-store.d.ts.map +1 -1
  130. package/packages/cloud-mcp-gateway/dist/auth-store.js +21 -1
  131. package/packages/cloud-mcp-gateway/dist/auth-store.js.map +1 -1
  132. package/packages/cloud-mcp-gateway/dist/auth-store.test.js +34 -0
  133. package/packages/cloud-mcp-gateway/dist/auth-store.test.js.map +1 -1
  134. package/packages/cloud-mcp-gateway/package.json +2 -2
  135. package/packages/contracts/package.json +1 -1
  136. package/packages/daemon/dist/cloud-runtime.d.ts +2 -0
  137. package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -1
  138. package/packages/daemon/dist/cloud-runtime.js +25 -1
  139. package/packages/daemon/dist/cloud-runtime.js.map +1 -1
  140. package/packages/daemon/dist/cloud-runtime.test.js +51 -0
  141. package/packages/daemon/dist/cloud-runtime.test.js.map +1 -1
  142. package/packages/daemon/dist/message-batcher.d.ts +7 -0
  143. package/packages/daemon/dist/message-batcher.d.ts.map +1 -1
  144. package/packages/daemon/dist/message-batcher.js +13 -0
  145. package/packages/daemon/dist/message-batcher.js.map +1 -1
  146. package/packages/daemon/dist/message-batcher.test.js +48 -0
  147. package/packages/daemon/dist/message-batcher.test.js.map +1 -1
  148. package/packages/daemon/dist/session-manager.js +1 -1
  149. package/packages/daemon/dist/session-manager.js.map +1 -1
  150. package/packages/daemon/package.json +5 -5
  151. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts +13 -1
  152. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts.map +1 -1
  153. package/packages/gsd-agent-core/dist/compaction/compaction.js +15 -1
  154. package/packages/gsd-agent-core/dist/compaction/compaction.js.map +1 -1
  155. package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
  156. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
  157. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
  158. package/packages/gsd-agent-core/package.json +5 -5
  159. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.d.ts.map +1 -1
  160. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.js +1 -0
  161. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.js.map +1 -1
  162. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +1 -1
  163. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
  164. package/packages/gsd-agent-modes/package.json +7 -7
  165. package/packages/mcp-server/dist/server.d.ts +18 -5
  166. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  167. package/packages/mcp-server/dist/server.js +46 -5
  168. package/packages/mcp-server/dist/server.js.map +1 -1
  169. package/packages/mcp-server/dist/session-manager.js +1 -1
  170. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  171. package/packages/mcp-server/package.json +4 -4
  172. package/packages/native/package.json +1 -1
  173. package/packages/pi-agent-core/package.json +1 -1
  174. package/packages/pi-ai/dist/env-api-keys.d.ts.map +1 -1
  175. package/packages/pi-ai/dist/env-api-keys.js +3 -0
  176. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  177. package/packages/pi-ai/dist/types.d.ts +2 -2
  178. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  179. package/packages/pi-ai/dist/types.js.map +1 -1
  180. package/packages/pi-ai/package.json +1 -1
  181. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  182. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  183. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  184. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
  185. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
  186. package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
  187. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  188. package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
  189. package/packages/pi-coding-agent/dist/core/session-manager.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/dist/rpc-client.js +1 -1
  193. package/packages/rpc-client/dist/rpc-client.js.map +1 -1
  194. package/packages/rpc-client/package.json +2 -2
  195. package/pkg/package.json +1 -1
  196. package/scripts/link-workspace-packages.cjs +14 -4
  197. package/src/resources/GSD-WORKFLOW.md +20 -25
  198. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +130 -51
  199. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +371 -39
  200. package/src/resources/extensions/cursor-cli/index.ts +18 -0
  201. package/src/resources/extensions/cursor-cli/models.ts +65 -0
  202. package/src/resources/extensions/cursor-cli/package.json +11 -0
  203. package/src/resources/extensions/cursor-cli/readiness.ts +143 -0
  204. package/src/resources/extensions/cursor-cli/stream-adapter.ts +440 -0
  205. package/src/resources/extensions/cursor-cli/tests/index.test.ts +54 -0
  206. package/src/resources/extensions/cursor-cli/tests/readiness.test.ts +29 -0
  207. package/src/resources/extensions/cursor-cli/tests/stream-adapter.test.ts +166 -0
  208. package/src/resources/extensions/gsd/activity-log.ts +4 -1
  209. package/src/resources/extensions/gsd/artifact-verification.ts +25 -9
  210. package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
  211. package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
  212. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  213. package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
  214. package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
  215. package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
  216. package/src/resources/extensions/gsd/auto-dispatch.ts +17 -5
  217. package/src/resources/extensions/gsd/auto-model-selection.ts +49 -18
  218. package/src/resources/extensions/gsd/auto-prompts.ts +73 -2
  219. package/src/resources/extensions/gsd/auto-start.ts +4 -3
  220. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +4 -0
  221. package/src/resources/extensions/gsd/auto-worktree.ts +50 -11
  222. package/src/resources/extensions/gsd/auto.ts +22 -3
  223. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -0
  224. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +128 -5
  225. package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
  226. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +12 -2
  227. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +23 -1
  228. package/src/resources/extensions/gsd/codebase-generator.ts +179 -18
  229. package/src/resources/extensions/gsd/commands/handlers/core.ts +39 -3
  230. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +135 -1
  231. package/src/resources/extensions/gsd/commands-worktree.ts +13 -2
  232. package/src/resources/extensions/gsd/db/queries.ts +40 -0
  233. package/src/resources/extensions/gsd/discussion-handoff.ts +10 -2
  234. package/src/resources/extensions/gsd/docs/preferences-reference.md +8 -2
  235. package/src/resources/extensions/gsd/doctor-engine-checks.ts +269 -18
  236. package/src/resources/extensions/gsd/doctor-providers.ts +39 -10
  237. package/src/resources/extensions/gsd/doctor-types.ts +5 -1
  238. package/src/resources/extensions/gsd/doctor.ts +95 -3
  239. package/src/resources/extensions/gsd/exec-sandbox.ts +6 -2
  240. package/src/resources/extensions/gsd/flat-phase-migration.ts +98 -22
  241. package/src/resources/extensions/gsd/git-service.ts +14 -17
  242. package/src/resources/extensions/gsd/gsd-db.ts +72 -0
  243. package/src/resources/extensions/gsd/guidance.ts +2 -0
  244. package/src/resources/extensions/gsd/guided-flow.ts +45 -2
  245. package/src/resources/extensions/gsd/key-manager.ts +1 -0
  246. package/src/resources/extensions/gsd/markdown-renderer.ts +25 -7
  247. package/src/resources/extensions/gsd/migrate-external.ts +23 -6
  248. package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
  249. package/src/resources/extensions/gsd/model-router.ts +4 -5
  250. package/src/resources/extensions/gsd/preferences-diagnostics.ts +24 -0
  251. package/src/resources/extensions/gsd/preferences-models.ts +6 -3
  252. package/src/resources/extensions/gsd/preferences-validation.ts +28 -2
  253. package/src/resources/extensions/gsd/preferences.ts +38 -4
  254. package/src/resources/extensions/gsd/prompts/plan-slice.md +4 -2
  255. package/src/resources/extensions/gsd/redact-secrets.ts +40 -0
  256. package/src/resources/extensions/gsd/repo-identity.ts +17 -0
  257. package/src/resources/extensions/gsd/repository-registry.ts +7 -1
  258. package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
  259. package/src/resources/extensions/gsd/safe-fs.ts +22 -2
  260. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +7 -9
  261. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +1 -0
  262. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
  263. package/src/resources/extensions/gsd/tests/auto-migrating-recovery.test.ts +18 -0
  264. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +178 -0
  265. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
  266. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  267. package/src/resources/extensions/gsd/tests/auto-remote-session-lock-cleanup.test.ts +41 -0
  268. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +41 -0
  269. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +4 -4
  270. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +3 -0
  271. package/src/resources/extensions/gsd/tests/auto-timeout-recovery-harness-abort.test.ts +55 -0
  272. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +8 -0
  273. package/src/resources/extensions/gsd/tests/batch-task-query.test.ts +85 -0
  274. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +29 -0
  275. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +365 -0
  276. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +62 -1
  277. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +138 -1
  278. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +397 -5
  279. package/src/resources/extensions/gsd/tests/doctor-workspace.test.ts +207 -0
  280. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +38 -1
  281. package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +87 -19
  282. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +89 -0
  283. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +9 -0
  284. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
  285. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +122 -0
  286. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
  287. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
  288. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
  289. package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
  290. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +2 -2
  291. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
  292. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +17 -0
  293. package/src/resources/extensions/gsd/tests/plan-task.test.ts +264 -1
  294. package/src/resources/extensions/gsd/tests/preferences.test.ts +150 -0
  295. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +142 -0
  296. package/src/resources/extensions/gsd/tests/recovery-verify-logs.test.ts +4 -4
  297. package/src/resources/extensions/gsd/tests/redact-secrets.test.ts +56 -0
  298. package/src/resources/extensions/gsd/tests/register-hooks-flat-phase-migration.test.ts +73 -0
  299. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +275 -0
  300. package/src/resources/extensions/gsd/tests/repository-registry.test.ts +41 -2
  301. package/src/resources/extensions/gsd/tests/require-slice-discussion-dispatch.test.ts +55 -0
  302. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
  303. package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
  304. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -0
  305. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
  306. package/src/resources/extensions/gsd/tests/status-per-repo.test.ts +94 -0
  307. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +35 -0
  308. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +63 -0
  309. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +66 -18
  310. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
  311. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  312. package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +52 -0
  313. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +53 -1
  314. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +208 -1
  315. package/src/resources/extensions/gsd/tests/workspace.test.ts +18 -4
  316. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
  317. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
  318. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +31 -1
  319. package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +32 -0
  320. package/src/resources/extensions/gsd/tools/plan-task.ts +107 -9
  321. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +58 -0
  322. package/src/resources/extensions/gsd/unit-context-manifest.ts +11 -5
  323. package/src/resources/extensions/gsd/unit-registry.ts +34 -2
  324. package/src/resources/extensions/gsd/unit-runtime.ts +42 -0
  325. package/src/resources/extensions/gsd/workspace.ts +42 -8
  326. package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
  327. package/src/resources/extensions/gsd/worktree-manager.ts +143 -5
  328. /package/dist/web/standalone/.next/static/{zQLcXm0Za8lexoXtgDpMG → bCg3aYrFN1DKZr2dqgE7L}/_buildManifest.js +0 -0
  329. /package/dist/web/standalone/.next/static/{zQLcXm0Za8lexoXtgDpMG → bCg3aYrFN1DKZr2dqgE7L}/_ssgManifest.js +0 -0
@@ -16,6 +16,7 @@ import { GSDError, GSD_IO_ERROR } from "./errors.js";
16
16
  const SEQ_PREFIX_RE = /^(\d+)-/;
17
17
  import type { ExtensionContext } from "@gsd/pi-coding-agent";
18
18
  import { gsdRoot } from "./paths.js";
19
+ import { redactSecrets } from "./redact-secrets.js";
19
20
  import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
20
21
  import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
21
22
 
@@ -127,7 +128,9 @@ export function saveActivityLog(
127
128
  const fd = openSync(filePath, "w");
128
129
  try {
129
130
  for (const entry of entries) {
130
- writeSync(fd, JSON.stringify(entry) + "\n");
131
+ // Redact secret-shaped substrings before persisting; .gsd/ is skipped by
132
+ // the secret scanner, so raw LLM/tool content could otherwise leak keys.
133
+ writeSync(fd, redactSecrets(JSON.stringify(entry)) + "\n");
131
134
  }
132
135
  } finally {
133
136
  closeSync(fd);
@@ -45,7 +45,6 @@ import { resolveWorktreeProjectRoot } from "./worktree-root.js";
45
45
  import { hasImplementationArtifacts } from "./milestone-implementation-evidence.js";
46
46
  import { loadAllCaptures, loadPendingCaptures } from "./captures.js";
47
47
  import { proveMilestoneCloseout } from "./milestone-closeout-proof.js";
48
-
49
48
  /**
50
49
  * Optional override for the legacy roadmap parser used by verifyExpectedArtifact.
51
50
  * Production leaves this null so the real parseLegacyRoadmap runs; tests inject
@@ -72,6 +71,11 @@ function parseRoadmapForRecovery(content: string): ReturnType<NonNullable<typeof
72
71
  return parseLegacyRoadmap(content) as unknown as ReturnType<NonNullable<typeof _roadmapParserFn>>;
73
72
  }
74
73
 
74
+ /** Slice count for plan-milestone verification; shared by scoped and legacy paths. */
75
+ export function countPlanMilestoneRoadmapSlices(content: string): number {
76
+ return parseRoadmapForRecovery(content).slices.length;
77
+ }
78
+
75
79
  export function diagnoseWorktreeIntegrityFailure(basePath: string): string | null {
76
80
  if (!isGsdWorktreePath(basePath)) return null;
77
81
  if (!existsSync(basePath)) {
@@ -336,8 +340,7 @@ export function verifyExpectedArtifact(
336
340
 
337
341
  if (unitType === "plan-milestone") {
338
342
  try {
339
- const roadmap = parseRoadmapForRecovery(readFileSync(absPath, "utf-8"));
340
- if (roadmap.slices.length === 0) {
343
+ if (countPlanMilestoneRoadmapSlices(readFileSync(absPath, "utf-8")) === 0) {
341
344
  logWarning("recovery", `verify-fail ${unitType} ${unitId}: roadmap has zero slices at ${absPath}`);
342
345
  return false;
343
346
  }
@@ -353,6 +356,19 @@ export function verifyExpectedArtifact(
353
356
  try {
354
357
  let taskIds: string[] | null = null;
355
358
  let dbPrimary = false;
359
+ const planContent = readFileSync(absPath, "utf-8");
360
+ let parsedTaskIds: string[] | null = null;
361
+ const getParsedTaskIds = (): string[] => {
362
+ if (parsedTaskIds) return parsedTaskIds;
363
+ parsedTaskIds = parseLegacyPlan(planContent).tasks.map((t: { id: string }) => t.id);
364
+ return parsedTaskIds;
365
+ };
366
+ const tasksBlockMatch = planContent.match(/<tasks>([\s\S]*?)<\/tasks>/i);
367
+ const tasksBlock = tasksBlockMatch?.[1] ?? "";
368
+ const hasEmbeddedTaskEntries =
369
+ tasksBlock.length > 0 &&
370
+ (/^\s*- \[[xX ]\] \*\*T\d+/m.test(tasksBlock) ||
371
+ /^\s*#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(tasksBlock));
356
372
  if (isDbAvailable()) {
357
373
  const refreshed = refreshWorkflowDatabaseFromDisk();
358
374
  if (refreshed) {
@@ -365,24 +381,24 @@ export function verifyExpectedArtifact(
365
381
  }
366
382
 
367
383
  if (!taskIds) {
368
- const planContent = readFileSync(absPath, "utf-8");
369
384
  const hasCheckboxTask = /^\s*- \[[xX ]\] \*\*T\d+/m.test(planContent);
370
385
  const hasHeadingTask = /^\s*#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(planContent);
371
386
  if (!hasCheckboxTask && !hasHeadingTask) {
372
387
  logWarning("recovery", `verify-fail ${unitType} ${unitId}: plan has no task checkbox/heading (len=${planContent.length}) at ${absPath}`);
373
388
  return false;
374
389
  }
375
- const plan = parseLegacyPlan(planContent);
376
- if (plan.tasks.length > 0) taskIds = plan.tasks.map((t: { id: string }) => t.id);
390
+ const parsedIds = getParsedTaskIds();
391
+ if (parsedIds.length > 0) taskIds = parsedIds;
377
392
  }
378
393
 
379
- if (taskIds && taskIds.length > 0) {
394
+ if (taskIds && taskIds.length > 0 && !hasEmbeddedTaskEntries) {
380
395
  const tasksDir = join(dirname(absPath), "tasks");
381
396
  if (existsSync(tasksDir)) {
382
397
  for (const tid of taskIds) {
383
398
  const taskPlanFile = join(tasksDir, `${tid}-PLAN.md`);
384
- if (!existsSync(taskPlanFile)) {
385
- logWarning("recovery", `verify-fail ${unitType} ${unitId}: task plan missing ${taskPlanFile}`);
399
+ const taskSummaryFile = join(tasksDir, `${tid}-SUMMARY.md`);
400
+ if (!existsSync(taskPlanFile) && !existsSync(taskSummaryFile)) {
401
+ logWarning("recovery", `verify-fail ${unitType} ${unitId}: task artifact missing for ${tid}`);
386
402
  return false;
387
403
  }
388
404
  }
@@ -79,13 +79,14 @@ export async function restorePreflightStashOrStop(
79
79
  }
80
80
 
81
81
  /**
82
- * Run a milestone merge surrounded by preflight stash + always-on postflight
83
- * pop. The previous code popped the stash only after a successful merge, which
84
- * leaked `gsd-preflight-stash:M00x:*` entries whenever `mergeAndExit` threw —
85
- * leaving the user's pre-merge working tree silently stashed away after a
86
- * merge-conflict or other merge error. This helper restores the stash on
87
- * every exit path, then surfaces the merge or stash failure (in priority
88
- * order) as the loop's stop reason.
82
+ * Run a milestone merge through Worktree Lifecycle's guarded merge option,
83
+ * which surrounds the inner merge with preflight stash + always-on postflight
84
+ * pop. The previous closeout code popped the stash only after a successful
85
+ * merge, which leaked `gsd-preflight-stash:M00x:*` entries whenever
86
+ * `mergeAndExit` threw leaving the user's pre-merge working tree silently
87
+ * stashed away after a merge-conflict or other merge error. Lifecycle now
88
+ * restores the stash on every attempted merge path, then this adapter surfaces
89
+ * the merge or stash failure (in priority order) as the loop's stop reason.
89
90
  *
90
91
  * Returns a `break` action when auto-mode must stop, or `null` when the merge
91
92
  * succeeded and the stash (if any) was restored cleanly.
@@ -97,66 +98,45 @@ export async function _runMilestoneMergeWithStashRestore(
97
98
  ): Promise<{ action: "break"; reason: string } | null> {
98
99
  const { ctx, pi, s, deps } = ic;
99
100
 
100
- const preflight = deps.preflightCleanRoot(
101
- s.originalBasePath || s.basePath,
101
+ const projectRoot = s.originalBasePath || s.basePath;
102
+ const mergeResult = deps.lifecycle.exitMilestone(
102
103
  milestoneId,
103
- ctx.ui.notify.bind(ctx.ui),
104
+ {
105
+ merge: true,
106
+ guardedMerge: {
107
+ projectRoot,
108
+ preflightCleanRoot: deps.preflightCleanRoot,
109
+ postflightPopStash: deps.postflightPopStash,
110
+ },
111
+ },
112
+ ctx.ui,
104
113
  );
105
- if (preflight.blocked) {
106
- const reason = preflight.blockedReason === "unmerged-conflicts"
114
+
115
+ if (mergeResult.ok) {
116
+ await markMilestoneMergedAndRebuild(s);
117
+ return null;
118
+ }
119
+
120
+ if (mergeResult.reason === "postflight-stash-restore-failed") {
121
+ await markMilestoneMergedAndRebuild(s);
122
+ }
123
+
124
+ if (mergeResult.reason === "preflight-dirty-overlap" || mergeResult.reason === "preflight-unmerged-conflicts") {
125
+ const reason = mergeResult.reason === "preflight-unmerged-conflicts"
107
126
  ? `Pre-merge unresolved Git conflicts block milestone ${milestoneId}`
108
127
  : `Pre-merge dirty working tree overlaps milestone ${milestoneId}`;
109
128
  await deps.stopAuto(ctx, pi, reason, {
110
129
  preserveCompletedMilestoneBranch: true,
111
130
  preserveCloseoutTranscript: options.preserveCloseoutTranscript,
112
131
  });
113
- return {
114
- action: "break",
115
- reason: preflight.blockedReason === "unmerged-conflicts"
116
- ? "preflight-unmerged-conflicts"
117
- : "preflight-dirty-overlap",
118
- };
119
- }
120
-
121
- let mergeError: unknown = null;
122
- const exitResult = deps.lifecycle.exitMilestone(
123
- milestoneId,
124
- { merge: true },
125
- ctx.ui,
126
- );
127
- if (exitResult.ok) {
128
- s.milestoneMergedInPhases = true;
129
- try {
130
- const projectRoot = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
131
- const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.js");
132
- await rebuildMarkdownProjectionsFromDb(projectRoot);
133
- } catch (err) {
134
- logWarning(
135
- "engine",
136
- `markdown projection rebuild after milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
137
- );
138
- }
139
- } else {
140
- mergeError = exitResult.cause ?? new Error(`exit ${exitResult.reason}`);
141
- }
142
-
143
- // Always attempt to restore the stashed working tree, even on merge error.
144
- // postflightPopStash itself does not throw; failures surface via the
145
- // PostflightResult.needsManualRecovery flag.
146
- let stashResult: PostflightResult | null = null;
147
- if (preflight.stashPushed) {
148
- stashResult = deps.postflightPopStash(
149
- s.originalBasePath || s.basePath,
150
- milestoneId,
151
- preflight.stashMarker,
152
- ctx.ui.notify.bind(ctx.ui),
153
- );
132
+ return { action: "break", reason: mergeResult.reason };
154
133
  }
155
134
 
156
135
  // Merge failure takes priority over stash recovery — the merge is the
157
136
  // authoritative gate. If the stash also needed manual recovery, the user
158
137
  // already saw the postflightPopStash notify above.
159
- if (mergeError) {
138
+ if (mergeResult.reason === "merge-conflict") {
139
+ const mergeError = mergeResult.cause;
160
140
  if (mergeError instanceof MergeConflictError) {
161
141
  // A merge conflict is a recoverable human checkpoint, not an
162
142
  // infrastructure failure — the user resolves the conflict and runs
@@ -173,6 +153,10 @@ export async function _runMilestoneMergeWithStashRestore(
173
153
  });
174
154
  return { action: "break", reason: "merge-conflict" };
175
155
  }
156
+ }
157
+
158
+ if (mergeResult.reason === "merge-failed" || mergeResult.reason === "merge-conflict") {
159
+ const mergeError = mergeResult.cause;
176
160
  logError("engine", "Milestone merge failed with non-conflict error", {
177
161
  milestone: milestoneId,
178
162
  error: String(mergeError),
@@ -190,12 +174,26 @@ export async function _runMilestoneMergeWithStashRestore(
190
174
  return { action: "break", reason: "merge-failed" };
191
175
  }
192
176
 
193
- if (stashResult) {
194
- return stopOnPostflightRecoveryNeeded(ic, stashResult, milestoneId);
177
+ if (mergeResult.postflight) {
178
+ return stopOnPostflightRecoveryNeeded(ic, mergeResult.postflight, milestoneId);
195
179
  }
196
180
  return null;
197
181
  }
198
182
 
183
+ async function markMilestoneMergedAndRebuild(s: AutoSession): Promise<void> {
184
+ s.milestoneMergedInPhases = true;
185
+ try {
186
+ const rebuildBasePath = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
187
+ const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.js");
188
+ await rebuildMarkdownProjectionsFromDb(rebuildBasePath);
189
+ } catch (err) {
190
+ logWarning(
191
+ "engine",
192
+ `markdown projection rebuild after milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
193
+ );
194
+ }
195
+ }
196
+
199
197
  export async function _runMilestoneMergeOnceWithStashRestore(
200
198
  ic: IterationContext,
201
199
  milestoneId: string,
@@ -373,6 +373,14 @@ export async function runFinalize(
373
373
  files: leak.files.map((file) => ({ path: file.path, status: file.status })),
374
374
  });
375
375
  ctx.ui.notify(message, "error");
376
+ try {
377
+ deps.checkpointWorkflowDatabase?.();
378
+ } catch (err) {
379
+ debugLog("autoLoop", {
380
+ phase: "root-write-leak-checkpoint-failed",
381
+ error: err instanceof Error ? err.message : String(err),
382
+ });
383
+ }
376
384
  await deps.stopAuto(ctx, pi, "Root-write leak during isolated auto-mode", {
377
385
  preserveCompletedMilestoneBranch: true,
378
386
  });
@@ -80,6 +80,7 @@ export interface LoopDeps {
80
80
  ) => Promise<void>;
81
81
  pauseAuto: PauseAutoFn;
82
82
  clearUnitTimeout: () => void;
83
+ checkpointWorkflowDatabase?: () => void;
83
84
  updateProgressWidget: (
84
85
  ctx: ExtensionContext,
85
86
  unitType: string,
@@ -731,7 +731,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
731
731
  return new GitServiceImpl(basePath, gitConfig);
732
732
  },
733
733
  worktreeProjection: new WorktreeStateProjection(),
734
- mergeMilestoneToMain: createMilestoneMergeTransaction(mergeMilestoneToMain),
734
+ mergeMilestone: createMilestoneMergeTransaction(mergeMilestoneToMain),
735
735
  });
736
736
  }
737
737
 
@@ -26,7 +26,7 @@ import { debugLog } from "../debug-logger.js";
26
26
  import { logWarning, logError } from "../workflow-logger.js";
27
27
  import { resolveAutoSupervisorConfig } from "../preferences.js";
28
28
  import { readUnitRuntimeRecord, type AutoUnitRuntimeRecord } from "../unit-runtime.js";
29
- import { consumeAutoWakeup } from "./schedule-wakeup.js";
29
+ import { clearAutoWakeup, consumeAutoWakeup } from "./schedule-wakeup.js";
30
30
  import { applyUnitSkillVisibility } from "../skill-scope.js";
31
31
 
32
32
  const UNIT_FAILSAFE_BUFFER_MS = 30_000;
@@ -248,7 +248,7 @@ export async function runUnit(
248
248
  ((supervisor.idle_timeout_minutes ?? 10) * 60 * 1000) + UNIT_FAILSAFE_BUFFER_MS,
249
249
  );
250
250
  let unitTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
251
- let result: UnitResult;
251
+ let result: UnitResult = { status: "cancelled" };
252
252
  try {
253
253
  const awaitAgentEndWithTimeout = (agentEndPromise: Promise<UnitResult>): Promise<UnitResult> => {
254
254
  const timeoutResult = new Promise<UnitResult>((resolve) => {
@@ -360,6 +360,9 @@ export async function runUnit(
360
360
  }
361
361
  } finally {
362
362
  if (unitTimeoutHandle) clearTimeout(unitTimeoutHandle);
363
+ if (result.status !== "completed") {
364
+ clearAutoWakeup(s.basePath, unitType, unitId);
365
+ }
363
366
  ctx.ui.setWorkingMessage?.(undefined);
364
367
  setVisibleSkills?.(undefined);
365
368
  }
@@ -35,6 +35,14 @@ export function consumeAutoWakeup(
35
35
  return wakeup;
36
36
  }
37
37
 
38
+ export function clearAutoWakeup(
39
+ basePath: string,
40
+ unitType: string,
41
+ unitId: string,
42
+ ): void {
43
+ pendingWakeups.delete(wakeupKey(basePath, unitType, unitId));
44
+ }
45
+
38
46
  export function _resetAutoWakeupsForTest(): void {
39
47
  pendingWakeups.clear();
40
48
  }
@@ -838,7 +838,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
838
838
  },
839
839
  },
840
840
  {
841
- name: "uat-verdict-gate (non-PASS blocks progression)",
841
+ name: "uat-verdict-gate (non-PASS observed; closeout enforces)",
842
842
  match: async ({ mid, basePath, prefs }) => {
843
843
  // Only applies when UAT dispatch is enabled
844
844
  if (!prefs?.uat_dispatch) return null;
@@ -852,9 +852,9 @@ export const DISPATCH_RULES: DispatchRule[] = [
852
852
  const { verdict, uatType } = result;
853
853
 
854
854
  if (!isAcceptableUatVerdict(verdict, uatType)) {
855
- // Do not hard-stop auto-mode on non-PASS verdicts. Allow progression
856
- // so follow-up slices can remediate, while complete-milestone still
857
- // enforces manual UAT PASS sign-off before closure.
855
+ // Observe non-PASS verdicts without hard-stopping auto-mode. Allow
856
+ // progression so follow-up slices can remediate, while
857
+ // complete-milestone still enforces manual UAT PASS sign-off before closure.
858
858
  continue;
859
859
  }
860
860
  }
@@ -1130,9 +1130,21 @@ export const DISPATCH_RULES: DispatchRule[] = [
1130
1130
  // call site.
1131
1131
  const sliceContextFile = resolveSliceFile(basePath, mid, state.activeSlice.id, "CONTEXT");
1132
1132
  if (sliceContextFile && existsSync(sliceContextFile)) return null; // discussion already done, proceed
1133
+
1134
+ const closedSliceIds = getClosedSliceIds(mid);
1135
+ const justClosedSliceId = closedSliceIds[closedSliceIds.length - 1];
1136
+ let priorVerdictWarning = "";
1137
+ if (justClosedSliceId) {
1138
+ const prior = await readUatGateVerdict(basePath, mid, justClosedSliceId);
1139
+ if (prior && !isAcceptableUatVerdict(prior.verdict, prior.uatType)) {
1140
+ priorVerdictWarning =
1141
+ ` Note: the slice just closed (${justClosedSliceId}) recorded a non-PASS UAT verdict (${prior.verdict.toUpperCase()}); review before continuing.`;
1142
+ }
1143
+ }
1144
+
1133
1145
  return {
1134
1146
  action: "stop" as const,
1135
- reason: `Slice ${state.activeSlice.id} requires discussion before planning (require_slice_discussion is enabled). Run /gsd discuss to discuss this slice, then /gsd auto to resume.`,
1147
+ reason: `Slice ${state.activeSlice.id} requires discussion before planning (require_slice_discussion is enabled). Run /gsd discuss to discuss this slice, then /gsd auto to resume.${priorVerdictWarning}`,
1136
1148
  level: "warning" as const,
1137
1149
  };
1138
1150
  },
@@ -409,7 +409,13 @@ export function resolvePreferredModelConfig(
409
409
  availableModelIds?: string[],
410
410
  preferredModelId?: string,
411
411
  ): PreferredModelConfig | undefined {
412
- const explicitConfig = resolveModelWithFallbacksForUnit(unitType, basePath, availableModelIds, preferredModelId);
412
+ const explicitConfig = resolveModelWithFallbacksForUnit(
413
+ unitType,
414
+ basePath,
415
+ availableModelIds,
416
+ preferredModelId,
417
+ true,
418
+ );
413
419
  if (explicitConfig) {
414
420
  return {
415
421
  ...explicitConfig,
@@ -422,26 +428,55 @@ export function resolvePreferredModelConfig(
422
428
  if (!isAutoMode) return undefined;
423
429
 
424
430
  const routingConfig = resolveDynamicRoutingConfig();
425
- if (!routingConfig.enabled || !routingConfig.tier_models) return undefined;
431
+ if (!routingConfig.enabled) return undefined;
426
432
 
427
- // Don't synthesize a routing config for flat-rate providers (#3453).
428
- // Users can opt into routing for flat-rate subscriptions (e.g. claude-code)
429
- // via dynamic_routing.allow_flat_rate_providers (#4386).
433
+ // Only an explicit flat-rate opt-out suppresses synthesized routing.
434
+ // `dynamic_routing.enabled: true` is user intent to run routing even when
435
+ // the start provider is a flat-rate subscription. Applied ahead of both the
436
+ // tier-pin and profile-derived synthesis paths so the opt-out is uniform.
430
437
  if (
431
- !routingConfig.allow_flat_rate_providers &&
438
+ routingConfig.allow_flat_rate_providers === false &&
432
439
  autoModeStartModel &&
433
440
  isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx)
434
441
  ) {
435
442
  return undefined;
436
443
  }
437
444
 
438
- const ceilingModel = routingConfig.tier_models.heavy
439
- ?? (autoModeStartModel ? `${autoModeStartModel.provider}/${autoModeStartModel.id}` : undefined);
440
- if (!ceilingModel) return undefined;
445
+ // With explicit `tier_models` pins, synthesize a heavy-tier ceiling that
446
+ // dynamic routing downgrades from per unit.
447
+ if (routingConfig.tier_models) {
448
+ const ceilingModel = routingConfig.tier_models.heavy
449
+ ?? (autoModeStartModel ? `${autoModeStartModel.provider}/${autoModeStartModel.id}` : undefined);
450
+ if (!ceilingModel) return undefined;
451
+
452
+ return {
453
+ primary: ceilingModel,
454
+ fallbacks: [],
455
+ source: "synthesized",
456
+ };
457
+ }
458
+
459
+ // No `tier_models` pins: fall back to the token-profile's per-phase model so
460
+ // profile tier resolution survives. `skipProfileDefaults` is left false here
461
+ // (unlike the explicit-detection call above) precisely because we WANT the
462
+ // profile defaults this time — they are the routing ceiling, not a hard
463
+ // user selection. Labeling the result `synthesized` (not `explicit`) keeps
464
+ // dynamic routing and fallbacks enabled downstream. Without this branch,
465
+ // `resolvePreferredModelConfig` returns undefined for profile-only configs
466
+ // and auto dispatch bleeds the start model onto every unit, silently
467
+ // discarding the profile (#1115: routing/fallbacks disabled when a
468
+ // token_profile is set but no tier_models are pinned).
469
+ const profileConfig = resolveModelWithFallbacksForUnit(
470
+ unitType,
471
+ basePath,
472
+ availableModelIds,
473
+ preferredModelId,
474
+ false,
475
+ );
476
+ if (!profileConfig) return undefined;
441
477
 
442
478
  return {
443
- primary: ceilingModel,
444
- fallbacks: [],
479
+ ...profileConfig,
445
480
  source: "synthesized",
446
481
  };
447
482
  }
@@ -647,15 +682,11 @@ export async function selectAndApplyModel(
647
682
  }
648
683
  }
649
684
 
650
- // Disable routing for flat-rate providers like GitHub Copilot (#3453).
651
- // All models cost the same per request, so downgrading to a cheaper
652
- // model provides no cost benefit — it only degrades quality.
685
+ // Disable routing for flat-rate providers only when the user explicitly
686
+ // opts out with dynamic_routing.allow_flat_rate_providers: false.
653
687
  // Fail-closed: if primary model can't be resolved, fall back to
654
688
  // provider-level signals rather than allowing unwanted downgrades.
655
- // Opt-in: dynamic_routing.allow_flat_rate_providers skips the bypass so
656
- // claude-code subscribers can still get intelligent per-task selection
657
- // across their subscription (#4386).
658
- if (routingConfig.enabled && !routingConfig.allow_flat_rate_providers) {
689
+ if (routingConfig.enabled && routingConfig.allow_flat_rate_providers === false) {
659
690
  const primaryModel = resolveModelId(modelConfig.primary, routingEligibleModels, ctx.model?.provider);
660
691
  if (primaryModel) {
661
692
  const primaryFlatRateCtx = buildFlatRateContext(primaryModel.provider, ctx, prefs);
@@ -20,14 +20,15 @@ import {
20
20
  relTaskFile, resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile,
21
21
  } from "./paths.js";
22
22
  import { resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
23
+ import { createRepositoryRegistryFromPreferences } from "./repository-registry.js";
23
24
  import { isContextModeEnabled } from "./preferences-types.js";
24
25
  import { parseRoadmap } from "./parsers-legacy.js";
25
26
  import type { GSDState, InlineLevel } from "./types.js";
26
27
  import type { GSDPreferences } from "./preferences.js";
27
- import { join, basename, relative } from "node:path";
28
+ import { join, basename, relative, sep } from "node:path";
28
29
  import { existsSync } from "node:fs";
29
30
  import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary, type MinimalModelRegistry } from "./context-budget.js";
30
- import { getPendingGates, getPendingGatesForTurn } from "./gsd-db.js";
31
+ import { getGateResults, getPendingGates, getPendingGatesForTurn } from "./gsd-db.js";
31
32
  import {
32
33
  GATE_REGISTRY,
33
34
  assertGateCoverage,
@@ -393,6 +394,37 @@ function formatExecutorConstraints(
393
394
  ].join("\n");
394
395
  }
395
396
 
397
+ /**
398
+ * Builds the Declared Repositories block for parent-workspace planners.
399
+ * Returns "" for single-repo projects so the common-case prompt is unchanged;
400
+ * only parent mode with declared child repos gets the registry list and the
401
+ * targetRepositories assignment instruction.
402
+ */
403
+ export function buildRepoRegistryBlock(base: string): string {
404
+ let registry;
405
+ try {
406
+ registry = createRepositoryRegistryFromPreferences(base, loadEffectiveGSDPreferences(base)?.preferences);
407
+ } catch {
408
+ return "";
409
+ }
410
+ const hasChildRepo = registry.repositories.some((repo) => repo.id !== "project");
411
+ if (registry.mode !== "parent" || !hasChildRepo) return "";
412
+
413
+ const lines = registry.repositories.map((repo) => {
414
+ const relPath = relative(registry.projectRoot, repo.root).split(sep).join("/") || ".";
415
+ const role = repo.role ? ` — ${repo.role}` : "";
416
+ const root = repo.id === "project" ? " (root)" : ` (${relPath})`;
417
+ return `- \`${repo.id}\`${root}${role}`;
418
+ });
419
+ return [
420
+ "### Declared Repositories",
421
+ "",
422
+ ...lines,
423
+ "",
424
+ "This is a parent workspace. Assign each task's `targetRepositories` to the repository id(s) it touches (from the list above). Omit `targetRepositories` to inherit the slice-wide default from `gsd_plan_slice`; set it explicitly when a task differs (e.g. `[\"project\"]` for parent-root work).",
425
+ ].join("\n");
426
+ }
427
+
396
428
  /**
397
429
  * Returns a markdown bullet list of known context file paths for the given
398
430
  * milestone (and optionally slice). Falls back to a generic tool-agnostic
@@ -2504,6 +2536,7 @@ async function renderSlicePrompt(options: {
2504
2536
  inlinedContext,
2505
2537
  dependencySummaries: depContent,
2506
2538
  sourceFilePaths: buildSourceFilePaths(base, mid, sid),
2539
+ repoRegistry: buildRepoRegistryBlock(base),
2507
2540
  executorContextConstraints,
2508
2541
  commitInstruction,
2509
2542
  skillActivation: buildSkillActivationBlock({
@@ -3362,6 +3395,44 @@ export async function buildValidateMilestonePrompt(
3362
3395
  trackPromptContext(contextTelemetry, "outstanding-items", "inline", outstandingBlock);
3363
3396
  }
3364
3397
 
3398
+ const persistedGateFlags: string[] = [];
3399
+ try {
3400
+ const gateLabels = {
3401
+ Q3: "Threat Surface",
3402
+ Q4: "Requirement Impact",
3403
+ } as const;
3404
+ for (const sid of seenValSlices) {
3405
+ for (const gate of getGateResults(mid, sid, "slice")) {
3406
+ if (gate.gate_id !== "Q3" && gate.gate_id !== "Q4") continue;
3407
+ const findings = gate.findings.trim();
3408
+ const rationale = gate.rationale.trim();
3409
+ const verdict = gate.verdict ?? "";
3410
+ const hasNonPassVerdict = verdict !== "" && verdict !== "pass" && verdict !== "omitted";
3411
+ if (!findings && !hasNonPassVerdict) continue;
3412
+
3413
+ const displayVerdict = verdict || gate.status;
3414
+ const detail = findings || rationale || "_No findings text recorded; reconcile the persisted non-pass verdict._";
3415
+ persistedGateFlags.push(`- **${sid} / ${gate.gate_id} (${gateLabels[gate.gate_id]}) / ${displayVerdict}:** ${detail}`);
3416
+ if (findings && rationale) persistedGateFlags.push(` - Rationale: ${rationale}`);
3417
+ }
3418
+ }
3419
+ } catch (err) {
3420
+ logWarning("prompt", `buildValidateMilestonePrompt persisted gate flags lookup failed: ${err instanceof Error ? err.message : String(err)}`);
3421
+ }
3422
+ if (persistedGateFlags.length > 0) {
3423
+ const persistedGateFlagsBlock = [
3424
+ "### Persisted Slice-Level Gate Flags (from quality_gates)",
3425
+ "",
3426
+ "These slice gates recorded findings or non-pass verdicts during execution. Reconcile each against the milestone-level evidence before returning MV03/MV04 verdicts.",
3427
+ "",
3428
+ ...persistedGateFlags,
3429
+ ].join("\n");
3430
+ inlined.push(persistedGateFlagsBlock);
3431
+ trackPromptContext(contextTelemetry, "persisted-slice-gate-flags", "inline", persistedGateFlagsBlock);
3432
+ } else {
3433
+ trackPromptContext(contextTelemetry, "persisted-slice-gate-flags", "skipped", null, "none");
3434
+ }
3435
+
3365
3436
  // Inline existing VALIDATION file if this is a re-validation round
3366
3437
  const validationPath = resolveMilestoneFile(base, mid, "VALIDATION");
3367
3438
  const validationRel = relMilestoneFile(base, mid, "VALIDATION");
@@ -1849,8 +1849,9 @@ export async function bootstrapAutoSession(
1849
1849
  : ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "default";
1850
1850
 
1851
1851
  // Flat-rate providers (e.g. GitHub Copilot, claude-code, user-declared
1852
- // subscription proxies, externalCli CLIs) suppress routing at dispatch
1853
- // time (#3453) reflect that in the banner. Thread the same
1852
+ // subscription proxies, externalCli CLIs) suppress routing only when the
1853
+ // user explicitly opts out with allow_flat_rate_providers: false. Reflect
1854
+ // that in the banner. Thread the same
1854
1855
  // FlatRateContext used by selectAndApplyModel so user-declared
1855
1856
  // flat-rate providers and externalCli auto-detection are respected.
1856
1857
  const { isFlatRateProvider, buildFlatRateContext } = await import("./auto-model-selection.js");
@@ -1862,7 +1863,7 @@ export async function bootstrapAutoSession(
1862
1863
  )?.preferences;
1863
1864
  const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
1864
1865
  const effectivelyEnabled = routingConfig.enabled
1865
- && (routingConfig.allow_flat_rate_providers
1866
+ && (routingConfig.allow_flat_rate_providers !== false
1866
1867
  || !(effectiveProvider && isFlatRateProvider(
1867
1868
  effectiveProvider,
1868
1869
  buildFlatRateContext(effectiveProvider, ctx, bannerPrefs),
@@ -97,6 +97,8 @@ export async function recoverTimedOutUnit(
97
97
  lastProgressAt: Date.now(),
98
98
  progressCount: (runtime?.progressCount ?? 0) + 1,
99
99
  lastProgressKind: reason === "idle" ? "idle-recovery-retry" : "hard-recovery-retry",
100
+ // This steering retry is a fresh chance for the same unit to save a valid result.
101
+ harnessAbort: undefined,
100
102
  });
101
103
 
102
104
  const steeringLines = isEscalation
@@ -222,6 +224,8 @@ export async function recoverTimedOutUnit(
222
224
  lastProgressAt: Date.now(),
223
225
  progressCount: (runtime?.progressCount ?? 0) + 1,
224
226
  lastProgressKind: reason === "idle" ? "idle-recovery-retry" : "hard-recovery-retry",
227
+ // This steering retry is a fresh chance for the same unit to save a valid result.
228
+ harnessAbort: undefined,
225
229
  });
226
230
 
227
231
  const steeringLines = isEscalation