@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
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, lstatSync, readdirSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, mkdirSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { execFileSync } from "node:child_process";
3
4
 
4
5
  import { loadFile, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
5
6
  import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
@@ -16,6 +17,8 @@ import { isClosedStatus } from "./status-guards.js";
16
17
  import type { DoctorIssue, DoctorIssueCode, DoctorReport } from "./doctor-types.js";
17
18
  import { GLOBAL_STATE_CODES } from "./doctor-types.js";
18
19
  import type { RoadmapSliceEntry } from "./types.js";
20
+ import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
21
+ import { createRepositoryRegistryFromPreferences } from "./repository-registry.js";
19
22
  import { checkGitHealth, checkRuntimeHealth, checkGlobalHealth, checkEngineHealth } from "./doctor-checks.js";
20
23
  import { checkEnvironmentHealth } from "./doctor-environment.js";
21
24
  import { runProviderChecks } from "./doctor-providers.js";
@@ -308,6 +311,94 @@ export async function readDoctorHistory(basePath: string, lastN = 50): Promise<D
308
311
  } catch { return []; }
309
312
  }
310
313
 
314
+ /**
315
+ * Resolve the git working-tree root for a path, or null if it is not a repo.
316
+ * Used by the workspace-repository probe to check that a declared child path is
317
+ * itself a git repository (not merely nested inside the parent's repo).
318
+ */
319
+ function resolveGitToplevel(cwd: string): string | null {
320
+ try {
321
+ const out = execFileSync("git", ["rev-parse", "--show-toplevel"], {
322
+ cwd,
323
+ stdio: ["ignore", "pipe", "pipe"],
324
+ encoding: "utf-8",
325
+ env: GIT_NO_PROMPT_ENV,
326
+ }).trim();
327
+ return out ? resolve(out) : null;
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+
333
+ /** realpath that falls back to the resolved path if the link cannot be read. */
334
+ function realpathSafe(p: string): string {
335
+ try {
336
+ return realpathSync(p);
337
+ } catch {
338
+ return resolve(p);
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Parent-workspace probe (#818): validate that every declared child repository
344
+ * exists on disk and is a git repository at its own root. The registry only
345
+ * checks paths stay inside the project root; it never checks existence or
346
+ * git-ness, so a typo'd path (e.g. `frontned`) would otherwise build cleanly
347
+ * and silently produce "no problems found." No-op for single-repo projects.
348
+ */
349
+ function checkWorkspaceRepositoryHealth(
350
+ basePath: string,
351
+ prefs: GSDPreferences | undefined,
352
+ issues: DoctorIssue[],
353
+ ): void {
354
+ if (prefs?.workspace?.mode !== "parent") return;
355
+ let registry;
356
+ try {
357
+ registry = createRepositoryRegistryFromPreferences(basePath, prefs);
358
+ } catch (err) {
359
+ issues.push({
360
+ severity: "error",
361
+ code: "invalid_preferences",
362
+ scope: "project",
363
+ unitId: "workspace",
364
+ message: `workspace registry failed to build: ${err instanceof Error ? err.message : String(err)}`,
365
+ fixable: false,
366
+ });
367
+ return;
368
+ }
369
+ for (const repo of registry.repositories) {
370
+ if (repo.id === "project") continue;
371
+ if (!existsSync(repo.root)) {
372
+ issues.push({
373
+ severity: "error",
374
+ code: "workspace_repo_path_missing",
375
+ scope: "project",
376
+ unitId: `workspace.repositories.${repo.id}`,
377
+ message: `declared repository "${repo.id}" path does not exist on disk: ${repo.root}`,
378
+ fixable: false,
379
+ });
380
+ continue;
381
+ }
382
+ // Must be a repo at its OWN root — a plain dir nested in the parent repo
383
+ // would otherwise pass a bare `nativeIsRepo` check via the enclosing
384
+ // parent's .git. Compare on realpath so symlinked temp roots (macOS
385
+ // /var vs /private/var) don't cause a false mismatch.
386
+ const toplevel = resolveGitToplevel(repo.root);
387
+ const declaredReal = realpathSafe(repo.root);
388
+ const toplevelReal = toplevel ? realpathSafe(toplevel) : null;
389
+ if (toplevelReal !== declaredReal) {
390
+ issues.push({
391
+ severity: "warning",
392
+ code: "workspace_repo_not_a_repo",
393
+ scope: "project",
394
+ unitId: `workspace.repositories.${repo.id}`,
395
+ message: `declared repository "${repo.id}" path is not a git repository at its own root: ${repo.root}`,
396
+ fixable: false,
397
+ });
398
+ }
399
+ }
400
+ }
401
+
311
402
  export async function runGSDDoctor(basePath: string, options?: { fix?: boolean; dryRun?: boolean; scope?: string; fixLevel?: "task" | "all"; isolationMode?: "none" | "worktree" | "branch"; includeBuild?: boolean; includeTests?: boolean }): Promise<DoctorReport> {
312
403
  const issues: DoctorIssue[] = [];
313
404
  const fixesApplied: string[] = [];
@@ -366,6 +457,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
366
457
  (prefs?.preferences?.git?.isolation === "worktree" ? "worktree" :
367
458
  prefs?.preferences?.git?.isolation === "branch" ? "branch" : "none");
368
459
  await checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode, dryRun);
460
+ checkWorkspaceRepositoryHealth(basePath, prefs?.preferences, issues);
369
461
  const gitMs = Date.now() - t0git;
370
462
 
371
463
  // Runtime health checks — timed
@@ -386,7 +478,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
386
478
  const envMs = Date.now() - t0env;
387
479
 
388
480
  // Engine health checks — DB constraints and projection drift
389
- await checkEngineHealth(basePath, issues, fixesApplied);
481
+ await checkEngineHealth(basePath, issues, fixesApplied, { repair: fix && !dryRun });
390
482
 
391
483
  const milestonesPath = milestonesDir(basePath);
392
484
  const legacyMilestonesPath2 = legacyMilestonesDir(basePath);
@@ -13,6 +13,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
13
13
  import { randomUUID } from "node:crypto";
14
14
  import { resolve } from "node:path";
15
15
  import { getShellConfig, killProcessTree, SIGKILL_GRACE_MS, HARD_DEADLINE_MS } from "@gsd/pi-coding-agent";
16
+ import { redactSecrets } from "./redact-secrets.js";
16
17
 
17
18
  export interface ExecSandboxRequest {
18
19
  /** Interpreter to use. */
@@ -315,8 +316,11 @@ export function runExecSandbox(
315
316
  const stderrBuf = Buffer.concat(stderrChunks);
316
317
  const stdoutSuffix = stdoutTruncated ? "\n[truncated: stdout cap reached]\n" : "";
317
318
  const stderrSuffix = stderrTruncated ? "\n[truncated: stderr cap reached]\n" : "";
318
- writeFileSync(stdoutPath, Buffer.concat([stdoutBuf, Buffer.from(stdoutSuffix, "utf-8")]));
319
- writeFileSync(stderrPath, Buffer.concat([stderrBuf, Buffer.from(stderrSuffix, "utf-8")]));
319
+ // Redact secret-shaped substrings before persisting to .gsd/exec/ (skipped by
320
+ // the secret scanner). The returned digest is derived from the raw buffer
321
+ // below and is redacted downstream when the session is written to activity/.
322
+ writeFileSync(stdoutPath, redactSecrets(Buffer.concat([stdoutBuf, Buffer.from(stdoutSuffix, "utf-8")]).toString("utf-8")));
323
+ writeFileSync(stderrPath, redactSecrets(Buffer.concat([stderrBuf, Buffer.from(stderrSuffix, "utf-8")]).toString("utf-8")));
320
324
 
321
325
  const digestBody = tail(stdoutBuf, opts.digest_chars);
322
326
  const digest =
@@ -11,18 +11,84 @@ import { migrateFromMarkdown } from "./md-importer.js";
11
11
  import { countDbHierarchy } from "./migration-auto-check.js";
12
12
  import { logWarning } from "./workflow-logger.js";
13
13
  import { LAYOUT_SEGMENTS } from "./layout-policy.js";
14
- import { canonicalPhaseDirName, milestonesDir, resolveMilestonePath } from "./paths.js";
14
+ import {
15
+ canonicalPhaseDirName,
16
+ dirIsContentBearingLegacyMilestone,
17
+ milestonesDir,
18
+ resolveMilestonePath,
19
+ } from "./paths.js";
15
20
 
16
21
  const LEGACY_MIGRATING_SEGMENT = "milestones.migrating";
22
+ const RETRYABLE_FS_ERROR_CODES = new Set(["EPERM", "EBUSY", "ENOTEMPTY"]);
23
+ const RM_RETRY_OPTIONS = { recursive: true, force: true, maxRetries: 5, retryDelay: 100 } as const;
24
+
25
+ type FlatPhaseMigrationFsOps = {
26
+ cpSync: typeof cpSync;
27
+ renameSync: typeof renameSync;
28
+ rmSync: typeof rmSync;
29
+ };
30
+
31
+ let fsOps: FlatPhaseMigrationFsOps = { cpSync, renameSync, rmSync };
32
+
33
+ export function _setFlatPhaseMigrationFsOpsForTest(
34
+ overrides: Partial<FlatPhaseMigrationFsOps>,
35
+ ): () => void {
36
+ const previous = fsOps;
37
+ fsOps = { ...fsOps, ...overrides };
38
+ return () => {
39
+ fsOps = previous;
40
+ };
41
+ }
17
42
 
18
43
  function legacyMigratingPath(basePath: string): string {
19
44
  return join(basePath, ".gsd", LEGACY_MIGRATING_SEGMENT);
20
45
  }
21
46
 
47
+ function removePathWithRetries(path: string): void {
48
+ fsOps.rmSync(path, { recursive: true, force: true, maxRetries: RM_RETRY_OPTIONS.maxRetries, retryDelay: RM_RETRY_OPTIONS.retryDelay });
49
+ }
50
+
51
+ function isRetryableFsError(err: unknown): boolean {
52
+ const code = (err as { code?: unknown } | null)?.code;
53
+ return typeof code === "string" && RETRYABLE_FS_ERROR_CODES.has(code);
54
+ }
55
+
56
+ function movePathWithCopyDeleteFallback(src: string, dst: string): void {
57
+ try {
58
+ fsOps.renameSync(src, dst);
59
+ return;
60
+ } catch (err) {
61
+ if (!isRetryableFsError(err)) throw err;
62
+ }
63
+
64
+ try {
65
+ fsOps.cpSync(src, dst, { recursive: true, force: true });
66
+ removePathWithRetries(src);
67
+ } catch (fallbackErr) {
68
+ if (existsSync(src) && existsSync(dst)) {
69
+ try {
70
+ removePathWithRetries(dst);
71
+ } catch {
72
+ // Leave the original error intact; the next run can pre-clean dst.
73
+ }
74
+ }
75
+ throw fallbackErr;
76
+ }
77
+ }
78
+
79
+ function expectedPhaseDirs(basePath: string): string[] {
80
+ return getAllMilestones().map((milestone) =>
81
+ resolveMilestonePath(basePath, milestone.id) ??
82
+ join(milestonesDir(basePath), canonicalPhaseDirName(milestone.id, milestone.title)),
83
+ );
84
+ }
85
+
22
86
  function hasLegacyMilestoneSubdirs(dirPath: string): boolean {
23
87
  if (!existsSync(dirPath)) return false;
24
88
  try {
25
- return readdirSync(dirPath).some(e => statSync(join(dirPath, e)).isDirectory());
89
+ return readdirSync(dirPath).some(
90
+ (e) => statSync(join(dirPath, e)).isDirectory() && dirIsContentBearingLegacyMilestone(join(dirPath, e)),
91
+ );
26
92
  } catch {
27
93
  return false;
28
94
  }
@@ -34,19 +100,26 @@ function rollbackPartialMigration(
34
100
  migratingPath?: string,
35
101
  ): void {
36
102
  // Remove the partially-written phases/ dir.
37
- rmSync(join(basePath, ".gsd", LAYOUT_SEGMENTS.level1), { recursive: true, force: true });
103
+ try {
104
+ removePathWithRetries(join(basePath, ".gsd", LAYOUT_SEGMENTS.level1));
105
+ } catch (removeErr) {
106
+ logWarning(
107
+ "migration",
108
+ `rollback: could not remove partial ${LAYOUT_SEGMENTS.level1}/: ${(removeErr as Error).message}`,
109
+ );
110
+ }
38
111
  // Restore milestones/ — prefer renaming the preserved migrating copy back.
39
112
  const milestonesPath = join(basePath, ".gsd", "milestones");
40
113
  try {
41
114
  if (migratingPath && existsSync(migratingPath) && !existsSync(milestonesPath)) {
42
- renameSync(migratingPath, milestonesPath);
115
+ movePathWithCopyDeleteFallback(migratingPath, milestonesPath);
43
116
  return;
44
117
  }
45
118
  if (existsSync(backupDir)) {
46
119
  cpSync(backupDir, milestonesPath, { recursive: true });
47
120
  }
48
121
  if (migratingPath && existsSync(migratingPath)) {
49
- rmSync(migratingPath, { recursive: true, force: true });
122
+ removePathWithRetries(migratingPath);
50
123
  }
51
124
  } catch (restoreErr) {
52
125
  logWarning(
@@ -162,18 +235,29 @@ export async function migrateToFlatPhase(basePath: string): Promise<void> {
162
235
  throw err;
163
236
  }
164
237
 
238
+ if (existsSync(migratingPath)) {
239
+ removePathWithRetries(migratingPath);
240
+ }
241
+
165
242
  // 3. Rename legacy tree aside before rendering so path resolvers target
166
243
  // phases/ instead of writing back into the nested milestones/ layout.
167
244
  // Keep the full tree on disk until render+verify succeed (unlike rmSync).
168
245
  try {
169
- renameSync(milestonesPath, migratingPath);
246
+ movePathWithCopyDeleteFallback(milestonesPath, migratingPath);
170
247
  } catch (err) {
171
248
  logWarning("migration", `failed to rename legacy milestones/ before render: ${(err as Error).message}`);
172
249
  throw err;
173
250
  }
174
- } else {
175
- // Interrupted run: clear any partial phases/ projection before retrying.
176
- rmSync(phasesPath, { recursive: true, force: true });
251
+ }
252
+
253
+ // Clear any stale or partially-rendered flat projection before this run
254
+ // writes. Verification below checks the current DB render, not leftovers.
255
+ try {
256
+ removePathWithRetries(phasesPath);
257
+ } catch (err) {
258
+ logWarning("migration", `failed to clear stale phases/ before render: ${(err as Error).message}`);
259
+ rollbackPartialMigration(basePath, backupDir, migratingPath);
260
+ throw err;
177
261
  }
178
262
 
179
263
  // 4. Render flat-phase from DB
@@ -211,22 +295,14 @@ export async function migrateToFlatPhase(basePath: string): Promise<void> {
211
295
  }
212
296
 
213
297
  const db = countDbHierarchy();
214
- let renderedDirCount = 0;
215
- try {
216
- // /^\d+-/ matches any numeric prefix (01-, 10-, 100-) so M100+ milestones
217
- // whose dirs start with "100-slug" are counted correctly.
218
- renderedDirCount = readdirSync(phasesPath, { withFileTypes: true })
219
- .filter((entry) => entry.isDirectory() && /^\d+-/.test(entry.name)).length;
220
- } catch {
221
- // phases/ doesn't exist or is unreadable — same as zero dirs
222
- }
223
- if (db.milestones > 0 && renderedDirCount !== db.milestones) {
298
+ const missingPhaseDirs = expectedPhaseDirs(basePath).filter((phaseDir) => !existsSync(phaseDir));
299
+ if (db.milestones > 0 && missingPhaseDirs.length > 0) {
224
300
  logWarning(
225
301
  "migration",
226
- `phases/ dir count mismatch: expected ${db.milestones}, found ${renderedDirCount}`,
302
+ `flat-phase migration missing ${missingPhaseDirs.length} expected phase dir(s): ${missingPhaseDirs.join(", ")}`,
227
303
  );
228
304
  rollbackPartialMigration(basePath, backupDir, migratingPath);
229
- throw new Error("flat-phase migration verification failed: phases dir milestone count mismatch");
305
+ throw new Error("flat-phase migration verification failed: missing rendered phase directories");
230
306
  }
231
307
  if (db.slices > 0 && renderResult.rendered === 0) {
232
308
  logWarning(
@@ -239,7 +315,7 @@ export async function migrateToFlatPhase(basePath: string): Promise<void> {
239
315
 
240
316
  // 6. Verified — remove the renamed legacy tree (backup already on disk).
241
317
  try {
242
- rmSync(migratingPath, { recursive: true, force: true });
318
+ removePathWithRetries(migratingPath);
243
319
  } catch (err) {
244
320
  logWarning(
245
321
  "migration",
@@ -18,7 +18,7 @@ import { gsdRoot } from "./paths.js";
18
18
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
19
19
  import { loadEffectiveGSDPreferences } from "./preferences.js";
20
20
  import { logWarning } from "./workflow-logger.js";
21
- import { createRepositoryRegistryFromPreferences } from "./repository-registry.js";
21
+ import { createRepositoryRegistryFromPreferences, defaultRepositoryTargets } from "./repository-registry.js";
22
22
  import { isGsdGitignored } from "./gitignore.js";
23
23
 
24
24
 
@@ -1266,26 +1266,25 @@ export function handleTurnGitActionError(action: TurnGitActionMode, err: unknown
1266
1266
  };
1267
1267
  }
1268
1268
 
1269
+ export function isRepositoryDirty(repoRoot: string): boolean {
1270
+ try {
1271
+ return runGit(repoRoot, ["status", "--porcelain"]).length > 0;
1272
+ } catch {
1273
+ // Fallback preserves legacy behavior if explicit status probing fails.
1274
+ return nativeHasChanges(repoRoot);
1275
+ }
1276
+ }
1277
+
1269
1278
  function collectRepositoryDirtyStatus(basePath: string): Record<string, boolean> {
1270
1279
  const preferences = loadEffectiveGSDPreferences(basePath)?.preferences;
1271
1280
  const registry = createRepositoryRegistryFromPreferences(basePath, preferences);
1272
1281
  const dirtyByRepository: Record<string, boolean> = {};
1273
1282
  const registeredRoots = new Set(registry.repositories.map((repo) => resolve(repo.root)));
1274
1283
  for (const repo of registry.repositories) {
1275
- try {
1276
- dirtyByRepository[repo.id] = runGit(repo.root, ["status", "--porcelain"]).length > 0;
1277
- } catch {
1278
- // Fallback preserves legacy behavior if explicit status probing fails.
1279
- dirtyByRepository[repo.id] = nativeHasChanges(repo.root);
1280
- }
1284
+ dirtyByRepository[repo.id] = isRepositoryDirty(repo.root);
1281
1285
  }
1282
1286
  for (const repoRoot of findUndeclaredNestedGitRepositories(registry.projectRoot, registeredRoots)) {
1283
- let dirty = false;
1284
- try {
1285
- dirty = runGit(repoRoot, ["status", "--porcelain"]).length > 0;
1286
- } catch {
1287
- dirty = nativeHasChanges(repoRoot);
1288
- }
1287
+ const dirty = isRepositoryDirty(repoRoot);
1289
1288
  if (!dirty) continue;
1290
1289
 
1291
1290
  const relPath = relative(registry.projectRoot, repoRoot).split(sep).join("/") || repoRoot;
@@ -1357,7 +1356,7 @@ function runPerRepositoryCommitAction(args: {
1357
1356
  } {
1358
1357
  const preferences = loadEffectiveGSDPreferences(args.basePath)?.preferences;
1359
1358
  const registry = createRepositoryRegistryFromPreferences(args.basePath, preferences);
1360
- const repoIds = args.targetRepositories?.length ? args.targetRepositories : ["project"];
1359
+ const repoIds = args.targetRepositories?.length ? args.targetRepositories : defaultRepositoryTargets(registry);
1361
1360
  const gitPrefs = preferences?.git ?? {};
1362
1361
  const commitMessages: Record<string, string> = {};
1363
1362
  const commitErrors: Record<string, string> = {};
@@ -1445,9 +1444,7 @@ export function runTurnGitAction(args: {
1445
1444
  };
1446
1445
  }
1447
1446
 
1448
- const primaryMessage =
1449
- repoCommitResult.commitMessages[args.targetRepositories?.[0] ?? "project"]
1450
- ?? Object.values(repoCommitResult.commitMessages)[0];
1447
+ const primaryMessage = Object.values(repoCommitResult.commitMessages)[0];
1451
1448
  return {
1452
1449
  action: args.action,
1453
1450
  status: "ok",
@@ -535,6 +535,78 @@ export function updateTaskStatus(milestoneId: string, sliceId: string, taskId: s
535
535
  applyStatusTransition({ entity: "task", milestoneId, sliceId, taskId, status, completedAt });
536
536
  }
537
537
 
538
+ export function repairTaskCompletionFromSummary(t: {
539
+ milestoneId: string;
540
+ sliceId: string;
541
+ taskId: string;
542
+ title?: string;
543
+ oneLiner?: string;
544
+ narrative?: string;
545
+ verificationResult: string;
546
+ duration?: string;
547
+ completedAt: string;
548
+ blockerDiscovered?: boolean;
549
+ deviations?: string;
550
+ knownIssues?: string;
551
+ keyFiles?: string[];
552
+ keyDecisions?: string[];
553
+ fullSummaryMd: string;
554
+ }): void {
555
+ const db = getDbOrNull();
556
+ if (!db) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
557
+ transaction(() => {
558
+ const seq = db.prepare(
559
+ `SELECT COALESCE(MAX(sequence), 0) + 1 AS next_sequence
560
+ FROM tasks
561
+ WHERE milestone_id = :mid AND slice_id = :sid`,
562
+ ).get({ ":mid": t.milestoneId, ":sid": t.sliceId }) as { next_sequence?: number } | undefined;
563
+ db.prepare(
564
+ `INSERT INTO tasks (
565
+ milestone_id, slice_id, id, title, status, one_liner, narrative,
566
+ verification_result, duration, completed_at, blocker_discovered,
567
+ deviations, known_issues, key_files, key_decisions, full_summary_md,
568
+ sequence
569
+ ) VALUES (
570
+ :milestone_id, :slice_id, :id, :title, 'complete', :one_liner, :narrative,
571
+ :verification_result, :duration, :completed_at, :blocker_discovered,
572
+ :deviations, :known_issues, :key_files, :key_decisions, :full_summary_md,
573
+ :sequence
574
+ )
575
+ ON CONFLICT(milestone_id, slice_id, id) DO UPDATE SET
576
+ title = CASE WHEN NULLIF(:title, '') IS NOT NULL THEN :title ELSE tasks.title END,
577
+ status = 'complete',
578
+ one_liner = CASE WHEN NULLIF(:one_liner, '') IS NOT NULL THEN :one_liner ELSE tasks.one_liner END,
579
+ narrative = CASE WHEN NULLIF(:narrative, '') IS NOT NULL THEN :narrative ELSE tasks.narrative END,
580
+ verification_result = :verification_result,
581
+ duration = CASE WHEN NULLIF(:duration, '') IS NOT NULL THEN :duration ELSE tasks.duration END,
582
+ completed_at = :completed_at,
583
+ blocker_discovered = :blocker_discovered,
584
+ deviations = CASE WHEN NULLIF(:deviations, '') IS NOT NULL THEN :deviations ELSE tasks.deviations END,
585
+ known_issues = CASE WHEN NULLIF(:known_issues, '') IS NOT NULL THEN :known_issues ELSE tasks.known_issues END,
586
+ key_files = :key_files,
587
+ key_decisions = :key_decisions,
588
+ full_summary_md = :full_summary_md`,
589
+ ).run({
590
+ ":milestone_id": t.milestoneId,
591
+ ":slice_id": t.sliceId,
592
+ ":id": t.taskId,
593
+ ":title": t.title ?? "",
594
+ ":one_liner": t.oneLiner ?? "",
595
+ ":narrative": t.narrative ?? "",
596
+ ":verification_result": t.verificationResult,
597
+ ":duration": t.duration ?? "",
598
+ ":completed_at": t.completedAt,
599
+ ":blocker_discovered": t.blockerDiscovered ? 1 : 0,
600
+ ":deviations": t.deviations ?? "",
601
+ ":known_issues": t.knownIssues ?? "",
602
+ ":key_files": JSON.stringify(t.keyFiles ?? []),
603
+ ":key_decisions": JSON.stringify(t.keyDecisions ?? []),
604
+ ":full_summary_md": t.fullSummaryMd,
605
+ ":sequence": seq?.next_sequence ?? 1,
606
+ });
607
+ });
608
+ }
609
+
538
610
  export function setTaskBlockerDiscovered(milestoneId: string, sliceId: string, taskId: string, discovered: boolean): void {
539
611
  if (!getDbOrNull()!) return;
540
612
  getDbOrNull()!.prepare(
@@ -209,6 +209,8 @@ const DOCTOR_FIX_HINTS: Partial<Record<DoctorIssueCode, string>> = {
209
209
  state_file_stale: "Run `/gsd doctor fix` to rebuild the projection from the database.",
210
210
  state_file_missing: "Run `/gsd doctor fix` to rebuild the projection from the database.",
211
211
  projection_drift: "Run `/gsd doctor fix` to rebuild markdown projections from the database (DB is the source of truth).",
212
+ artifact_user_content_missing:
213
+ "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.",
212
214
  uat_retry_exhausted: "Review the failing UAT criteria via `/gsd status`, fix the issue, then re-run `/gsd auto`.",
213
215
  };
214
216
 
@@ -42,7 +42,7 @@ import { gsdHome } from "./gsd-home.js";
42
42
  import {
43
43
  gsdRoot, milestonesDir, legacyMilestonesDir, resolveMilestoneFile,
44
44
  resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile,
45
- relMilestoneFile, relSliceFile,
45
+ relMilestoneFile, relSliceFile, clearPathCache,
46
46
  } from "./paths.js";
47
47
  import { join } from "node:path";
48
48
  import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
@@ -101,6 +101,7 @@ import {
101
101
  formatPriorContextBrief,
102
102
  } from "./preparation.js";
103
103
  import { verifyExpectedArtifact } from "./auto-recovery.js";
104
+ import { countPlanMilestoneRoadmapSlices } from "./artifact-verification.js";
104
105
  import type { MilestoneScope } from "./workspace.js";
105
106
  import { clearPendingGate, extractDepthVerificationMilestoneId, getPendingGate } from "./bootstrap/write-gate.js";
106
107
  import {
@@ -164,7 +165,7 @@ export const _scheduleAutoStartAfterIdleForTest = scheduleAutoStartAfterIdle;
164
165
  // These thin wrappers accept a MilestoneScope so callers that already hold a
165
166
  // pinned scope never have to re-derive (basePath, milestoneId) separately.
166
167
  // The underlying implementations in auto-recovery.ts / auto-artifact-paths.ts /
167
- // state.ts are unchanged — only the call surface in guided-flow.ts is migrated.
168
+ // state.ts remain the default for non-scope-specific units.
168
169
 
169
170
  /**
170
171
  * Scope-based overload of verifyExpectedArtifact.
@@ -176,9 +177,47 @@ export function verifyExpectedArtifactForScope(
176
177
  unitType: string,
177
178
  unitId: string,
178
179
  ): boolean {
180
+ if (
181
+ unitId === scope.milestoneId &&
182
+ (unitType === "discuss-milestone" || unitType === "plan-milestone")
183
+ ) {
184
+ // Layout-aware scope paths use resolveMilestoneFile; clear stale dir listings
185
+ // primed before discuss/plan wrote CONTEXT.md or ROADMAP.md (see checkAutoStartAfterDiscuss).
186
+ clearPathCache();
187
+ }
188
+ if (
189
+ unitId === scope.milestoneId &&
190
+ unitType === "discuss-milestone"
191
+ ) {
192
+ const path = resolveExpectedArtifactPathForScope(scope, unitType, unitId);
193
+ return path ? existsSync(path) : false;
194
+ }
195
+ if (unitId === scope.milestoneId && unitType === "plan-milestone") {
196
+ const path = resolveExpectedArtifactPathForScope(scope, unitType, unitId);
197
+ return verifyScopedPlanMilestoneArtifact(path, unitType, unitId);
198
+ }
179
199
  return verifyExpectedArtifact(unitType, unitId, scope.workspace.projectRoot);
180
200
  }
181
201
 
202
+ function verifyScopedPlanMilestoneArtifact(
203
+ path: string | null,
204
+ unitType: string,
205
+ unitId: string,
206
+ ): boolean {
207
+ if (!path || !existsSync(path)) return false;
208
+ try {
209
+ const roadmapContent = readFileSync(path, "utf-8");
210
+ if (countPlanMilestoneRoadmapSlices(roadmapContent) === 0) {
211
+ logWarning("recovery", `verify-fail ${unitType} ${unitId}: roadmap has zero slices at ${path}`);
212
+ return false;
213
+ }
214
+ return true;
215
+ } catch (err) {
216
+ logWarning("recovery", `plan-milestone roadmap verification failed: ${err instanceof Error ? err.message : String(err)}`);
217
+ return false;
218
+ }
219
+ }
220
+
182
221
  /**
183
222
  * Scope-based overload of resolveExpectedArtifactPath.
184
223
  * Returns the canonical absolute path (or null) using the scope's projectRoot.
@@ -188,6 +227,10 @@ export function resolveExpectedArtifactPathForScope(
188
227
  unitType: string,
189
228
  unitId: string,
190
229
  ): string | null {
230
+ if (unitId === scope.milestoneId) {
231
+ if (unitType === "discuss-milestone") return scope.contextFile();
232
+ if (unitType === "plan-milestone") return scope.roadmapFile();
233
+ }
191
234
  return resolveExpectedArtifactPath(unitType, unitId, scope.workspace.projectRoot);
192
235
  }
193
236
 
@@ -43,6 +43,7 @@ export const PROVIDER_REGISTRY: ProviderInfo[] = [
43
43
  // must be in the canonical registry so all consumers see the same catalog.
44
44
  // See: https://github.com/open-gsd/gsd-pi/issues/4541
45
45
  { id: "claude-code", label: "Claude Code CLI", category: "llm", authMode: "externalCli" },
46
+ { id: "cursor-agent", label: "Cursor Agent", category: "llm", envVar: "CURSOR_API_KEY", authMode: "externalCli" },
46
47
  { id: "openai", label: "OpenAI", category: "llm", envVar: "OPENAI_API_KEY", prefixes: ["sk-"], dashboardUrl: "platform.openai.com/api-keys" },
47
48
  { id: "github-copilot", label: "GitHub Copilot", category: "llm", envVar: "GITHUB_TOKEN", authMode: "browserOAuth" },
48
49
  { id: "openai-codex", label: "ChatGPT Plus/Pro (Codex)",category: "llm", authMode: "browserOAuth" },
@@ -20,6 +20,7 @@ import {
20
20
  getSliceScopedArtifacts,
21
21
  getMilestoneSlices,
22
22
  getSliceTasks,
23
+ getTasksBySliceIds,
23
24
  getTask,
24
25
  getSlice,
25
26
  insertArtifact,
@@ -136,6 +137,12 @@ function meaningfulSection(value: string | null | undefined): string {
136
137
  return trimmed;
137
138
  }
138
139
 
140
+ function renderGateFindings(gate: GateRow): string {
141
+ const findings = gate.findings.trim() || `- **Verdict:** ${gate.verdict}\n- **Rationale:** ${gate.rationale}`;
142
+ if (gate.verdict !== "flag") return findings;
143
+ return `> **Warning:** Verdict: flag - concerns recorded below\n\n${findings}`;
144
+ }
145
+
139
146
  function pushIndented(lines: string[], value: string, indent = " "): void {
140
147
  for (const line of value.split("\n")) {
141
148
  lines.push(`${indent}${line}`);
@@ -356,7 +363,7 @@ function renderTaskPlanMarkdown(task: TaskRow, taskGates: GateRow[] = []): strin
356
363
  if (gate && gate.verdict !== "omitted") {
357
364
  lines.push(`## ${label}`);
358
365
  lines.push("");
359
- lines.push(gate.findings.trim() || `- **Verdict:** ${gate.verdict}\n- **Rationale:** ${gate.rationale}`);
366
+ lines.push(renderGateFindings(gate));
360
367
  lines.push("");
361
368
  }
362
369
  }
@@ -390,7 +397,7 @@ function renderSlicePlanMarkdown(slice: SliceRow, tasks: TaskRow[], gates: GateR
390
397
  if (q3 && q3.verdict !== "omitted") {
391
398
  lines.push("## Threat Surface");
392
399
  lines.push("");
393
- lines.push(q3.findings.trim() || `- **Verdict:** ${q3.verdict}\n- **Rationale:** ${q3.rationale}`);
400
+ lines.push(renderGateFindings(q3));
394
401
  lines.push("");
395
402
  }
396
403
 
@@ -398,7 +405,7 @@ function renderSlicePlanMarkdown(slice: SliceRow, tasks: TaskRow[], gates: GateR
398
405
  if (q4 && q4.verdict !== "omitted") {
399
406
  lines.push("## Requirement Impact");
400
407
  lines.push("");
401
- lines.push(q4.findings.trim() || `- **Verdict:** ${q4.verdict}\n- **Rationale:** ${q4.rationale}`);
408
+ lines.push(renderGateFindings(q4));
402
409
  lines.push("");
403
410
  }
404
411
 
@@ -876,6 +883,17 @@ export async function renderAllFromDb(basePath: string): Promise<RenderAllResult
876
883
  const result: RenderAllResult = { rendered: 0, skipped: 0, errors: [] };
877
884
  const milestones = getAllMilestones();
878
885
 
886
+ // Pre-fetch slices once per milestone and batch-load all tasks in one query
887
+ // family, avoiding an N+1 (previously one task query per slice).
888
+ const slicesByMilestone = new Map<string, SliceRow[]>();
889
+ const slicePairs: Array<{ milestoneId: string; sliceId: string }> = [];
890
+ for (const milestone of milestones) {
891
+ const slices = getMilestoneSlices(milestone.id);
892
+ slicesByMilestone.set(milestone.id, slices);
893
+ for (const slice of slices) slicePairs.push({ milestoneId: milestone.id, sliceId: slice.id });
894
+ }
895
+ const tasksBySlice = getTasksBySliceIds(slicePairs);
896
+
879
897
  for (const milestone of milestones) {
880
898
  // Render roadmap checkboxes
881
899
  try {
@@ -894,8 +912,8 @@ export async function renderAllFromDb(basePath: string): Promise<RenderAllResult
894
912
  result.errors.push(`milestone artifacts ${milestone.id}: ${(err as Error).message}`);
895
913
  }
896
914
 
897
- // Iterate slices
898
- const slices = getMilestoneSlices(milestone.id);
915
+ // Iterate slices (pre-fetched above)
916
+ const slices = slicesByMilestone.get(milestone.id) ?? [];
899
917
  for (const slice of slices) {
900
918
  // Preserve slice-scoped artifacts imported from disk, including real PLAN
901
919
  // fallback content when task rows cannot regenerate it.
@@ -931,8 +949,8 @@ export async function renderAllFromDb(basePath: string): Promise<RenderAllResult
931
949
  );
932
950
  }
933
951
 
934
- // Iterate tasks
935
- const tasks = getSliceTasks(milestone.id, slice.id);
952
+ // Iterate tasks (batched above)
953
+ const tasks = tasksBySlice.get(`${milestone.id}\0${slice.id}`) ?? [];
936
954
  for (const task of tasks) {
937
955
  try {
938
956
  const ok = await renderTaskSummary(