@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
@@ -9,16 +9,62 @@ import { migrateFromMarkdown } from "./md-importer.js";
9
9
  import { countDbHierarchy } from "./migration-auto-check.js";
10
10
  import { logWarning } from "./workflow-logger.js";
11
11
  import { LAYOUT_SEGMENTS } from "./layout-policy.js";
12
- import { canonicalPhaseDirName, milestonesDir, resolveMilestonePath } from "./paths.js";
12
+ import { canonicalPhaseDirName, dirIsContentBearingLegacyMilestone, milestonesDir, resolveMilestonePath, } from "./paths.js";
13
13
  const LEGACY_MIGRATING_SEGMENT = "milestones.migrating";
14
+ const RETRYABLE_FS_ERROR_CODES = new Set(["EPERM", "EBUSY", "ENOTEMPTY"]);
15
+ const RM_RETRY_OPTIONS = { recursive: true, force: true, maxRetries: 5, retryDelay: 100 };
16
+ let fsOps = { cpSync, renameSync, rmSync };
17
+ export function _setFlatPhaseMigrationFsOpsForTest(overrides) {
18
+ const previous = fsOps;
19
+ fsOps = { ...fsOps, ...overrides };
20
+ return () => {
21
+ fsOps = previous;
22
+ };
23
+ }
14
24
  function legacyMigratingPath(basePath) {
15
25
  return join(basePath, ".gsd", LEGACY_MIGRATING_SEGMENT);
16
26
  }
27
+ function removePathWithRetries(path) {
28
+ fsOps.rmSync(path, { recursive: true, force: true, maxRetries: RM_RETRY_OPTIONS.maxRetries, retryDelay: RM_RETRY_OPTIONS.retryDelay });
29
+ }
30
+ function isRetryableFsError(err) {
31
+ const code = err?.code;
32
+ return typeof code === "string" && RETRYABLE_FS_ERROR_CODES.has(code);
33
+ }
34
+ function movePathWithCopyDeleteFallback(src, dst) {
35
+ try {
36
+ fsOps.renameSync(src, dst);
37
+ return;
38
+ }
39
+ catch (err) {
40
+ if (!isRetryableFsError(err))
41
+ throw err;
42
+ }
43
+ try {
44
+ fsOps.cpSync(src, dst, { recursive: true, force: true });
45
+ removePathWithRetries(src);
46
+ }
47
+ catch (fallbackErr) {
48
+ if (existsSync(src) && existsSync(dst)) {
49
+ try {
50
+ removePathWithRetries(dst);
51
+ }
52
+ catch {
53
+ // Leave the original error intact; the next run can pre-clean dst.
54
+ }
55
+ }
56
+ throw fallbackErr;
57
+ }
58
+ }
59
+ function expectedPhaseDirs(basePath) {
60
+ return getAllMilestones().map((milestone) => resolveMilestonePath(basePath, milestone.id) ??
61
+ join(milestonesDir(basePath), canonicalPhaseDirName(milestone.id, milestone.title)));
62
+ }
17
63
  function hasLegacyMilestoneSubdirs(dirPath) {
18
64
  if (!existsSync(dirPath))
19
65
  return false;
20
66
  try {
21
- return readdirSync(dirPath).some(e => statSync(join(dirPath, e)).isDirectory());
67
+ return readdirSync(dirPath).some((e) => statSync(join(dirPath, e)).isDirectory() && dirIsContentBearingLegacyMilestone(join(dirPath, e)));
22
68
  }
23
69
  catch {
24
70
  return false;
@@ -26,19 +72,24 @@ function hasLegacyMilestoneSubdirs(dirPath) {
26
72
  }
27
73
  function rollbackPartialMigration(basePath, backupDir, migratingPath) {
28
74
  // Remove the partially-written phases/ dir.
29
- rmSync(join(basePath, ".gsd", LAYOUT_SEGMENTS.level1), { recursive: true, force: true });
75
+ try {
76
+ removePathWithRetries(join(basePath, ".gsd", LAYOUT_SEGMENTS.level1));
77
+ }
78
+ catch (removeErr) {
79
+ logWarning("migration", `rollback: could not remove partial ${LAYOUT_SEGMENTS.level1}/: ${removeErr.message}`);
80
+ }
30
81
  // Restore milestones/ — prefer renaming the preserved migrating copy back.
31
82
  const milestonesPath = join(basePath, ".gsd", "milestones");
32
83
  try {
33
84
  if (migratingPath && existsSync(migratingPath) && !existsSync(milestonesPath)) {
34
- renameSync(migratingPath, milestonesPath);
85
+ movePathWithCopyDeleteFallback(migratingPath, milestonesPath);
35
86
  return;
36
87
  }
37
88
  if (existsSync(backupDir)) {
38
89
  cpSync(backupDir, milestonesPath, { recursive: true });
39
90
  }
40
91
  if (migratingPath && existsSync(migratingPath)) {
41
- rmSync(migratingPath, { recursive: true, force: true });
92
+ removePathWithRetries(migratingPath);
42
93
  }
43
94
  }
44
95
  catch (restoreErr) {
@@ -146,20 +197,29 @@ export async function migrateToFlatPhase(basePath) {
146
197
  logWarning("migration", `flat-phase migration backup failed: ${err.message}`);
147
198
  throw err;
148
199
  }
200
+ if (existsSync(migratingPath)) {
201
+ removePathWithRetries(migratingPath);
202
+ }
149
203
  // 3. Rename legacy tree aside before rendering so path resolvers target
150
204
  // phases/ instead of writing back into the nested milestones/ layout.
151
205
  // Keep the full tree on disk until render+verify succeed (unlike rmSync).
152
206
  try {
153
- renameSync(milestonesPath, migratingPath);
207
+ movePathWithCopyDeleteFallback(milestonesPath, migratingPath);
154
208
  }
155
209
  catch (err) {
156
210
  logWarning("migration", `failed to rename legacy milestones/ before render: ${err.message}`);
157
211
  throw err;
158
212
  }
159
213
  }
160
- else {
161
- // Interrupted run: clear any partial phases/ projection before retrying.
162
- rmSync(phasesPath, { recursive: true, force: true });
214
+ // Clear any stale or partially-rendered flat projection before this run
215
+ // writes. Verification below checks the current DB render, not leftovers.
216
+ try {
217
+ removePathWithRetries(phasesPath);
218
+ }
219
+ catch (err) {
220
+ logWarning("migration", `failed to clear stale phases/ before render: ${err.message}`);
221
+ rollbackPartialMigration(basePath, backupDir, migratingPath);
222
+ throw err;
163
223
  }
164
224
  // 4. Render flat-phase from DB
165
225
  let renderResult;
@@ -191,20 +251,11 @@ export async function migrateToFlatPhase(basePath) {
191
251
  throw new Error(`flat-phase migration render failed: ${renderResult.errors.slice(0, 3).join("; ")}`);
192
252
  }
193
253
  const db = countDbHierarchy();
194
- let renderedDirCount = 0;
195
- try {
196
- // /^\d+-/ matches any numeric prefix (01-, 10-, 100-) so M100+ milestones
197
- // whose dirs start with "100-slug" are counted correctly.
198
- renderedDirCount = readdirSync(phasesPath, { withFileTypes: true })
199
- .filter((entry) => entry.isDirectory() && /^\d+-/.test(entry.name)).length;
200
- }
201
- catch {
202
- // phases/ doesn't exist or is unreadable — same as zero dirs
203
- }
204
- if (db.milestones > 0 && renderedDirCount !== db.milestones) {
205
- logWarning("migration", `phases/ dir count mismatch: expected ${db.milestones}, found ${renderedDirCount}`);
254
+ const missingPhaseDirs = expectedPhaseDirs(basePath).filter((phaseDir) => !existsSync(phaseDir));
255
+ if (db.milestones > 0 && missingPhaseDirs.length > 0) {
256
+ logWarning("migration", `flat-phase migration missing ${missingPhaseDirs.length} expected phase dir(s): ${missingPhaseDirs.join(", ")}`);
206
257
  rollbackPartialMigration(basePath, backupDir, migratingPath);
207
- throw new Error("flat-phase migration verification failed: phases dir milestone count mismatch");
258
+ throw new Error("flat-phase migration verification failed: missing rendered phase directories");
208
259
  }
209
260
  if (db.slices > 0 && renderResult.rendered === 0) {
210
261
  logWarning("migration", "flat-phase migration verification failed: render produced no artifacts for populated DB");
@@ -213,7 +264,7 @@ export async function migrateToFlatPhase(basePath) {
213
264
  }
214
265
  // 6. Verified — remove the renamed legacy tree (backup already on disk).
215
266
  try {
216
- rmSync(migratingPath, { recursive: true, force: true });
267
+ removePathWithRetries(migratingPath);
217
268
  }
218
269
  catch (err) {
219
270
  logWarning("migration", `flat-phase migration succeeded but could not remove ${LEGACY_MIGRATING_SEGMENT}: ${err.message}`);
@@ -16,7 +16,7 @@ import { gsdRoot } from "./paths.js";
16
16
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
17
17
  import { loadEffectiveGSDPreferences } from "./preferences.js";
18
18
  import { logWarning } from "./workflow-logger.js";
19
- import { createRepositoryRegistryFromPreferences } from "./repository-registry.js";
19
+ import { createRepositoryRegistryFromPreferences, defaultRepositoryTargets } from "./repository-registry.js";
20
20
  import { isGsdGitignored } from "./gitignore.js";
21
21
  import { detectWorktreeName, } from "./worktree.js";
22
22
  import { SLICE_BRANCH_RE, QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
@@ -1024,28 +1024,25 @@ export function handleTurnGitActionError(action, err) {
1024
1024
  error: errorWithStreams.stderr?.trim() || errorWithStreams.message || getErrorMessage(err),
1025
1025
  };
1026
1026
  }
1027
+ export function isRepositoryDirty(repoRoot) {
1028
+ try {
1029
+ return runGit(repoRoot, ["status", "--porcelain"]).length > 0;
1030
+ }
1031
+ catch {
1032
+ // Fallback preserves legacy behavior if explicit status probing fails.
1033
+ return nativeHasChanges(repoRoot);
1034
+ }
1035
+ }
1027
1036
  function collectRepositoryDirtyStatus(basePath) {
1028
1037
  const preferences = loadEffectiveGSDPreferences(basePath)?.preferences;
1029
1038
  const registry = createRepositoryRegistryFromPreferences(basePath, preferences);
1030
1039
  const dirtyByRepository = {};
1031
1040
  const registeredRoots = new Set(registry.repositories.map((repo) => resolve(repo.root)));
1032
1041
  for (const repo of registry.repositories) {
1033
- try {
1034
- dirtyByRepository[repo.id] = runGit(repo.root, ["status", "--porcelain"]).length > 0;
1035
- }
1036
- catch {
1037
- // Fallback preserves legacy behavior if explicit status probing fails.
1038
- dirtyByRepository[repo.id] = nativeHasChanges(repo.root);
1039
- }
1042
+ dirtyByRepository[repo.id] = isRepositoryDirty(repo.root);
1040
1043
  }
1041
1044
  for (const repoRoot of findUndeclaredNestedGitRepositories(registry.projectRoot, registeredRoots)) {
1042
- let dirty = false;
1043
- try {
1044
- dirty = runGit(repoRoot, ["status", "--porcelain"]).length > 0;
1045
- }
1046
- catch {
1047
- dirty = nativeHasChanges(repoRoot);
1048
- }
1045
+ const dirty = isRepositoryDirty(repoRoot);
1049
1046
  if (!dirty)
1050
1047
  continue;
1051
1048
  const relPath = relative(registry.projectRoot, repoRoot).split(sep).join("/") || repoRoot;
@@ -1098,7 +1095,7 @@ function findUndeclaredNestedGitRepositories(projectRoot, registeredRoots) {
1098
1095
  function runPerRepositoryCommitAction(args) {
1099
1096
  const preferences = loadEffectiveGSDPreferences(args.basePath)?.preferences;
1100
1097
  const registry = createRepositoryRegistryFromPreferences(args.basePath, preferences);
1101
- const repoIds = args.targetRepositories?.length ? args.targetRepositories : ["project"];
1098
+ const repoIds = args.targetRepositories?.length ? args.targetRepositories : defaultRepositoryTargets(registry);
1102
1099
  const gitPrefs = preferences?.git ?? {};
1103
1100
  const commitMessages = {};
1104
1101
  const commitErrors = {};
@@ -1166,8 +1163,7 @@ export function runTurnGitAction(args) {
1166
1163
  skippedRepositories: repoCommitResult.skippedRepositories,
1167
1164
  };
1168
1165
  }
1169
- const primaryMessage = repoCommitResult.commitMessages[args.targetRepositories?.[0] ?? "project"]
1170
- ?? Object.values(repoCommitResult.commitMessages)[0];
1166
+ const primaryMessage = Object.values(repoCommitResult.commitMessages)[0];
1171
1167
  return {
1172
1168
  action: args.action,
1173
1169
  status: "ok",
@@ -410,6 +410,58 @@ export function insertTask(t) {
410
410
  export function updateTaskStatus(milestoneId, sliceId, taskId, status, completedAt) {
411
411
  applyStatusTransition({ entity: "task", milestoneId, sliceId, taskId, status, completedAt });
412
412
  }
413
+ export function repairTaskCompletionFromSummary(t) {
414
+ const db = getDbOrNull();
415
+ if (!db)
416
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
417
+ transaction(() => {
418
+ const seq = db.prepare(`SELECT COALESCE(MAX(sequence), 0) + 1 AS next_sequence
419
+ FROM tasks
420
+ WHERE milestone_id = :mid AND slice_id = :sid`).get({ ":mid": t.milestoneId, ":sid": t.sliceId });
421
+ db.prepare(`INSERT INTO tasks (
422
+ milestone_id, slice_id, id, title, status, one_liner, narrative,
423
+ verification_result, duration, completed_at, blocker_discovered,
424
+ deviations, known_issues, key_files, key_decisions, full_summary_md,
425
+ sequence
426
+ ) VALUES (
427
+ :milestone_id, :slice_id, :id, :title, 'complete', :one_liner, :narrative,
428
+ :verification_result, :duration, :completed_at, :blocker_discovered,
429
+ :deviations, :known_issues, :key_files, :key_decisions, :full_summary_md,
430
+ :sequence
431
+ )
432
+ ON CONFLICT(milestone_id, slice_id, id) DO UPDATE SET
433
+ title = CASE WHEN NULLIF(:title, '') IS NOT NULL THEN :title ELSE tasks.title END,
434
+ status = 'complete',
435
+ one_liner = CASE WHEN NULLIF(:one_liner, '') IS NOT NULL THEN :one_liner ELSE tasks.one_liner END,
436
+ narrative = CASE WHEN NULLIF(:narrative, '') IS NOT NULL THEN :narrative ELSE tasks.narrative END,
437
+ verification_result = :verification_result,
438
+ duration = CASE WHEN NULLIF(:duration, '') IS NOT NULL THEN :duration ELSE tasks.duration END,
439
+ completed_at = :completed_at,
440
+ blocker_discovered = :blocker_discovered,
441
+ deviations = CASE WHEN NULLIF(:deviations, '') IS NOT NULL THEN :deviations ELSE tasks.deviations END,
442
+ known_issues = CASE WHEN NULLIF(:known_issues, '') IS NOT NULL THEN :known_issues ELSE tasks.known_issues END,
443
+ key_files = :key_files,
444
+ key_decisions = :key_decisions,
445
+ full_summary_md = :full_summary_md`).run({
446
+ ":milestone_id": t.milestoneId,
447
+ ":slice_id": t.sliceId,
448
+ ":id": t.taskId,
449
+ ":title": t.title ?? "",
450
+ ":one_liner": t.oneLiner ?? "",
451
+ ":narrative": t.narrative ?? "",
452
+ ":verification_result": t.verificationResult,
453
+ ":duration": t.duration ?? "",
454
+ ":completed_at": t.completedAt,
455
+ ":blocker_discovered": t.blockerDiscovered ? 1 : 0,
456
+ ":deviations": t.deviations ?? "",
457
+ ":known_issues": t.knownIssues ?? "",
458
+ ":key_files": JSON.stringify(t.keyFiles ?? []),
459
+ ":key_decisions": JSON.stringify(t.keyDecisions ?? []),
460
+ ":full_summary_md": t.fullSummaryMd,
461
+ ":sequence": seq?.next_sequence ?? 1,
462
+ });
463
+ });
464
+ }
413
465
  export function setTaskBlockerDiscovered(milestoneId, sliceId, taskId, discovered) {
414
466
  if (!getDbOrNull())
415
467
  return;
@@ -151,6 +151,7 @@ const DOCTOR_FIX_HINTS = {
151
151
  state_file_stale: "Run `/gsd doctor fix` to rebuild the projection from the database.",
152
152
  state_file_missing: "Run `/gsd doctor fix` to rebuild the projection from the database.",
153
153
  projection_drift: "Run `/gsd doctor fix` to rebuild markdown projections from the database (DB is the source of truth).",
154
+ artifact_user_content_missing: "Doctor cannot recreate user-authored content from the database. Re-run `/gsd discuss` for CONTEXT artifacts or `/gsd auto` for RESEARCH artifacts in that milestone.",
154
155
  uat_retry_exhausted: "Review the failing UAT criteria via `/gsd status`, fix the issue, then re-run `/gsd auto`.",
155
156
  };
156
157
  export function doctorFixHint(code) {
@@ -20,7 +20,7 @@ import { assessInterruptedSession, formatInterruptedSessionRunningMessage, forma
20
20
  import { listUnitRuntimeRecords, clearUnitRuntimeRecord, isInFlightRuntimePhase } from "./unit-runtime.js";
21
21
  import { resolveExpectedArtifactPath } from "./auto.js";
22
22
  import { gsdHome } from "./gsd-home.js";
23
- import { gsdRoot, milestonesDir, legacyMilestonesDir, resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, relSliceFile, } from "./paths.js";
23
+ import { gsdRoot, milestonesDir, legacyMilestonesDir, resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, relSliceFile, clearPathCache, } from "./paths.js";
24
24
  import { join } from "node:path";
25
25
  import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
26
26
  import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
@@ -53,6 +53,7 @@ import { isWorkflowToolSurfaceName } from "./workflow-tool-surface.js";
53
53
  import { getUnitWorkflowDispatchReadinessError } from "./tool-contract.js";
54
54
  import { runPreparation, formatCodebaseBrief, formatPriorContextBrief, } from "./preparation.js";
55
55
  import { verifyExpectedArtifact } from "./auto-recovery.js";
56
+ import { countPlanMilestoneRoadmapSlices } from "./artifact-verification.js";
56
57
  import { clearPendingGate, extractDepthVerificationMilestoneId, getPendingGate } from "./bootstrap/write-gate.js";
57
58
  import { _getPendingAutoStart, deletePendingAutoStart, hasPendingAutoStart, setPendingAutoStart, } from "./pending-auto-start.js";
58
59
  import { clearGuidedUnitContext, setGuidedUnitContext } from "./guided-unit-context.js";
@@ -78,20 +79,57 @@ export const _scheduleAutoStartAfterIdleForTest = scheduleAutoStartAfterIdle;
78
79
  // These thin wrappers accept a MilestoneScope so callers that already hold a
79
80
  // pinned scope never have to re-derive (basePath, milestoneId) separately.
80
81
  // The underlying implementations in auto-recovery.ts / auto-artifact-paths.ts /
81
- // state.ts are unchanged — only the call surface in guided-flow.ts is migrated.
82
+ // state.ts remain the default for non-scope-specific units.
82
83
  /**
83
84
  * Scope-based overload of verifyExpectedArtifact.
84
85
  * Uses scope.workspace.projectRoot as the authoritative base path, making
85
86
  * the check immune to cwd-drift and worktree-path divergence.
86
87
  */
87
88
  export function verifyExpectedArtifactForScope(scope, unitType, unitId) {
89
+ if (unitId === scope.milestoneId &&
90
+ (unitType === "discuss-milestone" || unitType === "plan-milestone")) {
91
+ // Layout-aware scope paths use resolveMilestoneFile; clear stale dir listings
92
+ // primed before discuss/plan wrote CONTEXT.md or ROADMAP.md (see checkAutoStartAfterDiscuss).
93
+ clearPathCache();
94
+ }
95
+ if (unitId === scope.milestoneId &&
96
+ unitType === "discuss-milestone") {
97
+ const path = resolveExpectedArtifactPathForScope(scope, unitType, unitId);
98
+ return path ? existsSync(path) : false;
99
+ }
100
+ if (unitId === scope.milestoneId && unitType === "plan-milestone") {
101
+ const path = resolveExpectedArtifactPathForScope(scope, unitType, unitId);
102
+ return verifyScopedPlanMilestoneArtifact(path, unitType, unitId);
103
+ }
88
104
  return verifyExpectedArtifact(unitType, unitId, scope.workspace.projectRoot);
89
105
  }
106
+ function verifyScopedPlanMilestoneArtifact(path, unitType, unitId) {
107
+ if (!path || !existsSync(path))
108
+ return false;
109
+ try {
110
+ const roadmapContent = readFileSync(path, "utf-8");
111
+ if (countPlanMilestoneRoadmapSlices(roadmapContent) === 0) {
112
+ logWarning("recovery", `verify-fail ${unitType} ${unitId}: roadmap has zero slices at ${path}`);
113
+ return false;
114
+ }
115
+ return true;
116
+ }
117
+ catch (err) {
118
+ logWarning("recovery", `plan-milestone roadmap verification failed: ${err instanceof Error ? err.message : String(err)}`);
119
+ return false;
120
+ }
121
+ }
90
122
  /**
91
123
  * Scope-based overload of resolveExpectedArtifactPath.
92
124
  * Returns the canonical absolute path (or null) using the scope's projectRoot.
93
125
  */
94
126
  export function resolveExpectedArtifactPathForScope(scope, unitType, unitId) {
127
+ if (unitId === scope.milestoneId) {
128
+ if (unitType === "discuss-milestone")
129
+ return scope.contextFile();
130
+ if (unitType === "plan-milestone")
131
+ return scope.roadmapFile();
132
+ }
95
133
  return resolveExpectedArtifactPath(unitType, unitId, scope.workspace.projectRoot);
96
134
  }
97
135
  async function runQuickTaskChoice(ctx, pi) {
@@ -19,6 +19,7 @@ export const PROVIDER_REGISTRY = [
19
19
  // must be in the canonical registry so all consumers see the same catalog.
20
20
  // See: https://github.com/open-gsd/gsd-pi/issues/4541
21
21
  { id: "claude-code", label: "Claude Code CLI", category: "llm", authMode: "externalCli" },
22
+ { id: "cursor-agent", label: "Cursor Agent", category: "llm", envVar: "CURSOR_API_KEY", authMode: "externalCli" },
22
23
  { id: "openai", label: "OpenAI", category: "llm", envVar: "OPENAI_API_KEY", prefixes: ["sk-"], dashboardUrl: "platform.openai.com/api-keys" },
23
24
  { id: "github-copilot", label: "GitHub Copilot", category: "llm", envVar: "GITHUB_TOKEN", authMode: "browserOAuth" },
24
25
  { id: "openai-codex", label: "ChatGPT Plus/Pro (Codex)", category: "llm", authMode: "browserOAuth" },
@@ -12,7 +12,7 @@ import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from "node:
12
12
  import { logWarning } from "./workflow-logger.js";
13
13
  import { isClosedStatus } from "./status-guards.js";
14
14
  import { dirname, join, relative } from "node:path";
15
- import { getAllMilestones, getMilestone, getMilestoneScopedArtifacts, getSliceScopedArtifacts, getMilestoneSlices, getSliceTasks, getTask, getSlice, insertArtifact, deleteArtifactByPath, getGateResults, isDbAvailable, } from "./gsd-db.js";
15
+ import { getAllMilestones, getMilestone, getMilestoneScopedArtifacts, getSliceScopedArtifacts, getMilestoneSlices, getSliceTasks, getTasksBySliceIds, getTask, getSlice, insertArtifact, deleteArtifactByPath, getGateResults, isDbAvailable, } from "./gsd-db.js";
16
16
  import { resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, gsdProjectionRoot, gsdRoot, buildMilestoneFileName, buildTaskFileName, } from "./paths.js";
17
17
  import { saveFile, clearParseCache, registerCacheClearCallback } from "./files.js";
18
18
  import { parseRoadmap, parsePlan } from "./parsers-legacy.js";
@@ -99,6 +99,12 @@ function meaningfulSection(value) {
99
99
  return "";
100
100
  return trimmed;
101
101
  }
102
+ function renderGateFindings(gate) {
103
+ const findings = gate.findings.trim() || `- **Verdict:** ${gate.verdict}\n- **Rationale:** ${gate.rationale}`;
104
+ if (gate.verdict !== "flag")
105
+ return findings;
106
+ return `> **Warning:** Verdict: flag - concerns recorded below\n\n${findings}`;
107
+ }
102
108
  function pushIndented(lines, value, indent = " ") {
103
109
  for (const line of value.split("\n")) {
104
110
  lines.push(`${indent}${line}`);
@@ -289,7 +295,7 @@ function renderTaskPlanMarkdown(task, taskGates = []) {
289
295
  if (gate && gate.verdict !== "omitted") {
290
296
  lines.push(`## ${label}`);
291
297
  lines.push("");
292
- lines.push(gate.findings.trim() || `- **Verdict:** ${gate.verdict}\n- **Rationale:** ${gate.rationale}`);
298
+ lines.push(renderGateFindings(gate));
293
299
  lines.push("");
294
300
  }
295
301
  }
@@ -319,14 +325,14 @@ function renderSlicePlanMarkdown(slice, tasks, gates = []) {
319
325
  if (q3 && q3.verdict !== "omitted") {
320
326
  lines.push("## Threat Surface");
321
327
  lines.push("");
322
- lines.push(q3.findings.trim() || `- **Verdict:** ${q3.verdict}\n- **Rationale:** ${q3.rationale}`);
328
+ lines.push(renderGateFindings(q3));
323
329
  lines.push("");
324
330
  }
325
331
  const q4 = gates.find(g => g.gate_id === "Q4" && g.status === "complete");
326
332
  if (q4 && q4.verdict !== "omitted") {
327
333
  lines.push("## Requirement Impact");
328
334
  lines.push("");
329
- lines.push(q4.findings.trim() || `- **Verdict:** ${q4.verdict}\n- **Rationale:** ${q4.rationale}`);
335
+ lines.push(renderGateFindings(q4));
330
336
  lines.push("");
331
337
  }
332
338
  const proofLevel = meaningfulSection(slice.proof_level);
@@ -701,6 +707,17 @@ export async function renderSliceSummary(basePath, milestoneId, sliceId) {
701
707
  export async function renderAllFromDb(basePath) {
702
708
  const result = { rendered: 0, skipped: 0, errors: [] };
703
709
  const milestones = getAllMilestones();
710
+ // Pre-fetch slices once per milestone and batch-load all tasks in one query
711
+ // family, avoiding an N+1 (previously one task query per slice).
712
+ const slicesByMilestone = new Map();
713
+ const slicePairs = [];
714
+ for (const milestone of milestones) {
715
+ const slices = getMilestoneSlices(milestone.id);
716
+ slicesByMilestone.set(milestone.id, slices);
717
+ for (const slice of slices)
718
+ slicePairs.push({ milestoneId: milestone.id, sliceId: slice.id });
719
+ }
720
+ const tasksBySlice = getTasksBySliceIds(slicePairs);
704
721
  for (const milestone of milestones) {
705
722
  // Render roadmap checkboxes
706
723
  try {
@@ -723,8 +740,8 @@ export async function renderAllFromDb(basePath) {
723
740
  catch (err) {
724
741
  result.errors.push(`milestone artifacts ${milestone.id}: ${err.message}`);
725
742
  }
726
- // Iterate slices
727
- const slices = getMilestoneSlices(milestone.id);
743
+ // Iterate slices (pre-fetched above)
744
+ const slices = slicesByMilestone.get(milestone.id) ?? [];
728
745
  for (const slice of slices) {
729
746
  // Preserve slice-scoped artifacts imported from disk, including real PLAN
730
747
  // fallback content when task rows cannot regenerate it.
@@ -760,8 +777,8 @@ export async function renderAllFromDb(basePath) {
760
777
  catch (err) {
761
778
  result.errors.push(`slice summary ${milestone.id}/${slice.id}: ${err.message}`);
762
779
  }
763
- // Iterate tasks
764
- const tasks = getSliceTasks(milestone.id, slice.id);
780
+ // Iterate tasks (batched above)
781
+ const tasks = tasksBySlice.get(`${milestone.id}\0${slice.id}`) ?? [];
765
782
  for (const task of tasks) {
766
783
  try {
767
784
  const ok = await renderTaskSummary(basePath, milestone.id, slice.id, task.id);
@@ -8,7 +8,7 @@
8
8
  import { execFileSync } from "node:child_process";
9
9
  import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, cpSync, rmSync, statSync, symlinkSync } from "node:fs";
10
10
  import { join } from "node:path";
11
- import { externalGsdRoot, isInsideWorktree } from "./repo-identity.js";
11
+ import { externalGsdRoot, externalStateAlreadyExistsForProject, isInsideWorktree } from "./repo-identity.js";
12
12
  import { getErrorMessage } from "./error-utils.js";
13
13
  import { hasGitTrackedGsdFiles } from "./gitignore.js";
14
14
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
@@ -74,6 +74,15 @@ export function migrateToExternalState(basePath) {
74
74
  return { migrated: false };
75
75
  }
76
76
  }
77
+ // If external state already contains project data, this is not a legacy
78
+ // migration source. It is likely an already-migrated project whose symlink
79
+ // was replaced by a real directory, so copying would risk data loss.
80
+ if (externalStateAlreadyExistsForProject(basePath)) {
81
+ return {
82
+ migrated: false,
83
+ error: "External state already exists for this project; leaving local .gsd directory untouched to avoid overwriting authoritative state",
84
+ };
85
+ }
77
86
  const externalPath = externalGsdRoot(basePath);
78
87
  const migratingPath = join(basePath, ".gsd.migrating");
79
88
  try {
@@ -81,13 +90,13 @@ export function migrateToExternalState(basePath) {
81
90
  mkdirSync(externalPath, { recursive: true });
82
91
  // Rename .gsd -> .gsd.migrating (atomic lock).
83
92
  // On Windows, NTFS may reject rename with EPERM if file descriptors are
84
- // open (VS Code watchers, antivirus on-access scan). Fall back to
85
- // copy+delete (#1292).
93
+ // open (VS Code watchers, antivirus on-access scan). WSL/DrvFs can report
94
+ // the same transient lock as EACCES. Fall back to copy+delete (#1292).
86
95
  try {
87
96
  renameSync(localGsd, migratingPath);
88
97
  }
89
98
  catch (renameErr) {
90
- if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY") {
99
+ if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY" || renameErr?.code === "EACCES") {
91
100
  try {
92
101
  cpSync(localGsd, migratingPath, { recursive: true, force: true });
93
102
  rmSync(localGsd, { recursive: true, force: true });
@@ -239,7 +248,7 @@ function isLocalGsdExternalStateJunction(basePath, localGsd) {
239
248
  * Recover from a failed migration (`.gsd.migrating` exists).
240
249
  * Moves `.gsd.migrating` back to `.gsd` if `.gsd` doesn't exist, or removes an
241
250
  * orphaned staging directory when `.gsd` is already a verified external state
242
- * junction with intact current state.
251
+ * junction, or an intact real directory, with intact current state.
243
252
  */
244
253
  export function recoverFailedMigration(basePath) {
245
254
  const localGsd = join(basePath, ".gsd");
@@ -247,10 +256,18 @@ export function recoverFailedMigration(basePath) {
247
256
  if (!existsSync(migratingPath))
248
257
  return false;
249
258
  if (existsSync(localGsd)) {
250
- if (!isLocalGsdExternalStateJunction(basePath, localGsd))
251
- return false;
252
259
  if (!isCurrentGsdStateIntactForMigratingCleanup(basePath))
253
260
  return false;
261
+ if (!isLocalGsdExternalStateJunction(basePath, localGsd)) {
262
+ try {
263
+ const stat = lstatSync(localGsd);
264
+ if (!stat.isDirectory())
265
+ return false;
266
+ }
267
+ catch {
268
+ return false;
269
+ }
270
+ }
254
271
  try {
255
272
  rmSync(migratingPath, { recursive: true, force: true });
256
273
  return true;
@@ -1,10 +1,10 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Process-level transaction wrapper for milestone merge closeout.
3
3
  export function runMilestoneMergeTransaction(deps, input) {
4
- return deps.mergeMilestoneToMain(input.basePath, input.milestoneId, input.roadmapContent);
4
+ return deps.mergeMilestone(input.basePath, input.milestoneId, input.roadmapContent);
5
5
  }
6
- export function createMilestoneMergeTransaction(mergeMilestoneToMain) {
6
+ export function createMilestoneMergeTransaction(mergeMilestone) {
7
7
  return function mergeMilestoneTransaction(basePath, milestoneId, roadmapContent) {
8
- return runMilestoneMergeTransaction({ mergeMilestoneToMain }, { basePath, milestoneId, roadmapContent });
8
+ return runMilestoneMergeTransaction({ mergeMilestone }, { basePath, milestoneId, roadmapContent });
9
9
  };
10
10
  }
@@ -1,4 +1,5 @@
1
- import { loadGlobalGSDPreferences, loadProjectGSDPreferences, } from "./preferences.js";
1
+ import { loadEffectiveGSDPreferences, loadGlobalGSDPreferences, loadProjectGSDPreferences, } from "./preferences.js";
2
+ import { crossAxisPreferenceWarnings } from "./preferences-validation.js";
2
3
  const notifiedPreferenceDiagnostics = new Set();
3
4
  export function collectPreferenceDiagnostics(basePath) {
4
5
  const diagnostics = [
@@ -14,6 +15,28 @@ export function collectPreferenceDiagnostics(basePath) {
14
15
  seen.add(signature);
15
16
  unique.push(diagnostic);
16
17
  }
18
+ const effective = loadEffectiveGSDPreferences(basePath);
19
+ if (effective) {
20
+ const existingMessages = new Set(unique.map((diagnostic) => diagnostic.message));
21
+ for (const message of crossAxisPreferenceWarnings(effective.preferences)) {
22
+ if (existingMessages.has(message))
23
+ continue;
24
+ const diagnostic = {
25
+ path: effective.path,
26
+ scope: effective.scope,
27
+ severity: "warning",
28
+ kind: "validation",
29
+ message,
30
+ sanitized: true,
31
+ };
32
+ const signature = preferenceDiagnosticSignature(diagnostic);
33
+ if (seen.has(signature))
34
+ continue;
35
+ seen.add(signature);
36
+ existingMessages.add(message);
37
+ unique.push(diagnostic);
38
+ }
39
+ }
17
40
  return unique;
18
41
  }
19
42
  export function formatPreferenceDiagnostic(diagnostic) {
@@ -74,10 +74,12 @@ function resolveWinningPhase(models, chain) {
74
74
  * - Legacy: `planning: claude-opus-4-6`
75
75
  * - Extended: `planning: { model: claude-opus-4-6, fallbacks: [glm-5, minimax-m2.5] }`
76
76
  */
77
- export function resolveModelWithFallbacksForUnit(unitType, basePath, availableModelIds, preferredModelId) {
78
- const loadOpts = availableModelIds !== undefined || preferredModelId !== undefined
79
- ? { availableModelIds, preferredModelId }
80
- : undefined;
77
+ export function resolveModelWithFallbacksForUnit(unitType, basePath, availableModelIds, preferredModelId, skipProfileDefaults = false) {
78
+ const loadOpts = {
79
+ ...(availableModelIds !== undefined ? { availableModelIds } : {}),
80
+ ...(preferredModelId !== undefined ? { preferredModelId } : {}),
81
+ ...(skipProfileDefaults ? { skipProfileDefaults: true } : {}),
82
+ };
81
83
  const prefs = loadEffectiveGSDPreferences(basePath, loadOpts);
82
84
  const chain = phaseChainForUnit(unitType);
83
85
  if (!chain)