@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
@@ -0,0 +1,55 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ import { recoverTimedOutUnit } from "../auto-timeout-recovery.ts";
8
+ import {
9
+ readUnitHarnessAbort,
10
+ readUnitRuntimeRecord,
11
+ recordUnitHarnessAbort,
12
+ } from "../unit-runtime.ts";
13
+
14
+ test("timeout recovery retry clears stale harness abort for the same unit run", async () => {
15
+ const base = mkdtempSync(join(tmpdir(), "gsd-timeout-recovery-abort-"));
16
+ const startedAt = Date.now();
17
+ mkdirSync(join(base, ".gsd"), { recursive: true });
18
+ try {
19
+ recordUnitHarnessAbort(base, "gate-evaluate", "M001/S01/gates+Q3", startedAt, {
20
+ kind: "turn-abort",
21
+ reason: "Agent turn aborted before the unit could complete its gate evaluation.",
22
+ });
23
+ assert.equal(
24
+ readUnitHarnessAbort(base, "gate-evaluate", "M001/S01/gates+Q3", startedAt)?.kind,
25
+ "turn-abort",
26
+ "test setup should start with an abort marker",
27
+ );
28
+
29
+ const messages: Array<{ message: unknown; options: unknown }> = [];
30
+ const notifications: Array<{ message: string; level?: string }> = [];
31
+ const result = await recoverTimedOutUnit(
32
+ { ui: { notify: (message: string, level?: string) => notifications.push({ message, level }) } } as any,
33
+ { sendMessage: (message: unknown, options: unknown) => messages.push({ message, options }) } as any,
34
+ "gate-evaluate",
35
+ "M001/S01/gates+Q3",
36
+ "idle",
37
+ {
38
+ basePath: base,
39
+ verbose: false,
40
+ currentUnitStartedAt: startedAt,
41
+ unitRecoveryCount: new Map(),
42
+ },
43
+ );
44
+
45
+ assert.equal(result, "recovered");
46
+ assert.equal(readUnitHarnessAbort(base, "gate-evaluate", "M001/S01/gates+Q3", startedAt), null);
47
+ const record = readUnitRuntimeRecord(base, "gate-evaluate", "M001/S01/gates+Q3");
48
+ assert.equal(record?.phase, "recovered");
49
+ assert.equal(record?.lastProgressKind, "idle-recovery-retry");
50
+ assert.equal(messages.length, 1, "retry recovery should send one steering message");
51
+ assert.equal(notifications.length, 1, "retry recovery should notify once");
52
+ } finally {
53
+ rmSync(base, { recursive: true, force: true });
54
+ }
55
+ });
@@ -34,6 +34,11 @@ import {
34
34
  clearPendingAutoStart,
35
35
  setPendingAutoStart,
36
36
  } from "../guided-flow.ts";
37
+ import {
38
+ closeDatabase,
39
+ insertMilestone,
40
+ openDatabase,
41
+ } from "../gsd-db.ts";
37
42
 
38
43
  // ─── Bug 2: this-binding regression ─────────────────────────────────────
39
44
 
@@ -89,6 +94,8 @@ test("checkAutoStartAfterDiscuss completes when discussion manifest is absent",
89
94
  mkdirSync(milestoneDir, { recursive: true });
90
95
  writeFileSync(join(milestoneDir, "M001-CONTEXT.md"), "# Context\n", "utf-8");
91
96
  writeFileSync(join(base, ".gsd", "STATE.md"), "# State\n", "utf-8");
97
+ openDatabase(":memory:");
98
+ insertMilestone({ id: "M001", title: "M001", status: "queued" });
92
99
  setPendingAutoStart(base, {
93
100
  basePath: base,
94
101
  milestoneId: "M001",
@@ -110,6 +117,7 @@ test("checkAutoStartAfterDiscuss completes when discussion manifest is absent",
110
117
  },
111
118
  ]);
112
119
  } finally {
120
+ closeDatabase();
113
121
  clearPendingAutoStart(base);
114
122
  rmSync(base, { recursive: true, force: true });
115
123
  }
@@ -0,0 +1,85 @@
1
+ // batch-task-query.test.ts
2
+ // Verifies getTasksBySliceIds batches tasks correctly and matches the per-slice
3
+ // getSliceTasks path (guards the projection N+1 fix).
4
+
5
+ import { test } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { tmpdir } from "node:os";
10
+
11
+ import {
12
+ openDatabase,
13
+ closeDatabase,
14
+ insertMilestone,
15
+ insertSlice,
16
+ insertTask,
17
+ getSliceTasks,
18
+ getTasksBySliceIds,
19
+ } from "../gsd-db.ts";
20
+
21
+ function withDb(fn: () => void): void {
22
+ const base = mkdtempSync(join(tmpdir(), "gsd-batch-task-"));
23
+ mkdirSync(join(base, ".gsd"), { recursive: true });
24
+ try {
25
+ openDatabase(join(base, ".gsd", "gsd.db"));
26
+ fn();
27
+ } finally {
28
+ try { closeDatabase(); } catch {}
29
+ try { rmSync(base, { recursive: true, force: true }); } catch {}
30
+ }
31
+ }
32
+
33
+ function seed(): void {
34
+ insertMilestone({ id: "M001", title: "M1", status: "active" });
35
+ insertMilestone({ id: "M002", title: "M2", status: "active" });
36
+ insertSlice({ id: "S01", milestoneId: "M001", title: "S1", status: "active", risk: "low", depends: [] });
37
+ insertSlice({ id: "S02", milestoneId: "M001", title: "S2", status: "pending", risk: "low", depends: [] });
38
+ insertSlice({ id: "S01", milestoneId: "M002", title: "S1", status: "active", risk: "low", depends: [] });
39
+ // M001/S01 has two tasks; M001/S02 has one; M002/S01 has none.
40
+ insertTask({ id: "T02", sliceId: "S01", milestoneId: "M001", title: "second", status: "pending" });
41
+ insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "first", status: "complete" });
42
+ insertTask({ id: "T01", sliceId: "S02", milestoneId: "M001", title: "only", status: "pending" });
43
+ }
44
+
45
+ const key = (m: string, s: string) => `${m}\0${s}`;
46
+
47
+ test("getTasksBySliceIds buckets tasks per (milestone, slice)", () => {
48
+ withDb(() => {
49
+ seed();
50
+ const map = getTasksBySliceIds([
51
+ { milestoneId: "M001", sliceId: "S01" },
52
+ { milestoneId: "M001", sliceId: "S02" },
53
+ { milestoneId: "M002", sliceId: "S01" },
54
+ ]);
55
+ assert.equal(map.get(key("M001", "S01"))?.length, 2);
56
+ assert.equal(map.get(key("M001", "S02"))?.length, 1);
57
+ // A slice with no tasks is absent (caller falls back to []).
58
+ assert.equal(map.has(key("M002", "S01")), false);
59
+ });
60
+ });
61
+
62
+ test("getTasksBySliceIds preserves ORDER BY sequence, id within a bucket", () => {
63
+ withDb(() => {
64
+ seed();
65
+ const bucket = getTasksBySliceIds([{ milestoneId: "M001", sliceId: "S01" }]).get(key("M001", "S01"));
66
+ // Insertion order was T02 then T01; ordering is by sequence then id, so T01 first.
67
+ assert.deepEqual(bucket?.map((t) => t.id), ["T01", "T02"]);
68
+ });
69
+ });
70
+
71
+ test("getTasksBySliceIds equals getSliceTasks for a single slice", () => {
72
+ withDb(() => {
73
+ seed();
74
+ const batched = getTasksBySliceIds([{ milestoneId: "M001", sliceId: "S01" }]).get(key("M001", "S01"));
75
+ const direct = getSliceTasks("M001", "S01");
76
+ assert.deepEqual(batched, direct);
77
+ });
78
+ });
79
+
80
+ test("getTasksBySliceIds returns an empty map for empty input", () => {
81
+ withDb(() => {
82
+ seed();
83
+ assert.equal(getTasksBySliceIds([]).size, 0);
84
+ });
85
+ });
@@ -13,6 +13,7 @@ import {
13
13
  checkAutoStartAfterDiscuss,
14
14
  setPendingAutoStart,
15
15
  clearPendingAutoStart,
16
+ _getPendingAutoStart,
16
17
  } from "../guided-flow.ts";
17
18
  import { drainLogs } from "../workflow-logger.ts";
18
19
  import {
@@ -77,6 +78,7 @@ describe("checkAutoStartAfterDiscuss ready-notify DB guard (R3b)", () => {
77
78
  let cap: MockCapture;
78
79
 
79
80
  beforeEach(() => {
81
+ closeDatabase();
80
82
  clearPendingAutoStart();
81
83
  drainLogs();
82
84
  });
@@ -129,6 +131,33 @@ describe("checkAutoStartAfterDiscuss ready-notify DB guard (R3b)", () => {
129
131
  ]);
130
132
  });
131
133
 
134
+ test("fails closed and keeps pending handoff when DB is unavailable", () => {
135
+ base = mkBase();
136
+ closeDatabase();
137
+
138
+ cap = mkCapture();
139
+ setPendingAutoStart(base, {
140
+ basePath: base,
141
+ milestoneId: "M001",
142
+ startAuto: false,
143
+ ctx: mkCtx(cap),
144
+ pi: mkPi(cap),
145
+ });
146
+
147
+ const result = checkAutoStartAfterDiscuss();
148
+ assert.equal(result, false, "DB-unavailable handoff must fail closed");
149
+ assert.equal(
150
+ _getPendingAutoStart(base)?.milestoneId,
151
+ "M001",
152
+ "failed closed handoff must remain pending for a later retry",
153
+ );
154
+ assert.equal(
155
+ cap.notifies.some(n => n.level === "success"),
156
+ false,
157
+ "must not notify success before the milestone row is verified",
158
+ );
159
+ });
160
+
132
161
  test("announces 'ready' when DB row has executable slices", () => {
133
162
  base = mkBase();
134
163
  openDatabase(":memory:");
@@ -722,3 +722,368 @@ test("ensureCodebaseMapFresh: uses TTL cache before enumerating files", () => {
722
722
  cleanup(base);
723
723
  }
724
724
  });
725
+
726
+ // ─── Workspace-aware (parent mode) ───────────────────────────────────────
727
+
728
+ /**
729
+ * Build a parent workspace: a parent git repo whose .gsd/PREFERENCES.md declares
730
+ * child repositories, each child being its own separate git repo nested inside.
731
+ */
732
+ function makeTmpParentWorkspace(children: string[]): string {
733
+ const base = join(tmpdir(), `gsd-codebase-parent-${randomUUID()}`);
734
+ mkdirSync(join(base, ".gsd"), { recursive: true });
735
+ execSync("git init", { cwd: base, stdio: "ignore" });
736
+
737
+ const repoLines = children.map((id) => ` ${id}:\n path: ${id}`).join("\n");
738
+ writeFileSync(
739
+ join(base, ".gsd", "PREFERENCES.md"),
740
+ `---\nversion: 1\nworkspace:\n mode: parent\n repositories:\n${repoLines}\n---\n`,
741
+ "utf-8",
742
+ );
743
+
744
+ for (const child of children) {
745
+ const childPath = join(base, child);
746
+ mkdirSync(childPath, { recursive: true });
747
+ execSync("git init", { cwd: childPath, stdio: "ignore" });
748
+ }
749
+ return base;
750
+ }
751
+
752
+ function addChildFile(base: string, child: string, path: string, content = ""): void {
753
+ const childRoot = join(base, child);
754
+ const fullPath = join(childRoot, path);
755
+ mkdirSync(join(fullPath, ".."), { recursive: true });
756
+ writeFileSync(fullPath, content || `// ${child}/${path}\n`, "utf-8");
757
+ execSync(`git add "${path}"`, { cwd: childRoot, stdio: "ignore" });
758
+ }
759
+
760
+ test("workspace-aware: parent mode enumerates each declared child repo under a repo heading", () => {
761
+ const base = makeTmpParentWorkspace(["frontend", "backend"]);
762
+ try {
763
+ addChildFile(base, "frontend", "src/App.tsx");
764
+ addChildFile(base, "backend", "src/server.ts");
765
+
766
+ const result = generateCodebaseMap(base);
767
+
768
+ // Both repos appear as labelled sections.
769
+ assert.match(result.content, /## \[frontend\]/);
770
+ assert.match(result.content, /## \[backend\]/);
771
+ // Child files are rewritten to workspace-relative paths.
772
+ assert.match(result.content, /`frontend\/src\/App\.tsx`/);
773
+ assert.match(result.content, /`backend\/src\/server\.ts`/);
774
+ assert.equal(result.fileCount, 2);
775
+ } finally {
776
+ cleanup(base);
777
+ }
778
+ });
779
+
780
+ test("workspace-aware: metadata records the repositories whose files appear in the map", () => {
781
+ const base = makeTmpParentWorkspace(["frontend", "backend"]);
782
+ try {
783
+ addChildFile(base, "frontend", "src/App.tsx");
784
+ addChildFile(base, "backend", "src/server.ts");
785
+
786
+ const result = generateCodebaseMap(base);
787
+ const metadata = parseCodebaseMapMetadata(result.content);
788
+
789
+ assert.ok(metadata, "metadata should be present");
790
+ assert.deepEqual(metadata?.repositories?.sort(), ["backend", "frontend"]);
791
+ } finally {
792
+ cleanup(base);
793
+ }
794
+ });
795
+
796
+ test("workspace-aware: parseCodebaseMap recovers descriptions across repo sections", () => {
797
+ const base = makeTmpParentWorkspace(["frontend", "backend"]);
798
+ try {
799
+ addChildFile(base, "frontend", "src/App.tsx");
800
+ addChildFile(base, "backend", "src/server.ts");
801
+
802
+ // Seed descriptions keyed by the workspace-relative paths.
803
+ const descriptions = new Map<string, string>([
804
+ ["frontend/src/App.tsx", "React entry"],
805
+ ["backend/src/server.ts", "Express API"],
806
+ ]);
807
+ const result = generateCodebaseMap(base, undefined, descriptions);
808
+ writeCodebaseMap(base, result.content);
809
+
810
+ const parsed = parseCodebaseMap(readCodebaseMap(base)!);
811
+ assert.equal(parsed.get("frontend/src/App.tsx"), "React entry");
812
+ assert.equal(parsed.get("backend/src/server.ts"), "Express API");
813
+ } finally {
814
+ cleanup(base);
815
+ }
816
+ });
817
+
818
+ test("workspace-aware: project mode (no declared children) stays single-section", () => {
819
+ // A plain single repo with no workspace config must render exactly as before.
820
+ const base = makeTmpRepo();
821
+ try {
822
+ addFile(base, "src/main.ts");
823
+
824
+ const result = generateCodebaseMap(base);
825
+
826
+ assert.doesNotMatch(result.content, /## \[/);
827
+ assert.match(result.content, /`src\/main\.ts`/);
828
+ const metadata = parseCodebaseMapMetadata(result.content);
829
+ assert.equal(metadata?.repositories, undefined);
830
+ } finally {
831
+ cleanup(base);
832
+ }
833
+ });
834
+
835
+ test("workspace-aware: updateCodebaseMap tracks child-repo file additions", () => {
836
+ const base = makeTmpParentWorkspace(["frontend"]);
837
+ try {
838
+ addChildFile(base, "frontend", "src/App.tsx");
839
+ const first = updateCodebaseMap(base);
840
+ writeCodebaseMap(base, first.content);
841
+ assert.equal(first.added, 1);
842
+
843
+ addChildFile(base, "frontend", "src/index.tsx");
844
+ const second = updateCodebaseMap(base);
845
+ assert.equal(second.added, 1);
846
+ assert.equal(second.removed, 0);
847
+ assert.equal(second.fileCount, 2);
848
+ } finally {
849
+ cleanup(base);
850
+ }
851
+ });
852
+
853
+ test("workspace-aware: workspace-relative exclude patterns filter child-repo files", () => {
854
+ // Regression guard: exclude patterns like `frontend/` are workspace-relative,
855
+ // so they must be matched against the rewritten path, not the child-relative one.
856
+ const base = makeTmpParentWorkspace(["frontend", "backend"]);
857
+ try {
858
+ addChildFile(base, "frontend", "src/App.tsx");
859
+ addChildFile(base, "backend", "src/server.ts");
860
+
861
+ const result = generateCodebaseMap(base, { excludePatterns: ["frontend/"] });
862
+
863
+ assert.doesNotMatch(result.content, /frontend\/src\/App\.tsx/);
864
+ assert.match(result.content, /backend\/src\/server\.ts/);
865
+ assert.equal(result.fileCount, 1);
866
+ } finally {
867
+ cleanup(base);
868
+ }
869
+ });
870
+
871
+ test("workspace-aware: maxFiles cap does not starve child repos for the project repo", () => {
872
+ // The implicit project repo is enumerated alongside children; the cap must be
873
+ // applied fairly (round-robin) so a large project repo cannot crowd out children.
874
+ const base = makeTmpParentWorkspace(["frontend", "backend"]);
875
+ try {
876
+ addChildFile(base, "frontend", "a.ts");
877
+ addChildFile(base, "frontend", "b.ts");
878
+ addChildFile(base, "backend", "c.ts");
879
+
880
+ // Cap at 2 files — with fair distribution both repos should appear.
881
+ const result = generateCodebaseMap(base, { maxFiles: 2 });
882
+ const repoSections = (result.content.match(/^## \[.+\]/gm) ?? []).length;
883
+
884
+ assert.equal(result.fileCount, 2);
885
+ assert.ok(repoSections >= 2, "both child repos should be represented under a tight cap");
886
+ } finally {
887
+ cleanup(base);
888
+ }
889
+ });
890
+
891
+ test("workspace-aware: overlapping paths dedupe without falsely reporting truncation", () => {
892
+ // Regression guard: a path tracked by both the implicit project repo and a child
893
+ // repo collapses to one workspace-relative entry. Deduplicated duplicates must not
894
+ // count toward the maxFiles cap, so the map must not be flagged as truncated.
895
+ const base = join(tmpdir(), `gsd-codebase-parent-${randomUUID()}`);
896
+ mkdirSync(join(base, ".gsd"), { recursive: true });
897
+ execSync("git init", { cwd: base, stdio: "ignore" });
898
+ writeFileSync(
899
+ join(base, ".gsd", "PREFERENCES.md"),
900
+ `---\nversion: 1\nworkspace:\n mode: parent\n repositories:\n lib:\n path: lib\n---\n`,
901
+ "utf-8",
902
+ );
903
+ try {
904
+ // The project repo tracks lib/util.ts before lib becomes its own repo; the child
905
+ // repo then tracks util.ts, so both resolve to the same workspace path lib/util.ts.
906
+ addFile(base, "lib/util.ts");
907
+ execSync("git init", { cwd: join(base, "lib"), stdio: "ignore" });
908
+ execSync("git add util.ts", { cwd: join(base, "lib"), stdio: "ignore" });
909
+
910
+ const result = generateCodebaseMap(base);
911
+
912
+ assert.equal(result.fileCount, 1);
913
+ assert.equal(result.truncated, false);
914
+ assert.equal((result.content.match(/`lib\/util\.ts`/g) ?? []).length, 1);
915
+ } finally {
916
+ cleanup(base);
917
+ }
918
+ });
919
+
920
+ test("workspace-aware: project directories interleaving child prefixes stay under one heading", () => {
921
+ // Regression guard: the implicit project repo has an empty prefix, so its
922
+ // root-level dirs (aaa/, zzz/) sort lexically around a child prefix (mmm/).
923
+ // Groups must be ordered per repo so `## [project]` is emitted exactly once.
924
+ const base = makeTmpParentWorkspace(["mmm"]);
925
+ try {
926
+ addFile(base, "aaa/x.ts"); // project repo, sorts before "mmm"
927
+ addFile(base, "zzz/y.ts"); // project repo, sorts after "mmm"
928
+ addChildFile(base, "mmm", "src/thing.ts");
929
+
930
+ const result = generateCodebaseMap(base);
931
+
932
+ const projectHeadings = (result.content.match(/^## \[project\]$/gm) ?? []).length;
933
+ const mmmHeadings = (result.content.match(/^## \[mmm\]$/gm) ?? []).length;
934
+ const allHeadings = (result.content.match(/^## \[.+\]$/gm) ?? []).length;
935
+
936
+ assert.equal(projectHeadings, 1, "project section must not be split across headings");
937
+ assert.equal(mmmHeadings, 1);
938
+ assert.equal(allHeadings, 2);
939
+ } finally {
940
+ cleanup(base);
941
+ }
942
+ });
943
+
944
+ test("workspace-aware: renaming a declared repo id invalidates map freshness", () => {
945
+ // Regression guard: repo identity is rendered into the map, so renaming a repo id
946
+ // (path unchanged → identical file paths) must be caught by the freshness
947
+ // fingerprint rather than leaving a stale `## [old-id]` heading in place.
948
+ const base = makeTmpParentWorkspace(["frontend"]);
949
+ try {
950
+ addChildFile(base, "frontend", "src/App.tsx");
951
+
952
+ ensureCodebaseMapFresh(base, undefined, { ttlMs: 0, force: true });
953
+ assert.match(readCodebaseMap(base)!, /^## \[frontend\]$/m);
954
+
955
+ // Rename the repo id to `web`; the path (and therefore the file paths) is unchanged.
956
+ writeFileSync(
957
+ join(base, ".gsd", "PREFERENCES.md"),
958
+ `---\nversion: 1\nworkspace:\n mode: parent\n repositories:\n web:\n path: frontend\n---\n`,
959
+ "utf-8",
960
+ );
961
+
962
+ const refreshed = ensureCodebaseMapFresh(base, undefined, { ttlMs: 0, force: true });
963
+ const written = readCodebaseMap(base)!;
964
+
965
+ assert.equal(refreshed.status, "updated");
966
+ assert.match(written, /^## \[web\]$/m);
967
+ assert.doesNotMatch(written, /^## \[frontend\]$/m);
968
+ assert.deepEqual(parseCodebaseMapMetadata(written)?.repositories, ["web"]);
969
+ } finally {
970
+ cleanup(base);
971
+ }
972
+ });
973
+
974
+ test("single-repo: directory order stays byte-identical code-unit sort (not locale-aware)", () => {
975
+ // Byte-identical single-repo invariant: pre-workspace output ordered
976
+ // directories with a bare `.sort()` (UTF-16 code-unit order), so uppercase
977
+ // sorts before lowercase ("Zoo" < "apple"). A locale-aware comparator inverts
978
+ // these and silently changes project-mode CODEBASE.md, so guard the ordering.
979
+ const base = makeTmpRepo();
980
+ try {
981
+ addFile(base, "Zoo/keeper.ts"); // 'Z' (0x5A) < 'a' (0x61) → sorts first
982
+ addFile(base, "apple/tree.ts");
983
+
984
+ const content = generateCodebaseMap(base).content;
985
+ const zooIdx = content.indexOf("### Zoo/");
986
+ const appleIdx = content.indexOf("### apple/");
987
+
988
+ assert.notEqual(zooIdx, -1);
989
+ assert.notEqual(appleIdx, -1);
990
+ assert.ok(zooIdx < appleIdx, "code-unit order must place Zoo/ before apple/");
991
+ // Project mode emits no repo partition heading.
992
+ assert.doesNotMatch(content, /^## \[.+\]$/m);
993
+ } finally {
994
+ cleanup(base);
995
+ }
996
+ });
997
+
998
+ test("workspace-aware: an invalid workspace registry surfaces an error instead of silently falling back", () => {
999
+ // Regression guard: a broken parent-mode config (here a child path escaping the
1000
+ // project root) must propagate as an error. Swallowing it and falling back to a
1001
+ // single-root map would silently drop every child repo from planning context.
1002
+ const base = makeTmpRepo();
1003
+ try {
1004
+ addFile(base, "src/main.ts");
1005
+ writeFileSync(
1006
+ join(base, ".gsd", "PREFERENCES.md"),
1007
+ `---\nversion: 1\nworkspace:\n mode: parent\n repositories:\n frontend:\n path: ../escapes-root\n---\n`,
1008
+ "utf-8",
1009
+ );
1010
+
1011
+ assert.throws(() => generateCodebaseMap(base), /resolves outside project root/);
1012
+ } finally {
1013
+ cleanup(base);
1014
+ }
1015
+ });
1016
+
1017
+ test("workspace-aware: files sharing a directory across repos keep their own repo heading", () => {
1018
+ // Regression guard: the implicit project repo and a child repo can own different
1019
+ // files under the same workspace-relative directory (lib/). Groups are keyed by
1020
+ // (repo, dir), so each file must render under its own `## [repo-id]` heading
1021
+ // instead of collapsing into one section labelled with a single (wrong) repo.
1022
+ const base = join(tmpdir(), `gsd-codebase-parent-${randomUUID()}`);
1023
+ mkdirSync(join(base, ".gsd"), { recursive: true });
1024
+ execSync("git init", { cwd: base, stdio: "ignore" });
1025
+ writeFileSync(
1026
+ join(base, ".gsd", "PREFERENCES.md"),
1027
+ `---\nversion: 1\nworkspace:\n mode: parent\n repositories:\n lib:\n path: lib\n---\n`,
1028
+ "utf-8",
1029
+ );
1030
+ try {
1031
+ // Project repo tracks lib/app.ts before lib becomes its own repo; the child
1032
+ // repo then tracks helper.ts → workspace path lib/helper.ts. Different files,
1033
+ // same directory, different repos.
1034
+ addFile(base, "lib/app.ts");
1035
+ execSync("git init", { cwd: join(base, "lib"), stdio: "ignore" });
1036
+ writeFileSync(join(base, "lib", "helper.ts"), "// helper\n", "utf-8");
1037
+ execSync("git add helper.ts", { cwd: join(base, "lib"), stdio: "ignore" });
1038
+
1039
+ const content = generateCodebaseMap(base).content;
1040
+
1041
+ // Both files present, each under its own repo section (not merged under one).
1042
+ assert.match(content, /## \[project\]/);
1043
+ assert.match(content, /## \[lib\]/);
1044
+ const projectIdx = content.indexOf("## [project]");
1045
+ const appIdx = content.indexOf("`lib/app.ts`");
1046
+ const libIdx = content.indexOf("## [lib]");
1047
+ const helperIdx = content.indexOf("`lib/helper.ts`");
1048
+ assert.ok(
1049
+ projectIdx < appIdx && appIdx < libIdx && libIdx < helperIdx,
1050
+ "lib/app.ts must sit under [project] and lib/helper.ts under [lib]",
1051
+ );
1052
+ } finally {
1053
+ cleanup(base);
1054
+ }
1055
+ });
1056
+
1057
+ test("ensureCodebaseMapFresh: editing declared repos within the TTL window bypasses the cache", () => {
1058
+ // Regression guard: the freshness TTL cache key folds in the workspace signature
1059
+ // (mode + declared repos). Adding a child repo must miss the cache and re-enumerate
1060
+ // rather than serve a stale map that omits the new repo until the TTL expires.
1061
+ const base = makeTmpParentWorkspace(["frontend"]);
1062
+ try {
1063
+ addChildFile(base, "frontend", "src/App.tsx");
1064
+
1065
+ const initial = ensureCodebaseMapFresh(base, undefined, { ttlMs: 60_000 });
1066
+ assert.equal(initial.status, "generated");
1067
+ assert.doesNotMatch(readCodebaseMap(base)!, /## \[backend\]/);
1068
+
1069
+ // Declare a second child repo and back it with a real nested repo + file.
1070
+ mkdirSync(join(base, "backend"), { recursive: true });
1071
+ execSync("git init", { cwd: join(base, "backend"), stdio: "ignore" });
1072
+ addChildFile(base, "backend", "src/server.ts");
1073
+ writeFileSync(
1074
+ join(base, ".gsd", "PREFERENCES.md"),
1075
+ `---\nversion: 1\nworkspace:\n mode: parent\n repositories:\n frontend:\n path: frontend\n backend:\n path: backend\n---\n`,
1076
+ "utf-8",
1077
+ );
1078
+
1079
+ // Same TTL window, no force: the changed workspace signature must miss the cache.
1080
+ const refreshed = ensureCodebaseMapFresh(base, undefined, { ttlMs: 60_000 });
1081
+ const written = readCodebaseMap(base)!;
1082
+
1083
+ assert.equal(refreshed.status, "updated");
1084
+ assert.match(written, /## \[backend\]/);
1085
+ assert.match(written, /`backend\/src\/server\.ts`/);
1086
+ } finally {
1087
+ cleanup(base);
1088
+ }
1089
+ });
@@ -12,7 +12,14 @@ import { tmpdir } from "node:os";
12
12
 
13
13
  import { buildSliceSummaryExcerpt, buildCompleteMilestonePrompt, buildValidateMilestonePrompt } from "../auto-prompts.ts";
14
14
  import { invalidateAllCaches } from "../cache.ts";
15
- import { closeDatabase, insertMilestone, openDatabase } from "../gsd-db.ts";
15
+ import {
16
+ closeDatabase,
17
+ insertGateRow,
18
+ insertMilestone,
19
+ insertSlice,
20
+ openDatabase,
21
+ saveGateResult,
22
+ } from "../gsd-db.ts";
16
23
 
17
24
  // ─── Fixture helpers ──────────────────────────────────────────────────────
18
25
 
@@ -366,6 +373,60 @@ test("validate-milestone prompt uses slice excerpts and on-demand paths instead
366
373
  );
367
374
  });
368
375
 
376
+ test("validate-milestone prompt inlines persisted slice-level Q3/Q4 gate flags", async (t) => {
377
+ const base = createBase();
378
+ t.after(() => {
379
+ try { closeDatabase(); } catch { /* ignore */ }
380
+ cleanup(base);
381
+ });
382
+ invalidateAllCaches();
383
+
384
+ openDatabase(join(base, ".gsd", "gsd.db"));
385
+ insertMilestone({ id: "M001", title: "Test Milestone", status: "active" });
386
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Risky Slice", status: "complete" });
387
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", scope: "slice" });
388
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q4", scope: "slice" });
389
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q8", scope: "slice" });
390
+ saveGateResult({
391
+ milestoneId: "M001",
392
+ sliceId: "S01",
393
+ gateId: "Q3",
394
+ verdict: "flag",
395
+ rationale: "New callback boundary.",
396
+ findings: "Re-check token exposure in the web callback.",
397
+ });
398
+ saveGateResult({
399
+ milestoneId: "M001",
400
+ sliceId: "S01",
401
+ gateId: "Q4",
402
+ verdict: "flag",
403
+ rationale: "Requirement touched but not re-tested.",
404
+ findings: "R012 must be re-tested; revisit decision D-3.",
405
+ });
406
+ saveGateResult({
407
+ milestoneId: "M001",
408
+ sliceId: "S01",
409
+ gateId: "Q8",
410
+ verdict: "flag",
411
+ rationale: "Operational readiness gap.",
412
+ findings: "Operational finding should not be milestone validation input.",
413
+ });
414
+ writeRoadmap(base, makeRoadmap());
415
+ writeSummary(base, "S01", makeFatSummary("S01"));
416
+
417
+ const prompt = await buildValidateMilestonePrompt("M001", "Test Milestone", base);
418
+
419
+ assert.match(prompt, /### Persisted Slice-Level Gate Flags \(from quality_gates\)/);
420
+ assert.match(prompt, /S01 \/ Q3 \(Threat Surface\) \/ flag/);
421
+ assert.match(prompt, /Re-check token exposure in the web callback\./);
422
+ assert.match(prompt, /S01 \/ Q4 \(Requirement Impact\) \/ flag/);
423
+ assert.match(prompt, /R012 must be re-tested; revisit decision D-3\./);
424
+ assert.ok(
425
+ !prompt.includes("Operational finding should not be milestone validation input."),
426
+ "validate-milestone should not inline unrelated Q8 gate findings",
427
+ );
428
+ });
429
+
369
430
  test("validate-milestone prompt inlines planned verification classes as canonical rows", async (t) => {
370
431
  const base = createBase();
371
432
  t.after(() => {