@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
@@ -13,6 +13,7 @@ import { resolveManifest } from "../unit-context-manifest.js";
13
13
  import { isBlockedStateFile, isBashWriteToStateFile, BLOCKED_WRITE_ERROR } from "../write-intercept.js";
14
14
  import { loadFile, saveFile, formatContinue } from "../files.js";
15
15
  import { clearAutoCompletionStopInProgress, clearToolInvocationError, getAutoRuntimeSnapshot, getSourceObservationStore, isAutoActive, isAutoCompletionStopInProgress, isAutoPaused, isInteractiveElicitationInFlight, markToolEnd, markToolStart, recordAutoToolSurfaceSnapshot, recordToolInvocationError, } from "../auto-runtime-state.js";
16
+ import { isDeterministicPolicyError, isQueuedUserMessageSkip, isToolInvocationError, isToolUnavailableError, } from "../auto-tool-tracking.js";
16
17
  import { applyProviderPayloadPolicy } from "../provider-payload-policy.js";
17
18
  import { checkToolCallLoop, recordToolCallLoopMutation, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
18
19
  import { MINIMAL_AUTO_BASE_TOOL_NAMES } from "./core-session-tools.js";
@@ -44,7 +45,14 @@ import { clearPendingAutoStart } from "../pending-auto-start.js";
44
45
  import { resolveWorkflowToolBasePath } from "./dynamic-tools.js";
45
46
  import { getRequiredWorkflowToolsForUnit } from "../unit-tool-contracts.js";
46
47
  import { flushAllManifests } from "../workflow-manifest.js";
48
+ import { recordUnitHarnessAbort } from "../unit-runtime.js";
47
49
  let approvalQuestionAbortInFlight = false;
50
+ function recordCurrentUnitHarnessAbort(abort) {
51
+ const dash = getAutoRuntimeSnapshot();
52
+ if (!dash.active || !dash.basePath || !dash.currentUnit)
53
+ return;
54
+ recordUnitHarnessAbort(dash.basePath, dash.currentUnit.type, dash.currentUnit.id, dash.currentUnit.startedAt, abort);
55
+ }
48
56
  async function loadWelcomeScreenModule() {
49
57
  const candidates = [];
50
58
  const gsdBinPath = process.env.GSD_BIN_PATH;
@@ -160,9 +168,23 @@ const WORKFLOW_GSD_TOOL_NAMES = [
160
168
  ...MINIMAL_GSD_TOOL_NAMES,
161
169
  ...Object.values(AUTO_UNIT_SCOPED_TOOLS).flat(),
162
170
  ].filter(isGsdManagedTool);
171
+ const WORKFLOW_ONLY_AUTO_BASE_TOOL_NAMES = [
172
+ "find",
173
+ "glob",
174
+ "grep",
175
+ "ls",
176
+ "read",
177
+ "subagent",
178
+ ];
163
179
  function isGsdManagedTool(name) {
164
180
  return name.startsWith("gsd_") || name === "memory_query" || name === "capture_thought" || name === "gsd_graph";
165
181
  }
182
+ function autoBaseToolNamesForUnit(unitType) {
183
+ const manifest = unitType ? resolveManifest(unitType) : null;
184
+ return manifest?.tools.mode === "workflow-only"
185
+ ? WORKFLOW_ONLY_AUTO_BASE_TOOL_NAMES
186
+ : MINIMAL_AUTO_BASE_TOOL_NAMES;
187
+ }
166
188
  /**
167
189
  * Resolves requested tool names against active tools using exact and MCP-scoped matches.
168
190
  *
@@ -213,7 +235,7 @@ export function buildMinimalAutoGsdToolSet(activeToolNames, unitType, registered
213
235
  return buildRunUatGsdToolSet(activeToolNames, registeredToolNames);
214
236
  }
215
237
  const unitTools = unitType ? AUTO_UNIT_SCOPED_TOOLS[unitType] ?? [] : [];
216
- const autoBaseTools = new Set(MINIMAL_AUTO_BASE_TOOL_NAMES);
238
+ const autoBaseTools = new Set(autoBaseToolNamesForUnit(unitType));
217
239
  const availableBaseTools = registeredToolNames.filter((name) => autoBaseTools.has(name));
218
240
  const preserved = [...new Set([
219
241
  ...activeToolNames.filter((name) => autoBaseTools.has(name)),
@@ -446,6 +468,75 @@ function formatLoopGuardBlockReason(reason) {
446
468
  return reason;
447
469
  return LOOP_GUARD_INTERACTIVE_INSTRUCTIONS.reduce((formatted, instruction) => formatted.replace(instruction, LOOP_GUARD_AUTO_INSTRUCTION), reason);
448
470
  }
471
+ function isGateResultPersistenceTool(toolName) {
472
+ return toolName === "gsd_save_gate_result" || toolName === "gsd_uat_result_save";
473
+ }
474
+ function resultDetails(result) {
475
+ if (!result || typeof result !== "object")
476
+ return {};
477
+ const details = result.details;
478
+ return details && typeof details === "object" ? details : {};
479
+ }
480
+ function isAbortedExecutionToolResult(toolName, result) {
481
+ if (toolName !== "gsd_exec" && toolName !== "gsd_uat_exec")
482
+ return false;
483
+ const details = resultDetails(result);
484
+ return details.aborted === true || details.force_resolved === true || details.timed_out === true;
485
+ }
486
+ function isRetryableHarnessToolError(toolName, result, errorText) {
487
+ if (isGateResultPersistenceTool(toolName))
488
+ return false;
489
+ if (isDeterministicPolicyError(errorText))
490
+ return false;
491
+ if (toolName === "gsd_exec" || toolName === "gsd_uat_exec") {
492
+ if (isAbortedExecutionToolResult(toolName, result))
493
+ return true;
494
+ if (isToolUnavailableError(errorText))
495
+ return true;
496
+ if (isQueuedUserMessageSkip(errorText))
497
+ return true;
498
+ return isToolInvocationError(errorText);
499
+ }
500
+ if (isToolUnavailableError(errorText))
501
+ return true;
502
+ if (isQueuedUserMessageSkip(errorText))
503
+ return true;
504
+ return false;
505
+ }
506
+ function recordRetryableHarnessToolError(toolName, result, errorText) {
507
+ if (!isRetryableHarnessToolError(toolName, result, errorText))
508
+ return;
509
+ recordCurrentUnitHarnessAbort({
510
+ kind: "tool-error",
511
+ reason: errorText || "Tool execution failed before the unit could complete its gate evaluation.",
512
+ toolName,
513
+ });
514
+ }
515
+ function recordRetryableTurnAbort(event) {
516
+ const origin = typeof event.abortOrigin === "string" ? event.abortOrigin : undefined;
517
+ if (origin === "session-transition")
518
+ return;
519
+ const messages = Array.isArray(event.messages) ? event.messages : [];
520
+ const lastMsg = messages[messages.length - 1];
521
+ const stopReason = lastMsg && typeof lastMsg === "object"
522
+ ? lastMsg.stopReason
523
+ : undefined;
524
+ if (stopReason !== "aborted" && stopReason !== "error")
525
+ return;
526
+ const errorMessage = lastMsg && typeof lastMsg === "object"
527
+ ? lastMsg.errorMessage
528
+ : undefined;
529
+ const reason = [
530
+ "Agent turn aborted before the unit could complete its gate evaluation.",
531
+ origin ? `origin=${origin}` : undefined,
532
+ typeof stopReason === "string" ? `stopReason=${stopReason}` : undefined,
533
+ typeof errorMessage === "string" && errorMessage.trim() ? `error=${errorMessage.trim()}` : undefined,
534
+ ].filter(Boolean).join(" ");
535
+ recordCurrentUnitHarnessAbort({
536
+ kind: "turn-abort",
537
+ reason,
538
+ });
539
+ }
449
540
  function beginSourceObservationStoreForCurrentUnit(ctx) {
450
541
  if (!isAutoActive())
451
542
  return null;
@@ -696,8 +787,8 @@ export function registerHooks(pi, ecosystemHandlers) {
696
787
  await applyCompactionThresholdOverride(ctx);
697
788
  await prepareWorkflowMcpForHookContext(ctx, basePath);
698
789
  // Migrate legacy .gsd/milestones/ to flat-phase .gsd/phases/ when detected.
699
- // Best-effort never blocks session startup. Skipped inside auto-worktrees
700
- // (migration already ran on the project root before the worktree was created).
790
+ // Fail closed on migration errors: resolvers assume flat-phase paths after
791
+ // startup, so continuing with nested disk state corrupts later checks.
701
792
  try {
702
793
  const { isInAutoWorktree } = await import("../auto-worktree.js");
703
794
  if (!isInAutoWorktree(basePath)) {
@@ -710,10 +801,18 @@ export function registerHooks(pi, ecosystemHandlers) {
710
801
  await migrateToFlatPhase(basePath);
711
802
  }
712
803
  else {
713
- safetyLogWarning("bootstrap", "flat-phase migration deferred: legacy .gsd/milestones/ layout detected but the workflow database could not be opened — will retry on next session");
804
+ safetyLogWarning("bootstrap", "flat-phase migration required: legacy .gsd/milestones/ layout detected but the workflow database could not be opened — fix database access before starting GSD");
805
+ throw new Error("flat-phase migration required but the workflow database could not be opened; fix database access before starting GSD");
714
806
  }
715
807
  }
716
808
  }
809
+ }
810
+ catch (err) {
811
+ const message = err instanceof Error ? err.message : String(err);
812
+ safetyLogWarning("bootstrap", `flat-phase migration failed: ${message}`);
813
+ throw new Error(`flat-phase migration failed: ${message}`);
814
+ }
815
+ try {
717
816
  const projectRoot = resolveWorktreeProjectRoot(basePath);
718
817
  const { pruneStaleFlatPhaseBackups } = await import("../flat-phase-migration.js");
719
818
  const pruned = pruneStaleFlatPhaseBackups(projectRoot);
@@ -722,7 +821,7 @@ export function registerHooks(pi, ecosystemHandlers) {
722
821
  }
723
822
  }
724
823
  catch (err) {
725
- safetyLogWarning("bootstrap", `flat-phase migration: ${err instanceof Error ? err.message : String(err)}`);
824
+ safetyLogWarning("bootstrap", `flat-phase backup pruning: ${err instanceof Error ? err.message : String(err)}`);
726
825
  }
727
826
  // Apply show_token_cost preference (#1515)
728
827
  try {
@@ -871,6 +970,7 @@ export function registerHooks(pi, ecosystemHandlers) {
871
970
  });
872
971
  pi.on("agent_end", async (event, ctx) => {
873
972
  approvalQuestionAbortInFlight = false;
973
+ recordRetryableTurnAbort(event);
874
974
  resetToolCallLoopGuard();
875
975
  resetPendingGatePauseGuard();
876
976
  await resetAskUserQuestionsTurnCache();
@@ -1077,6 +1177,12 @@ export function registerHooks(pi, ecosystemHandlers) {
1077
1177
  // ── Loop guard: block repeated identical tool calls ──
1078
1178
  const loopCheck = checkToolCallLoop(toolName, event.input);
1079
1179
  if (loopCheck.block) {
1180
+ recordCurrentUnitHarnessAbort({
1181
+ kind: "tool-loop-guard",
1182
+ reason: loopCheck.reason ?? "Tool-call loop guard blocked a repeated tool call.",
1183
+ toolName,
1184
+ count: loopCheck.count,
1185
+ });
1080
1186
  return { block: true, reason: formatLoopGuardBlockReason(loopCheck.reason) };
1081
1187
  }
1082
1188
  const deferredGateGuard = shouldBlockDeferredApprovalTool(toolName, event.input, discussionBasePath);
@@ -1299,6 +1405,7 @@ export function registerHooks(pi, ecosystemHandlers) {
1299
1405
  // Let recordToolInvocationError classify the failure so non-gsd_ harness
1300
1406
  // errors and deterministic policy rejections are handled consistently.
1301
1407
  recordToolInvocationError(event.toolName, errorText);
1408
+ recordRetryableHarnessToolError(toolName, resultPayload, errorText);
1302
1409
  }
1303
1410
  else if (isAutoActive()) {
1304
1411
  clearToolInvocationError();
@@ -1466,6 +1573,7 @@ export function registerHooks(pi, ecosystemHandlers) {
1466
1573
  // for every finalized tool call on every engine, so error classification
1467
1574
  // and evidence persistence here cover external engines that skip tool_result.
1468
1575
  pi.on("tool_execution_end", async (event) => {
1576
+ const toolName = canonicalToolName(event.toolName);
1469
1577
  markToolEnd(event.toolCallId);
1470
1578
  // #2883/#4974: Capture deterministic invocation/policy errors
1471
1579
  // so postUnitPreVerification can break the retry loop instead of re-dispatching.
@@ -1476,6 +1584,7 @@ export function registerHooks(pi, ecosystemHandlers) {
1476
1584
  // Let recordToolInvocationError classify the failure so non-gsd_ harness
1477
1585
  // errors and deterministic policy rejections are handled consistently.
1478
1586
  recordToolInvocationError(event.toolName, errorText);
1587
+ recordRetryableHarnessToolError(toolName, event.result, errorText);
1479
1588
  }
1480
1589
  else if (isAutoActive()) {
1481
1590
  clearToolInvocationError();
@@ -3,19 +3,63 @@
3
3
  import { Type } from "@sinclair/typebox";
4
4
  import { getAutoRuntimeSnapshot } from "../auto-runtime-state.js";
5
5
  import { scheduleAutoWakeup } from "../auto/schedule-wakeup.js";
6
+ import { logWarning } from "../workflow-logger.js";
6
7
  import { resolveCtxCwd } from "./dynamic-tools.js";
7
8
  const MAX_WAKEUP_DELAY_SECONDS = 24 * 60 * 60;
9
+ const INTERACTIVE_WAKEUP_CUSTOM_TYPE = "gsd-schedule-wakeup";
10
+ // One pending interactive wakeup per session, keyed by base path — the same
11
+ // scoping auto-mode uses for its wakeup map (see `wakeupKey`). Re-arming cancels
12
+ // only that session's prior timer, so repeated polling never stacks overlapping
13
+ // wakeups, and concurrent projects in one host process don't cancel each other.
14
+ const pendingInteractiveWakeups = new Map();
15
+ function scheduleInteractiveWakeup(pi, key, delaySeconds, prompt, reason) {
16
+ const existing = pendingInteractiveWakeups.get(key);
17
+ if (existing)
18
+ clearTimeout(existing);
19
+ const handle = setTimeout(() => {
20
+ if (pendingInteractiveWakeups.get(key) === handle) {
21
+ pendingInteractiveWakeups.delete(key);
22
+ }
23
+ try {
24
+ void Promise.resolve(pi.sendMessage({
25
+ customType: INTERACTIVE_WAKEUP_CUSTOM_TYPE,
26
+ content: prompt,
27
+ display: true,
28
+ details: { delaySeconds, reason },
29
+ }, { triggerTurn: true })).catch((error) => {
30
+ logWarning("bootstrap", `ScheduleWakeup interactive dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
31
+ });
32
+ }
33
+ catch (error) {
34
+ logWarning("bootstrap", `ScheduleWakeup interactive dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
35
+ }
36
+ }, delaySeconds * 1000);
37
+ pendingInteractiveWakeups.set(key, handle);
38
+ if (typeof handle === "object" &&
39
+ handle !== null &&
40
+ "unref" in handle &&
41
+ typeof handle.unref === "function") {
42
+ handle.unref();
43
+ }
44
+ }
45
+ export function _resetInteractiveWakeupsForTest() {
46
+ for (const handle of pendingInteractiveWakeups.values()) {
47
+ clearTimeout(handle);
48
+ }
49
+ pendingInteractiveWakeups.clear();
50
+ }
8
51
  export function registerScheduleWakeupTool(pi) {
9
52
  pi.registerTool({
10
53
  name: "ScheduleWakeup",
11
54
  label: "Schedule Wakeup",
12
- description: "In GSD auto-mode, pause the current unit and continue the same session after a delay. " +
13
- "Use this for long external processes that need polling without ending the unit as no-artifact.",
14
- promptSnippet: "Schedule a same-session auto-mode wakeup after a delay.",
55
+ description: "Schedule a delayed continuation turn. In GSD auto-mode, continue the current unit in the same session; " +
56
+ "outside auto-mode, start a new triggered turn with the supplied wakeup prompt.",
57
+ promptSnippet: "Schedule a wakeup prompt after a delay.",
15
58
  promptGuidelines: [
16
59
  "Use ScheduleWakeup at the end of an execute-task turn when waiting for a long external process.",
17
60
  "Include a prompt that says exactly what external state to check next and what artifact to write when done.",
18
61
  "Re-arm ScheduleWakeup on each polling turn if the external process is still running.",
62
+ "Outside auto-mode, use ScheduleWakeup when the user asks you to check back or poll later.",
19
63
  ],
20
64
  parameters: Type.Object({
21
65
  delaySeconds: Type.Number({
@@ -34,22 +78,27 @@ export function registerScheduleWakeupTool(pi) {
34
78
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
35
79
  const dash = getAutoRuntimeSnapshot();
36
80
  const currentUnit = dash.currentUnit;
37
- const basePath = dash.basePath || resolveCtxCwd(ctx);
81
+ const delaySeconds = Math.max(1, Math.min(MAX_WAKEUP_DELAY_SECONDS, Math.floor(params.delaySeconds)));
82
+ const reason = params.reason ?? "";
38
83
  if (!dash.active || !currentUnit) {
84
+ const interactiveKey = dash.basePath || resolveCtxCwd(ctx);
85
+ scheduleInteractiveWakeup(pi, interactiveKey, delaySeconds, params.prompt, reason);
39
86
  return {
40
- content: [{ type: "text", text: "ScheduleWakeup is only available during an active GSD auto-mode unit." }],
41
- details: { operation: "schedule_wakeup", error: "auto_mode_inactive" },
42
- isError: true,
87
+ content: [{
88
+ type: "text",
89
+ text: `Wakeup scheduled for ${delaySeconds}s. GSD will start a new turn with the wakeup prompt.`,
90
+ }],
91
+ details: { operation: "schedule_wakeup", delaySeconds, mode: "interactive" },
43
92
  };
44
93
  }
45
- const delaySeconds = Math.max(1, Math.min(MAX_WAKEUP_DELAY_SECONDS, Math.floor(params.delaySeconds)));
94
+ const basePath = dash.basePath || resolveCtxCwd(ctx);
46
95
  scheduleAutoWakeup({
47
96
  basePath,
48
97
  unitType: currentUnit.type,
49
98
  unitId: currentUnit.id,
50
99
  delayMs: delaySeconds * 1000,
51
100
  prompt: params.prompt,
52
- reason: params.reason ?? "",
101
+ reason,
53
102
  createdAt: Date.now(),
54
103
  });
55
104
  return {
@@ -24,6 +24,8 @@
24
24
  */
25
25
  import { createHash } from "node:crypto";
26
26
  import { INHERENTLY_REPEATABLE_TOOL_SET } from "./core-session-tools.js";
27
+ import { hasBrowserContractPrefix } from "../../shared/browser-contract.js";
28
+ import { canonicalToolName } from "../engine-hook-contract.js";
27
29
  const MAX_CONSECUTIVE_IDENTICAL_CALLS = 4;
28
30
  /** Interactive/user-facing tools where even 1 duplicate is confusing. */
29
31
  const STRICT_LOOP_TOOLS = new Set(["ask_user_questions"]);
@@ -111,8 +113,14 @@ export function checkToolCallLoop(toolName, args) {
111
113
  perToolCounts.set(toolName, perToolCount);
112
114
  perToolLastMutationEpoch.set(toolName, mutationEpoch);
113
115
  // Read-only navigation tools are normal context gathering; Guard 1 still
114
- // catches true reread loops with identical arguments.
115
- if (PER_TOOL_CAP_EXEMPT_TOOLS.has(toolName)) {
116
+ // catches true reread loops with identical arguments. Browser Automation
117
+ // Contract tools (browser_*) are the same shape: a browser-backed UAT makes
118
+ // many distinct-arg calls (read a message, count edits, inspect a card, …),
119
+ // so the arg-independent per-tool cap misfires on legitimate verification;
120
+ // Guard 1's identical-signature streak still catches a genuinely stuck
121
+ // browser loop.
122
+ if (PER_TOOL_CAP_EXEMPT_TOOLS.has(toolName) ||
123
+ hasBrowserContractPrefix(canonicalToolName(toolName))) {
116
124
  return { block: false, count: consecutiveCount };
117
125
  }
118
126
  const perToolCap = INHERENTLY_REPEATABLE_TOOL_SET.has(toolName)
@@ -947,7 +947,7 @@ function allowedPlanningDispatchAgentsList() {
947
947
  return [...ALLOWED_PLANNING_DISPATCH_AGENTS].join(", ");
948
948
  }
949
949
  function allowsControlledSubagentDispatch(policy) {
950
- return ((policy.mode === "planning-dispatch" || policy.mode === "verification") &&
950
+ return ((policy.mode === "planning-dispatch" || policy.mode === "verification" || policy.mode === "workflow-only") &&
951
951
  Array.isArray(policy.allowedSubagents));
952
952
  }
953
953
  function warnMissingControlledDispatchAgentClasses(unitType, mode, toolName) {
@@ -975,6 +975,7 @@ function warnMissingControlledDispatchAgentClasses(unitType, mode, toolName) {
975
975
  const PLANNING_SAFE_TOOLS = new Set([
976
976
  "read", "grep", "find", "ls", "glob",
977
977
  "ask_user_questions",
978
+ "memory_query",
978
979
  "search-the-web", "resolve_library", "get_library_docs", "fetch_page",
979
980
  "search_and_read",
980
981
  ]);
@@ -1031,6 +1032,9 @@ function planningBlock(unitType, mode, what) {
1031
1032
  * → allows Bash for project verification commands, keeps
1032
1033
  * writes restricted to .gsd/, and permits subagent dispatch
1033
1034
  * only when the manifest declares allowedSubagents.
1035
+ * - "workflow-only"
1036
+ * → allows reads, GSD workflow tools, and declared read-only
1037
+ * subagents; blocks generic Bash and direct file writes.
1034
1038
  *
1035
1039
  * `pathOrCommand` is the file path for write/edit-shaped tools and the
1036
1040
  * shell command for bash. Other tools ignore this argument.
@@ -1101,6 +1105,15 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
1101
1105
  }
1102
1106
  return planningBlock(unitType, policy.mode, "subagent dispatch is not permitted in planning units");
1103
1107
  }
1108
+ if (policy.mode === "workflow-only") {
1109
+ if (tool === "bash") {
1110
+ return planningBlock(unitType, policy.mode, `bash is not permitted; use the unit's dedicated GSD workflow tools for durable validation output`);
1111
+ }
1112
+ if (PLANNING_WRITE_TOOLS.has(tool)) {
1113
+ return planningBlock(unitType, policy.mode, `cannot ${tool} "${pathOrCommand}" — direct artifact writes are disabled; call the required GSD workflow tool instead`);
1114
+ }
1115
+ return planningBlock(unitType, policy.mode, `tool "${tool}" is not on the workflow-only allowlist`);
1116
+ }
1104
1117
  if (tool === "bash") {
1105
1118
  if (policy.mode === "verification") {
1106
1119
  if (BASH_VERIFICATION_RE.test(pathOrCommand) || BASH_READ_ONLY_RE.test(pathOrCommand))
@@ -9,9 +9,11 @@
9
9
  */
10
10
  import { createHash } from "node:crypto";
11
11
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
12
- import { join, dirname, extname } from "node:path";
12
+ import { join, dirname, extname, relative, sep } from "node:path";
13
13
  import { execSync } from "node:child_process";
14
14
  import { gsdRoot } from "./paths.js";
15
+ import { createRepositoryRegistryFromPreferences, } from "./repository-registry.js";
16
+ import { loadEffectiveGSDPreferences } from "./preferences.js";
15
17
  // ─── Defaults ────────────────────────────────────────────────────────────────
16
18
  const DEFAULT_EXCLUDES = [
17
19
  // ── AI / tooling meta ──
@@ -144,6 +146,84 @@ function enumerateFiles(basePath, excludes, maxFiles) {
144
146
  const truncated = filtered.length > maxFiles;
145
147
  return { files: truncated ? filtered.slice(0, maxFiles) : filtered, truncated };
146
148
  }
149
+ /**
150
+ * Build a workspace-aware registry for parent mode, or null when the project is
151
+ * single-repo (no declared child repositories). Returns null so the caller falls
152
+ * back to the legacy single-root enumeration and produces byte-identical output.
153
+ */
154
+ function loadWorkspaceRegistry(basePath) {
155
+ const preferences = loadEffectiveGSDPreferences(basePath)?.preferences;
156
+ const workspace = preferences?.workspace;
157
+ const mode = workspace?.mode ?? "project";
158
+ // Parent mode is only meaningful with at least one declared child repo; the
159
+ // implicit "project" repo is always present, so require a declared non-project id.
160
+ const hasDeclaredChildRepo = workspace?.repositories
161
+ ? Object.keys(workspace.repositories).some((id) => id !== "project")
162
+ : false;
163
+ if (mode !== "parent" || !hasDeclaredChildRepo) {
164
+ return null;
165
+ }
166
+ return createRepositoryRegistryFromPreferences(basePath, preferences);
167
+ }
168
+ /** Stable key fragment for workspace-aware enumeration inputs (mode + declared repos). */
169
+ function workspaceEnumerationSignature(basePath) {
170
+ const registry = loadWorkspaceRegistry(basePath);
171
+ if (!registry)
172
+ return "single-root";
173
+ return JSON.stringify({
174
+ mode: registry.mode,
175
+ repositories: registry.repositories
176
+ .map((repo) => ({ id: repo.id, root: repo.root }))
177
+ .sort((a, b) => a.id.localeCompare(b.id)),
178
+ });
179
+ }
180
+ /**
181
+ * Enumerate tracked files across every declared repository in a parent workspace.
182
+ * Child-repo paths are rewritten to be workspace-relative (e.g. `frontend/src/x`)
183
+ * and tagged with their declaring repo id so the map can render repo-labelled
184
+ * sections. The maxFiles cap is applied across the union of all repositories.
185
+ */
186
+ function enumerateWorkspaceFiles(registry, excludes, maxFiles) {
187
+ const perRepo = [];
188
+ const seen = new Set();
189
+ let totalEligible = 0;
190
+ for (const repo of registry.repositories) {
191
+ const rawFiles = lsFiles(repo.root);
192
+ const repoPrefix = relative(registry.projectRoot, repo.root).split(sep).join("/");
193
+ const repoEntries = [];
194
+ for (const file of rawFiles) {
195
+ // Rewrite before exclusion so workspace-relative patterns (e.g. frontend/…) apply.
196
+ const workspacePath = repoPrefix ? `${repoPrefix}/${file}` : file;
197
+ if (shouldExclude(workspacePath, excludes))
198
+ continue;
199
+ if (seen.has(workspacePath))
200
+ continue;
201
+ seen.add(workspacePath);
202
+ totalEligible++;
203
+ repoEntries.push({ workspacePath, repoId: repo.id });
204
+ }
205
+ perRepo.push(repoEntries);
206
+ }
207
+ const files = [];
208
+ const repos = [];
209
+ const indices = new Array(perRepo.length).fill(0);
210
+ while (files.length < maxFiles) {
211
+ let progressed = false;
212
+ for (let i = 0; i < perRepo.length; i++) {
213
+ if (indices[i] < perRepo[i].length) {
214
+ const entry = perRepo[i][indices[i]++];
215
+ files.push(entry.workspacePath);
216
+ repos.push(entry.repoId);
217
+ progressed = true;
218
+ if (files.length >= maxFiles)
219
+ break;
220
+ }
221
+ }
222
+ if (!progressed)
223
+ break;
224
+ }
225
+ return { files, repos, truncated: totalEligible > files.length };
226
+ }
147
227
  function resolveGeneratorOptions(options) {
148
228
  const excludes = [...DEFAULT_EXCLUDES, ...(options?.excludePatterns ?? [])];
149
229
  const maxFiles = options?.maxFiles ?? DEFAULT_MAX_FILES;
@@ -159,38 +239,69 @@ function resolveGeneratorOptions(options) {
159
239
  }),
160
240
  };
161
241
  }
162
- function computeCodebaseFingerprint(files, resolved, truncated) {
242
+ function computeCodebaseFingerprint(files, resolved, truncated, repos) {
163
243
  return createHash("sha1")
164
244
  .update(JSON.stringify({
165
245
  files,
246
+ ...(repos ? { repos } : {}),
166
247
  truncated,
167
248
  optionSignature: resolved.optionSignature,
168
249
  }))
169
250
  .digest("hex");
170
251
  }
171
252
  // ─── Grouping ────────────────────────────────────────────────────────────────
172
- function groupByDirectory(files, descriptions, collapseThreshold) {
253
+ function groupByDirectory(files, descriptions, collapseThreshold, repos) {
173
254
  const dirMap = new Map();
174
- for (const file of files) {
255
+ for (let i = 0; i < files.length; i++) {
256
+ const file = files[i];
175
257
  const dir = dirname(file);
176
- const dirKey = dir === "." ? "" : dir;
177
- if (!dirMap.has(dirKey)) {
178
- dirMap.set(dirKey, []);
258
+ const dirPath = dir === "." ? "" : dir;
259
+ const repoId = repos?.[i];
260
+ const groupKey = repoId !== undefined ? `${repoId}\0${dirPath}` : dirPath;
261
+ if (!dirMap.has(groupKey)) {
262
+ dirMap.set(groupKey, []);
179
263
  }
180
- dirMap.get(dirKey).push({
264
+ dirMap.get(groupKey).push({
181
265
  path: file,
182
266
  description: descriptions.get(file) ?? "",
267
+ repo: repoId,
183
268
  });
184
269
  }
185
270
  const groups = [];
186
- const sortedDirs = [...dirMap.keys()].sort();
187
- for (const dir of sortedDirs) {
188
- const dirFiles = dirMap.get(dir);
271
+ const repoOrder = new Map();
272
+ if (repos) {
273
+ for (let i = 0; i < repos.length; i++) {
274
+ if (!repoOrder.has(repos[i])) {
275
+ repoOrder.set(repos[i], repoOrder.size);
276
+ }
277
+ }
278
+ }
279
+ const sortedDirs = [...dirMap.keys()].sort((a, b) => {
280
+ // Single-repo (project) mode must stay byte-identical to the pre-workspace
281
+ // output, which ordered directories with a bare `.sort()` (UTF-16 code-unit
282
+ // order, so uppercase sorts before lowercase). localeCompare would diverge
283
+ // for mixed-case names (e.g. "Zoo" before "apple" flips), so preserve
284
+ // code-unit ordering here; only the repo-partitioning key is added on top.
285
+ if (!repos)
286
+ return a < b ? -1 : a > b ? 1 : 0;
287
+ const repoA = dirMap.get(a)[0]?.repo ?? "";
288
+ const repoB = dirMap.get(b)[0]?.repo ?? "";
289
+ const orderA = repoOrder.get(repoA) ?? 0;
290
+ const orderB = repoOrder.get(repoB) ?? 0;
291
+ if (orderA !== orderB)
292
+ return orderA - orderB;
293
+ return a.localeCompare(b);
294
+ });
295
+ for (const groupKey of sortedDirs) {
296
+ const dirFiles = dirMap.get(groupKey);
189
297
  dirFiles.sort((a, b) => a.path.localeCompare(b.path));
298
+ const dirPath = repos ? groupKey.slice(groupKey.indexOf("\0") + 1) : groupKey;
190
299
  groups.push({
191
- path: dir,
300
+ path: dirPath,
192
301
  files: dirFiles,
193
302
  collapsed: dirFiles.length > collapseThreshold,
303
+ // Each group is scoped to one repo in workspace-aware mode.
304
+ repo: dirFiles[0]?.repo,
194
305
  });
195
306
  }
196
307
  return groups;
@@ -207,7 +318,18 @@ function renderCodebaseMap(groups, totalFiles, truncated, metadata) {
207
318
  lines.push(`Note: Truncated to first ${totalFiles} files. Run with higher --max-files to include all.`);
208
319
  }
209
320
  lines.push("");
321
+ let emittedRepo;
210
322
  for (const group of groups) {
323
+ // In a workspace-aware map, partition directories under their declaring
324
+ // repository. `## [repo-id]` headings are used (not `###`) so they never
325
+ // collide with the per-directory `### <dir>/` headings or the
326
+ // `parseCodebaseMap` list-item regexes. Groups are sorted by declaring
327
+ // repo (enumeration order) then directory path so each repo is contiguous.
328
+ if (group.repo && group.repo !== emittedRepo) {
329
+ emittedRepo = group.repo;
330
+ lines.push(`## [${group.repo}]`);
331
+ lines.push("");
332
+ }
211
333
  const heading = group.path || "(root)";
212
334
  lines.push(`### ${heading}/`);
213
335
  if (group.collapsed) {
@@ -250,13 +372,15 @@ function renderCodebaseMap(groups, totalFiles, truncated, metadata) {
250
372
  function buildCodebaseMap(basePath, resolved, existingDescriptions, enumerated) {
251
373
  const listed = enumerated ?? enumerateFiles(basePath, resolved.excludes, resolved.maxFiles);
252
374
  const descriptions = existingDescriptions ?? new Map();
253
- const groups = groupByDirectory(listed.files, descriptions, resolved.collapseThreshold);
375
+ const groups = groupByDirectory(listed.files, descriptions, resolved.collapseThreshold, listed.repos);
254
376
  const generatedAt = new Date().toISOString().split(".")[0] + "Z";
377
+ const repoIds = listed.repos ? Array.from(new Set(listed.repos)) : undefined;
255
378
  const metadata = {
256
379
  generatedAt,
257
- fingerprint: computeCodebaseFingerprint(listed.files, resolved, listed.truncated),
380
+ fingerprint: computeCodebaseFingerprint(listed.files, resolved, listed.truncated, listed.repos),
258
381
  fileCount: listed.files.length,
259
382
  truncated: listed.truncated,
383
+ ...(repoIds ? { repositories: repoIds } : {}),
260
384
  };
261
385
  const content = renderCodebaseMap(groups, listed.files.length, listed.truncated, metadata);
262
386
  return {
@@ -273,9 +397,21 @@ function buildCodebaseMap(basePath, resolved, existingDescriptions, enumerated)
273
397
  * Generate a fresh CODEBASE.md from scratch.
274
398
  * Preserves existing descriptions if `existingDescriptions` is provided.
275
399
  */
400
+ /**
401
+ * Resolve workspace-aware enumeration for parent mode, or undefined to fall
402
+ * back to the legacy single-root enumeration. Centralized so all three public
403
+ * entry points (generate/update/ensure) share one discovery path.
404
+ */
405
+ function resolveWorkspaceEnumeration(basePath, resolved) {
406
+ const registry = loadWorkspaceRegistry(basePath);
407
+ if (!registry)
408
+ return undefined;
409
+ return enumerateWorkspaceFiles(registry, resolved.excludes, resolved.maxFiles);
410
+ }
276
411
  export function generateCodebaseMap(basePath, options, existingDescriptions) {
277
412
  const resolved = resolveGeneratorOptions(options);
278
- return buildCodebaseMap(basePath, resolved, existingDescriptions);
413
+ const enumerated = resolveWorkspaceEnumeration(basePath, resolved);
414
+ return buildCodebaseMap(basePath, resolved, existingDescriptions, enumerated);
279
415
  }
280
416
  /**
281
417
  * Incremental update: re-scan files, preserve existing descriptions,
@@ -293,7 +429,8 @@ export function updateCodebaseMap(basePath, options) {
293
429
  const existingFiles = new Set(existingDescriptions.keys());
294
430
  // Generate new map preserving descriptions — reuse the returned file list
295
431
  // to avoid a second enumeration (prevents race between content and stats).
296
- const result = buildCodebaseMap(basePath, resolved, existingDescriptions);
432
+ const enumerated = resolveWorkspaceEnumeration(basePath, resolved);
433
+ const result = buildCodebaseMap(basePath, resolved, existingDescriptions, enumerated);
297
434
  const currentSet = new Set(result.files);
298
435
  // Count changes
299
436
  let added = 0;
@@ -326,7 +463,7 @@ function clearFreshnessCache(basePath) {
326
463
  }
327
464
  export function ensureCodebaseMapFresh(basePath, options, ensureOptions) {
328
465
  const resolved = resolveGeneratorOptions(options);
329
- const cacheKey = `${basePath}::${resolved.optionSignature}`;
466
+ const cacheKey = `${basePath}::${resolved.optionSignature}::${workspaceEnumerationSignature(basePath)}`;
330
467
  const ttlMs = ensureOptions?.ttlMs ?? DEFAULT_REFRESH_TTL_MS;
331
468
  const force = ensureOptions?.force === true;
332
469
  const now = Date.now();
@@ -338,8 +475,9 @@ export function ensureCodebaseMapFresh(basePath, options, ensureOptions) {
338
475
  }
339
476
  }
340
477
  const existing = readCodebaseMap(basePath);
341
- const listed = enumerateFiles(basePath, resolved.excludes, resolved.maxFiles);
342
- const fingerprint = computeCodebaseFingerprint(listed.files, resolved, listed.truncated);
478
+ const listed = resolveWorkspaceEnumeration(basePath, resolved)
479
+ ?? enumerateFiles(basePath, resolved.excludes, resolved.maxFiles);
480
+ const fingerprint = computeCodebaseFingerprint(listed.files, resolved, listed.truncated, listed.repos);
343
481
  const cacheAndReturn = (result) => {
344
482
  freshnessCache.set(cacheKey, { checkedAt: now, result });
345
483
  return result;