@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
@@ -4,7 +4,7 @@ import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync
4
4
  import { join } from 'node:path';
5
5
  import { tmpdir } from 'node:os';
6
6
 
7
- import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask, getSlice, getTask, getGateResults } from '../gsd-db.ts';
7
+ import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask, getSlice, getTask, getSliceTasks, getGateResults } from '../gsd-db.ts';
8
8
  import { handlePlanTask } from '../tools/plan-task.ts';
9
9
  import { parseTaskPlanFile } from '../files.ts';
10
10
 
@@ -24,6 +24,26 @@ function seedParent(): void {
24
24
  insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Planning slice', status: 'pending', demo: 'Rendered plans exist.' });
25
25
  }
26
26
 
27
+ function writeParentWorkspacePreferences(base: string): void {
28
+ mkdirSync(join(base, 'frontend', 'src'), { recursive: true });
29
+ mkdirSync(join(base, 'backend', 'src'), { recursive: true });
30
+ writeFileSync(
31
+ join(base, '.gsd', 'PREFERENCES.md'),
32
+ [
33
+ '---',
34
+ 'workspace:',
35
+ ' mode: parent',
36
+ ' repositories:',
37
+ ' frontend:',
38
+ ' path: frontend',
39
+ ' backend:',
40
+ ' path: backend',
41
+ '---',
42
+ ].join('\n'),
43
+ 'utf-8',
44
+ );
45
+ }
46
+
27
47
  function validParams() {
28
48
  return {
29
49
  milestoneId: 'M001',
@@ -155,6 +175,32 @@ test('handlePlanTask rejects absolute task IO paths outside the active worktree'
155
175
  }
156
176
  });
157
177
 
178
+ test('handlePlanTask rejects parent-checkout task IO paths from an active worktree', async () => {
179
+ const projectRoot = mkdtempSync(join(tmpdir(), 'gsd-plan-task-project-'));
180
+ const worktree = join(projectRoot, '.gsd', 'worktrees', 'M001');
181
+ mkdirSync(join(projectRoot, '.gsd'), { recursive: true });
182
+ mkdirSync(join(worktree, '.gsd', 'phases', '01-test'), { recursive: true });
183
+ openDatabase(join(projectRoot, '.gsd', 'gsd.db'));
184
+
185
+ try {
186
+ seedParent();
187
+ const parentCheckoutPath = join(projectRoot, 'src', 'index.ts');
188
+ const result = await handlePlanTask({
189
+ ...validParams(),
190
+ files: [parentCheckoutPath],
191
+ inputs: [parentCheckoutPath],
192
+ expectedOutput: [parentCheckoutPath],
193
+ }, worktree);
194
+
195
+ assert.ok('error' in result);
196
+ assert.match(result.error, /validation failed: files contains path outside allowed repository roots/);
197
+ assert.match(result.error, new RegExp(worktree.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
198
+ assert.equal(getTask('M001', 'S02', 'T02'), null, 'parent-checkout paths must not persist worktree task planning');
199
+ } finally {
200
+ cleanup(projectRoot);
201
+ }
202
+ });
203
+
158
204
  test('handlePlanTask rejects missing parent slice', async () => {
159
205
  const base = makeTmpBase();
160
206
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -243,3 +289,220 @@ test('handlePlanTask reruns idempotently and refreshes parse-visible state', asy
243
289
  cleanup(base);
244
290
  }
245
291
  });
292
+
293
+ test('handlePlanTask persists targetRepositories for parent-workspace tasks', async () => {
294
+ const base = makeTmpBase();
295
+ openDatabase(join(base, '.gsd', 'gsd.db'));
296
+
297
+ try {
298
+ seedParent();
299
+ const result = await handlePlanTask({
300
+ ...validParams(),
301
+ targetRepositories: ['project'],
302
+ }, base);
303
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
304
+
305
+ const tasks = getSliceTasks('M001', 'S02');
306
+ const planned = tasks.find((t) => t.id === 'T02');
307
+ assert.ok(planned, 'planned task should exist');
308
+ assert.deepEqual(planned?.target_repositories, ['project']);
309
+ } finally {
310
+ cleanup(base);
311
+ }
312
+ });
313
+
314
+ test('handlePlanTask rejects non-array targetRepositories', async () => {
315
+ const base = makeTmpBase();
316
+ openDatabase(join(base, '.gsd', 'gsd.db'));
317
+
318
+ try {
319
+ seedParent();
320
+ const result = await handlePlanTask({
321
+ ...validParams(),
322
+ targetRepositories: 'frontend' as unknown as string[],
323
+ }, base);
324
+ assert.ok('error' in result);
325
+ assert.match(result.error, /validation failed: targetRepositories/);
326
+ } finally {
327
+ cleanup(base);
328
+ }
329
+ });
330
+
331
+ test('handlePlanTask rejects empty targetRepositories', async () => {
332
+ const base = makeTmpBase();
333
+ openDatabase(join(base, '.gsd', 'gsd.db'));
334
+
335
+ try {
336
+ seedParent();
337
+ const result = await handlePlanTask({
338
+ ...validParams(),
339
+ targetRepositories: [],
340
+ }, base);
341
+ assert.ok('error' in result);
342
+ assert.match(result.error, /validation failed: targetRepositories must include at least one repository id when provided/);
343
+ assert.equal(getTask('M001', 'S02', 'T02'), null, 'invalid target repositories must not persist');
344
+ } finally {
345
+ cleanup(base);
346
+ }
347
+ });
348
+
349
+ test('handlePlanTask rejects unknown target repositories', async () => {
350
+ const base = makeTmpBase();
351
+ openDatabase(join(base, '.gsd', 'gsd.db'));
352
+
353
+ try {
354
+ seedParent();
355
+ const result = await handlePlanTask({
356
+ ...validParams(),
357
+ targetRepositories: ['frontend'],
358
+ }, base);
359
+ assert.ok('error' in result);
360
+ assert.match(result.error, /validation failed: unknown targetRepositories:/);
361
+ assert.equal(getTask('M001', 'S02', 'T02'), null, 'unknown target repositories must not persist');
362
+ } finally {
363
+ cleanup(base);
364
+ }
365
+ });
366
+
367
+ test('handlePlanTask enforces path scope to declared target repositories', async () => {
368
+ const base = makeTmpBase();
369
+ openDatabase(join(base, '.gsd', 'gsd.db'));
370
+
371
+ try {
372
+ seedParent();
373
+ mkdirSync(join(base, 'frontend', 'src'), { recursive: true });
374
+ mkdirSync(join(base, 'backend', 'src'), { recursive: true });
375
+ writeFileSync(
376
+ join(base, '.gsd', 'PREFERENCES.md'),
377
+ [
378
+ '---',
379
+ 'workspace:',
380
+ ' mode: parent',
381
+ ' repositories:',
382
+ ' frontend:',
383
+ ' path: frontend',
384
+ ' backend:',
385
+ ' path: backend',
386
+ '---',
387
+ ].join('\n'),
388
+ 'utf-8',
389
+ );
390
+
391
+ const result = await handlePlanTask({
392
+ ...validParams(),
393
+ targetRepositories: ['frontend'],
394
+ files: [join(base, 'backend', 'src', 'server.ts')],
395
+ inputs: ['app.js'],
396
+ expectedOutput: ['app.js'],
397
+ }, base);
398
+ assert.ok('error' in result);
399
+ assert.match(result.error, /validation failed: files contains path outside allowed repository roots/);
400
+ assert.equal(getTask('M001', 'S02', 'T02'), null, 'invalid scoped paths must not persist');
401
+ } finally {
402
+ cleanup(base);
403
+ }
404
+ });
405
+
406
+ test('handlePlanTask inherits parent slice target repositories for path scope', async () => {
407
+ const base = makeTmpBase();
408
+ openDatabase(join(base, '.gsd', 'gsd.db'));
409
+
410
+ try {
411
+ seedParent();
412
+ insertSlice({
413
+ id: 'S02',
414
+ milestoneId: 'M001',
415
+ planning: { targetRepositories: ['frontend'] },
416
+ });
417
+ writeParentWorkspacePreferences(base);
418
+
419
+ const result = await handlePlanTask({
420
+ ...validParams(),
421
+ files: [join(base, 'backend', 'src', 'server.ts')],
422
+ inputs: ['app.js'],
423
+ expectedOutput: ['app.js'],
424
+ }, base);
425
+ assert.ok('error' in result);
426
+ assert.match(result.error, /validation failed: files contains path outside allowed repository roots/);
427
+ assert.equal(getTask('M001', 'S02', 'T02'), null, 'slice-scoped invalid paths must not persist');
428
+ } finally {
429
+ cleanup(base);
430
+ }
431
+ });
432
+
433
+ test('handlePlanTask preserves stored task target repositories when omitted replan needs them for path scope', async () => {
434
+ const base = makeTmpBase();
435
+ openDatabase(join(base, '.gsd', 'gsd.db'));
436
+
437
+ try {
438
+ seedParent();
439
+ writeParentWorkspacePreferences(base);
440
+ insertSlice({
441
+ id: 'S02',
442
+ milestoneId: 'M001',
443
+ planning: { targetRepositories: ['frontend'] },
444
+ });
445
+ insertTask({
446
+ id: 'T02',
447
+ sliceId: 'S02',
448
+ milestoneId: 'M001',
449
+ title: 'Parent root work',
450
+ status: 'pending',
451
+ planning: { targetRepositories: ['project'] },
452
+ });
453
+
454
+ const rootFile = join(base, 'root-config.json');
455
+ const result = await handlePlanTask({
456
+ ...validParams(),
457
+ files: [rootFile],
458
+ inputs: [rootFile],
459
+ expectedOutput: [rootFile],
460
+ }, base);
461
+
462
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
463
+ assert.deepEqual(getTask('M001', 'S02', 'T02')?.target_repositories, ['project']);
464
+ } finally {
465
+ cleanup(base);
466
+ }
467
+ });
468
+
469
+ test('handlePlanTask follows the current slice target default when omitted replan paths validate there', async () => {
470
+ const base = makeTmpBase();
471
+ openDatabase(join(base, '.gsd', 'gsd.db'));
472
+
473
+ try {
474
+ seedParent();
475
+ writeParentWorkspacePreferences(base);
476
+ insertSlice({
477
+ id: 'S02',
478
+ milestoneId: 'M001',
479
+ planning: { targetRepositories: ['frontend'] },
480
+ });
481
+ insertTask({
482
+ id: 'T02',
483
+ sliceId: 'S02',
484
+ milestoneId: 'M001',
485
+ title: 'Child repo work',
486
+ status: 'pending',
487
+ planning: { targetRepositories: ['frontend'] },
488
+ });
489
+ insertSlice({
490
+ id: 'S02',
491
+ milestoneId: 'M001',
492
+ planning: { targetRepositories: ['backend'] },
493
+ });
494
+
495
+ const backendFile = join(base, 'backend', 'src', 'server.ts');
496
+ const result = await handlePlanTask({
497
+ ...validParams(),
498
+ files: [backendFile],
499
+ inputs: [backendFile],
500
+ expectedOutput: [backendFile],
501
+ }, base);
502
+
503
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
504
+ assert.deepEqual(getTask('M001', 'S02', 'T02')?.target_repositories, ['backend']);
505
+ } finally {
506
+ cleanup(base);
507
+ }
508
+ });
@@ -141,6 +141,26 @@ test("invalid value types produce errors and fall back to undefined", () => {
141
141
  }
142
142
  });
143
143
 
144
+ test("remote_questions accepts object config and false disables without error", () => {
145
+ const disabled = validatePreferences({ remote_questions: false } as any);
146
+ assert.equal(disabled.errors.length, 0);
147
+ assert.equal(disabled.preferences.remote_questions, undefined);
148
+
149
+ const configured = validatePreferences({
150
+ remote_questions: { channel: "telegram", channel_id: "12345" },
151
+ });
152
+ assert.equal(configured.errors.length, 0);
153
+ assert.deepEqual(configured.preferences.remote_questions, {
154
+ channel: "telegram",
155
+ channel_id: "12345",
156
+ });
157
+
158
+ for (const value of ["telegram", 0]) {
159
+ const invalid = validatePreferences({ remote_questions: value } as any);
160
+ assert.ok(invalid.errors.includes("remote_questions must be an object"));
161
+ }
162
+ });
163
+
144
164
  test("flat_rate_providers: accepts string array", () => {
145
165
  const { errors, preferences } = validatePreferences({
146
166
  flat_rate_providers: ["my-proxy", "private-cli"],
@@ -327,6 +347,136 @@ test("workspace.repositories duplicate path error includes both repository ids",
327
347
  );
328
348
  });
329
349
 
350
+ test("workspace.mode parent with no repositories is rejected", () => {
351
+ const { errors } = validatePreferences({
352
+ workspace: {
353
+ mode: "parent",
354
+ },
355
+ });
356
+
357
+ assert.ok(
358
+ errors.some((e) =>
359
+ e.includes('workspace.mode "parent" requires at least one repository under workspace.repositories'),
360
+ ),
361
+ );
362
+ });
363
+
364
+ test("workspace.mode parent with empty repositories map is rejected", () => {
365
+ const { errors } = validatePreferences({
366
+ workspace: {
367
+ mode: "parent",
368
+ repositories: {},
369
+ },
370
+ });
371
+
372
+ assert.ok(
373
+ errors.some((e) =>
374
+ e.includes('workspace.mode "parent" requires at least one repository under workspace.repositories'),
375
+ ),
376
+ );
377
+ });
378
+
379
+ test("workspace.mode parent with declared repositories is valid", () => {
380
+ const { errors } = validatePreferences({
381
+ workspace: {
382
+ mode: "parent",
383
+ repositories: {
384
+ frontend: { path: "frontend" },
385
+ },
386
+ },
387
+ });
388
+
389
+ assert.equal(errors.length, 0);
390
+ });
391
+
392
+ test("mode:team + workspace.mode:parent warns about root-only push/PR", () => {
393
+ const { warnings } = validatePreferences({
394
+ mode: "team",
395
+ workspace: {
396
+ mode: "parent",
397
+ repositories: {
398
+ frontend: { path: "frontend" },
399
+ },
400
+ },
401
+ });
402
+ const crossAxisWarnings = warnings.filter((w) => w.includes("mode:team + workspace.mode:parent"));
403
+
404
+ assert.ok(
405
+ crossAxisWarnings.some((w) =>
406
+ w.includes("ADR-044"),
407
+ ),
408
+ "expected a cross-axis warning naming ADR-044",
409
+ );
410
+ assert.equal(crossAxisWarnings.length, 1, "expected exactly one cross-axis warning");
411
+ });
412
+
413
+ test("mode:team + workspace.mode:parent warns after global/project merge", (t) => {
414
+ const originalCwd = process.cwd();
415
+ const originalGsdHome = process.env.GSD_HOME;
416
+ const tempProject = mkdtempSync(join(tmpdir(), "gsd-prefs-cross-axis-project-"));
417
+ const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-prefs-cross-axis-home-"));
418
+
419
+ t.after(() => {
420
+ clearGSDPreferencesCache();
421
+ process.chdir(originalCwd);
422
+ if (originalGsdHome === undefined) delete process.env.GSD_HOME;
423
+ else process.env.GSD_HOME = originalGsdHome;
424
+ rmSync(tempProject, { recursive: true, force: true });
425
+ rmSync(tempGsdHome, { recursive: true, force: true });
426
+ });
427
+
428
+ mkdirSync(join(tempProject, ".gsd"), { recursive: true });
429
+ process.env.GSD_HOME = tempGsdHome;
430
+ process.chdir(tempProject);
431
+ clearGSDPreferencesCache();
432
+
433
+ writeFileSync(getGlobalGSDPreferencesPath(), "---\nversion: 1\nmode: team\n---\n", "utf-8");
434
+ writeFileSync(
435
+ getProjectGSDPreferencesPath(tempProject),
436
+ [
437
+ "---",
438
+ "version: 1",
439
+ "workspace:",
440
+ " mode: parent",
441
+ " repositories:",
442
+ " frontend:",
443
+ " path: frontend",
444
+ "---",
445
+ "",
446
+ ].join("\n"),
447
+ "utf-8",
448
+ );
449
+
450
+ const rawGlobalWarnings = loadGlobalGSDPreferences()?.warnings ?? [];
451
+ const rawProjectWarnings = loadProjectGSDPreferences(tempProject)?.warnings ?? [];
452
+ assert.equal(rawGlobalWarnings.filter((w) => w.includes("workspace.mode:parent")).length, 0);
453
+ assert.equal(rawProjectWarnings.filter((w) => w.includes("mode:team + workspace.mode:parent")).length, 0);
454
+
455
+ const loaded = loadEffectiveGSDPreferences(tempProject);
456
+ assert.equal(loaded?.preferences.mode, "team");
457
+ assert.equal(loaded?.preferences.workspace?.mode, "parent");
458
+ assert.equal(
459
+ (loaded?.warnings ?? []).filter((w) => w.includes("mode:team + workspace.mode:parent")).length,
460
+ 1,
461
+ );
462
+
463
+ const diagnostics = collectPreferenceDiagnostics(tempProject).filter((diagnostic) =>
464
+ diagnostic.message.includes("mode:team + workspace.mode:parent"),
465
+ );
466
+ assert.equal(diagnostics.length, 1);
467
+ assert.equal(diagnostics[0]?.scope, "project");
468
+ });
469
+
470
+ test("mode:team without workspace.mode:parent does not warn about push", () => {
471
+ const { warnings } = validatePreferences({
472
+ mode: "team",
473
+ });
474
+
475
+ assert.ok(
476
+ !warnings.some((w) => w.includes("workspace.mode:parent")),
477
+ "team mode alone must not trigger the parent-workspace push warning",
478
+ );
479
+ });
330
480
 
331
481
  test("workspace is a recognized preference key (no unknown warning)", () => {
332
482
  const { warnings } = validatePreferences({
@@ -182,6 +182,70 @@ test("prefs wizard save path preserves every known preference key", async () =>
182
182
  }
183
183
  });
184
184
 
185
+ test("verification wizard prompts for per-unit cost cap and persists it (regression #1121)", async () => {
186
+ // Regression for #1121: the `/gsd prefs` Verification category never prompted for
187
+ // `per_unit_cost_cap_usd`, so the per-unit cost cap could only be set by hand-editing
188
+ // PREFERENCES.md. Drive the wizard into Verification, answer the cap prompt, and assert
189
+ // the prompt exists and the entered value is serialized + round-trips back as a number.
190
+ const dir = mkdtempSync(join(tmpdir(), "gsd-prefs-wizard-"));
191
+ const prefsPath = join(dir, "PREFERENCES.md");
192
+
193
+ const inputLabels: string[] = [];
194
+ let topMenuVisits = 0;
195
+
196
+ const ctx = {
197
+ ui: {
198
+ notify() {},
199
+ select: async (label: string, options: string[]) => {
200
+ if (label === "GSD Preferences") {
201
+ topMenuVisits += 1;
202
+ if (topMenuVisits === 1) {
203
+ const verification = options.find((o) => o.startsWith("Verification"));
204
+ assert.ok(verification, "wizard menu must offer a Verification category");
205
+ return verification;
206
+ }
207
+ return "── Save & Exit ──";
208
+ }
209
+ // verification_commands string-list sub-menu — leave it untouched
210
+ if (options.includes("Done")) return "Done";
211
+ // boolean/enum prompts within the category — keep the current value
212
+ if (options.includes("(keep current)")) return "(keep current)";
213
+ return options[options.length - 1];
214
+ },
215
+ input: async (label: string) => {
216
+ inputLabels.push(label);
217
+ if (label.includes("Per-unit cost cap")) return "12.5";
218
+ return null; // escape every other numeric/string prompt (no change)
219
+ },
220
+ },
221
+ waitForIdle: async () => {},
222
+ reload: async () => {},
223
+ } as any;
224
+
225
+ try {
226
+ await handlePrefsWizard(ctx, "project", {}, { pathOverride: prefsPath });
227
+
228
+ // The Verification wizard must actually ask for the per-unit cost cap.
229
+ assert.ok(
230
+ inputLabels.some((l) => l.includes("Per-unit cost cap")),
231
+ `Verification wizard must prompt for the per-unit cost cap; prompts seen:\n${inputLabels.join("\n")}`,
232
+ );
233
+
234
+ // The entered value must be serialized to PREFERENCES.md...
235
+ const saved = readFileSync(prefsPath, "utf-8");
236
+ assert.match(saved, /per_unit_cost_cap_usd:\s*12\.5(\s|$)/m);
237
+
238
+ // ...and round-trip back through the parser/validator as a number.
239
+ const parsed = parsePreferencesMarkdown(saved);
240
+ assert.notEqual(parsed, null);
241
+ const { errors, preferences } = validatePreferences(parsed!);
242
+ assert.deepEqual(errors, []);
243
+ assert.equal(preferences.per_unit_cost_cap_usd, 12.5);
244
+ } finally {
245
+ rmSync(dir, { recursive: true, force: true });
246
+ }
247
+ });
248
+
185
249
  test("category summaries expose the wizard menu surface for configured prefs", () => {
186
250
  const summaries = buildCategorySummaries(PREF_SAMPLE_VALUES);
187
251
  assert.deepEqual(
@@ -203,11 +267,14 @@ test("category summaries expose the wizard menu surface for configured prefs", (
203
267
  "timeouts",
204
268
  "uok",
205
269
  "verification",
270
+ "workspace",
206
271
  ],
207
272
  );
208
273
  assert.match(summaries.models, /phase/);
209
274
  assert.match(summaries.integrations, /remote: C123/);
210
275
  assert.match(summaries.verification, /1 cmd/);
276
+ assert.match(summaries.workspace, /mode: parent/);
277
+ assert.match(summaries.workspace, /1 repo/);
211
278
  });
212
279
 
213
280
  test("models wizard offers discovered models for enabled providers", async () => {
@@ -276,3 +343,78 @@ test("models wizard offers discovered models for enabled providers", async () =>
276
343
  rmSync(dir, { recursive: true, force: true });
277
344
  }
278
345
  });
346
+
347
+ test("workspace wizard category configures parent mode with a declared repository", async () => {
348
+ const dir = mkdtempSync(join(tmpdir(), "gsd-prefs-wizard-workspace-"));
349
+ const prefsPath = join(dir, "PREFERENCES.md");
350
+ try {
351
+ // Drive: Workspace → parent mode → Add repository (id=frontend, path=frontend,
352
+ // role=ui, verification=npm test, commit policy auto) → Done → Save & Exit.
353
+ const selects = [
354
+ "Workspace (single-repo)", // pick the Workspace category
355
+ "parent", // promptEnum: workspace mode
356
+ "Add repository", // sub-menu
357
+ "(keep current)", // promptEnum: commit policy -> keep auto default
358
+ "Done", // exit repo sub-menu
359
+ "── Save & Exit ──", // exit wizard
360
+ ];
361
+ const inputs = [
362
+ "frontend", // repository id
363
+ "frontend", // repository path
364
+ "ui", // role
365
+ "npm test", // verification commands
366
+ ];
367
+ const ctx = {
368
+ ui: {
369
+ notify() {},
370
+ select: async () => selects.shift(),
371
+ input: async () => inputs.shift(),
372
+ },
373
+ waitForIdle: async () => {},
374
+ reload: async () => {},
375
+ } as any;
376
+
377
+ await handlePrefsWizard(ctx, "project", {}, { pathOverride: prefsPath });
378
+
379
+ const saved = readFileSync(prefsPath, "utf-8");
380
+ assert.match(saved, /mode: parent/);
381
+ assert.match(saved, /frontend:/);
382
+ assert.match(saved, /path: frontend/);
383
+ assert.match(saved, /role: ui/);
384
+ assert.match(saved, /npm test/);
385
+ assert.equal(selects.length, 0, "all queued selects should be consumed");
386
+ assert.equal(inputs.length, 0, "all queued inputs should be consumed");
387
+ } finally {
388
+ rmSync(dir, { recursive: true, force: true });
389
+ }
390
+ });
391
+
392
+ test("workspace wizard does not save parent mode when no repository is declared", async () => {
393
+ const dir = mkdtempSync(join(tmpdir(), "gsd-prefs-wizard-workspace-empty-"));
394
+ const prefsPath = join(dir, "PREFERENCES.md");
395
+ try {
396
+ // parent mode with no repos added → should warn and not persist workspace.
397
+ const selects = [
398
+ "Workspace (single-repo)",
399
+ "parent",
400
+ "Done", // add nothing
401
+ "── Save & Exit ──",
402
+ ];
403
+ const ctx = {
404
+ ui: {
405
+ notify() {},
406
+ select: async () => selects.shift(),
407
+ input: async () => "",
408
+ },
409
+ waitForIdle: async () => {},
410
+ reload: async () => {},
411
+ } as any;
412
+
413
+ await handlePrefsWizard(ctx, "project", {}, { pathOverride: prefsPath });
414
+
415
+ const saved = readFileSync(prefsPath, "utf-8");
416
+ assert.doesNotMatch(saved, /mode: parent/, "parent mode must not persist without a declared repo");
417
+ } finally {
418
+ rmSync(dir, { recursive: true, force: true });
419
+ }
420
+ });
@@ -227,7 +227,7 @@ test("plan-slice verify-fail logs a recovery warning when the tasks dir is missi
227
227
  }
228
228
  });
229
229
 
230
- test("plan-slice verify-fail logs a recovery warning when an individual task plan file is missing", () => {
230
+ test("plan-slice verify-fail logs a recovery warning when an individual task artifact is missing", () => {
231
231
  const base = createFixtureBase("gsd-recovery-logs-taskplan-");
232
232
  try {
233
233
  const dir = sliceDir(base, "M001", "S01");
@@ -236,17 +236,17 @@ test("plan-slice verify-fail logs a recovery warning when an individual task pla
236
236
  "# S01: Has task\n\n## Tasks\n\n- [ ] **T01: A** `est:15m`\n",
237
237
  "utf-8",
238
238
  );
239
- // Create the tasks dir but NOT the T01-PLAN.md file inside it.
239
+ // Create the tasks dir but NOT any T01 task artifact inside it.
240
240
  mkdirSync(join(dir, "tasks"), { recursive: true });
241
241
 
242
242
  const { result, logs } = verifyAndCaptureLogs("plan-slice", "M001/S01", base);
243
243
 
244
- assert.equal(result, false, "a missing task plan file must fail verification");
244
+ assert.equal(result, false, "a missing task artifact must fail verification");
245
245
  const recovery = findRecovery(logs);
246
246
  assert.ok(recovery, "a recovery warning must be logged");
247
247
  assert.match(
248
248
  recovery!.message,
249
- /verify-fail plan-slice M001\/S01: task plan missing .*T01-PLAN\.md/u,
249
+ /verify-fail plan-slice M001\/S01: task artifact missing for T01/u,
250
250
  );
251
251
  } finally {
252
252
  rmSync(base, { recursive: true, force: true });