gsd-pi 2.80.0-dev.2be7caf18 → 2.80.0-dev.3065f7bfa

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 (268) hide show
  1. package/README.md +1 -0
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/phases.js +178 -108
  4. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +20 -30
  6. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  7. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +5 -32
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +16 -0
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +17 -4
  10. package/dist/resources/extensions/gsd/auto-prompts.js +90 -15
  11. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  12. package/dist/resources/extensions/gsd/auto-start.js +197 -6
  13. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  14. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  15. package/dist/resources/extensions/gsd/auto-worktree.js +111 -1
  16. package/dist/resources/extensions/gsd/auto.js +79 -24
  17. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +42 -2
  18. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +49 -36
  19. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +15 -5
  20. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +9 -3
  21. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +7 -1
  22. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +9 -3
  23. package/dist/resources/extensions/gsd/bootstrap/query-tools.js +8 -2
  24. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +238 -28
  25. package/dist/resources/extensions/gsd/bootstrap/system-context.js +82 -23
  26. package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
  27. package/dist/resources/extensions/gsd/commands-handlers.js +23 -9
  28. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  29. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  30. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  31. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  32. package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +2 -0
  33. package/dist/resources/extensions/gsd/git-service.js +36 -4
  34. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  35. package/dist/resources/extensions/gsd/guided-flow.js +66 -32
  36. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  37. package/dist/resources/extensions/gsd/native-git-bridge.js +32 -8
  38. package/dist/resources/extensions/gsd/orphan-stash-audit.js +101 -0
  39. package/dist/resources/extensions/gsd/parallel-orchestrator.js +13 -3
  40. package/dist/resources/extensions/gsd/pre-execution-checks.js +22 -0
  41. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  42. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
  43. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  44. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
  45. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  46. package/dist/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  47. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  48. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  49. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  50. package/dist/resources/extensions/gsd/workflow-protocol.js +131 -0
  51. package/dist/resources/extensions/gsd/worktree-resolver.js +68 -21
  52. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  53. package/dist/web/standalone/.next/BUILD_ID +1 -1
  54. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  55. package/dist/web/standalone/.next/build-manifest.json +2 -2
  56. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  57. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.html +1 -1
  74. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  81. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  83. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  84. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  85. package/package.json +3 -3
  86. package/packages/mcp-server/src/workflow-tools.test.ts +9 -2
  87. package/packages/native/tsconfig.tsbuildinfo +1 -1
  88. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  89. package/packages/pi-agent-core/dist/agent-loop.js +4 -1
  90. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  91. package/packages/pi-agent-core/dist/agent.d.ts +5 -0
  92. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  93. package/packages/pi-agent-core/dist/agent.js +2 -0
  94. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  95. package/packages/pi-agent-core/dist/index.d.ts +1 -0
  96. package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
  97. package/packages/pi-agent-core/dist/index.js +2 -0
  98. package/packages/pi-agent-core/dist/index.js.map +1 -1
  99. package/packages/pi-agent-core/dist/token-audit.d.ts +47 -0
  100. package/packages/pi-agent-core/dist/token-audit.d.ts.map +1 -0
  101. package/packages/pi-agent-core/dist/token-audit.js +221 -0
  102. package/packages/pi-agent-core/dist/token-audit.js.map +1 -0
  103. package/packages/pi-agent-core/dist/types.d.ts +9 -0
  104. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  105. package/packages/pi-agent-core/dist/types.js.map +1 -1
  106. package/packages/pi-agent-core/src/agent-loop.test.ts +128 -0
  107. package/packages/pi-agent-core/src/agent-loop.ts +4 -1
  108. package/packages/pi-agent-core/src/agent.ts +8 -0
  109. package/packages/pi-agent-core/src/index.ts +2 -0
  110. package/packages/pi-agent-core/src/token-audit.test.ts +189 -0
  111. package/packages/pi-agent-core/src/token-audit.ts +287 -0
  112. package/packages/pi-agent-core/src/types.ts +14 -0
  113. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  114. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  115. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +18 -0
  117. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
  119. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/agent-session.js +41 -7
  121. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -0
  124. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
  126. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -6
  128. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +3 -3
  130. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +32 -1
  132. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js +2 -0
  135. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts +2 -0
  137. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts.map +1 -0
  138. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js +46 -0
  139. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js.map +1 -0
  140. package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -2
  141. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/core/sdk.js +74 -2
  143. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  144. package/packages/pi-coding-agent/dist/core/skill-tool.test.js +22 -0
  145. package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
  146. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +6 -7
  147. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  148. package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -3
  149. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  150. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  151. package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +25 -0
  152. package/packages/pi-coding-agent/src/core/agent-session.ts +45 -7
  153. package/packages/pi-coding-agent/src/core/extensions/loader.ts +10 -0
  154. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +3 -3
  155. package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -5
  156. package/packages/pi-coding-agent/src/core/extensions/types.ts +35 -1
  157. package/packages/pi-coding-agent/src/core/hooks-runner.test.ts +2 -0
  158. package/packages/pi-coding-agent/src/core/sdk-tool-filter.test.ts +60 -0
  159. package/packages/pi-coding-agent/src/core/sdk.ts +85 -3
  160. package/packages/pi-coding-agent/src/core/skill-tool.test.ts +28 -0
  161. package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -10
  162. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  163. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
  164. package/src/resources/extensions/gsd/auto/phases.ts +251 -172
  165. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  166. package/src/resources/extensions/gsd/auto/run-unit.ts +25 -30
  167. package/src/resources/extensions/gsd/auto/session.ts +8 -0
  168. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +8 -34
  169. package/src/resources/extensions/gsd/auto-dispatch.ts +16 -0
  170. package/src/resources/extensions/gsd/auto-post-unit.ts +18 -4
  171. package/src/resources/extensions/gsd/auto-prompts.ts +95 -14
  172. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  173. package/src/resources/extensions/gsd/auto-start.ts +230 -9
  174. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  175. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  176. package/src/resources/extensions/gsd/auto-worktree.ts +123 -0
  177. package/src/resources/extensions/gsd/auto.ts +87 -19
  178. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +49 -1
  179. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +50 -36
  180. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +16 -5
  181. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +10 -3
  182. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +8 -1
  183. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +10 -3
  184. package/src/resources/extensions/gsd/bootstrap/query-tools.ts +9 -2
  185. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +283 -28
  186. package/src/resources/extensions/gsd/bootstrap/system-context.ts +90 -22
  187. package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
  188. package/src/resources/extensions/gsd/commands-handlers.ts +34 -15
  189. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  190. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  191. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  192. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  193. package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +3 -0
  194. package/src/resources/extensions/gsd/git-service.ts +46 -8
  195. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  196. package/src/resources/extensions/gsd/guided-flow.ts +84 -39
  197. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  198. package/src/resources/extensions/gsd/native-git-bridge.ts +39 -6
  199. package/src/resources/extensions/gsd/orphan-stash-audit.ts +117 -0
  200. package/src/resources/extensions/gsd/parallel-orchestrator.ts +13 -3
  201. package/src/resources/extensions/gsd/pre-execution-checks.ts +23 -0
  202. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  203. package/src/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
  204. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  205. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
  206. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  207. package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  208. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +2 -2
  209. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +417 -10
  210. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  211. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  212. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +168 -6
  213. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
  214. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +3 -1
  215. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +31 -0
  216. package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +3 -2
  217. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  218. package/src/resources/extensions/gsd/tests/context-store.test.ts +7 -1
  219. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
  220. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  221. package/src/resources/extensions/gsd/tests/execute-task-rendering.test.ts +5 -2
  222. package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +219 -0
  223. package/src/resources/extensions/gsd/tests/finalize-survivor-branch.test.ts +132 -0
  224. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
  225. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +6 -3
  226. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +239 -1
  227. package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +32 -0
  228. package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -0
  229. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  230. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -0
  231. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +242 -0
  232. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +34 -2
  233. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -0
  234. package/src/resources/extensions/gsd/tests/orphan-merge-bootstrap.test.ts +133 -0
  235. package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +201 -0
  236. package/src/resources/extensions/gsd/tests/parallel-orchestrator-fast-forward.test.ts +113 -0
  237. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +45 -5
  238. package/src/resources/extensions/gsd/tests/prompt-duplication-cuts.test.ts +230 -0
  239. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  240. package/src/resources/extensions/gsd/tests/query-tools-db-open.test.ts +3 -3
  241. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +38 -17
  242. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  243. package/src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts +96 -0
  244. package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +106 -0
  245. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  246. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  247. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +38 -0
  248. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
  249. package/src/resources/extensions/gsd/tests/system-context-memory.test.ts +112 -0
  250. package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +7 -9
  251. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +291 -0
  252. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  253. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  254. package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +5 -4
  255. package/src/resources/extensions/gsd/tests/workflow-protocol-excerpt.test.ts +99 -0
  256. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  257. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +1 -0
  258. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +22 -19
  259. package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +66 -0
  260. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +167 -4
  261. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  262. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  263. package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
  264. package/src/resources/extensions/gsd/workflow-protocol.ts +160 -0
  265. package/src/resources/extensions/gsd/worktree-resolver.ts +85 -19
  266. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +0 -97
  267. /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → O2WvnXROue6l7nAp_4dhC}/_buildManifest.js +0 -0
  268. /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → O2WvnXROue6l7nAp_4dhC}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -259,6 +259,7 @@ Full documentation is in the [`docs/`](./docs/) directory:
259
259
 
260
260
  - **[Architecture](./docs/dev/architecture.md)** — system design and dispatch pipeline
261
261
  - **[CI/CD Pipeline](./docs/dev/ci-cd-pipeline.md)** — three-stage promotion pipeline (Dev → Test → Prod)
262
+ - **[E2E Testing](./tests/e2e/README.md)** — real-process CLI/runtime coverage and CI runner expectations
262
263
  - **[Pipeline Simplification (ADR-003)](./docs/dev/ADR-003-pipeline-simplification.md)** — merged research into planning, mechanical completion
263
264
  - **[VS Code Extension](./vscode-extension/README.md)** — chat participant, sidebar dashboard, RPC integration
264
265
 
@@ -1 +1 @@
1
- 8e6c04d763f9ba00
1
+ 89938c18ffd18359
@@ -23,12 +23,12 @@ import { existsSync, cpSync } from "node:fs";
23
23
  import { logWarning, logError, _resetLogs, drainLogs, drainAndSummarize, formatForNotification, hasAnyIssues, } from "../workflow-logger.js";
24
24
  import { gsdRoot } from "../paths.js";
25
25
  import { atomicWriteSync } from "../atomic-write.js";
26
- import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
26
+ import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
27
27
  import { writeUnitRuntimeRecord } from "../unit-runtime.js";
28
28
  import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
29
29
  import { getEligibleSlices } from "../slice-parallel-eligibility.js";
30
30
  import { startSliceParallel } from "../slice-parallel-orchestrator.js";
31
- import { isDbAvailable, getMilestoneSlices, refreshOpenDatabaseFromDisk } from "../gsd-db.js";
31
+ import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
32
32
  import { ensurePlanV2Graph, isEmptyPlanV2GraphResult, isMissingFinalizedContextResult } from "../uok/plan-v2.js";
33
33
  import { resolveUokFlags } from "../uok/flags.js";
34
34
  import { UokGateRunner } from "../uok/gate-runner.js";
@@ -42,12 +42,10 @@ import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit,
42
42
  function isSamePathLocal(a, b) {
43
43
  return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
44
44
  }
45
- function refreshPlanSliceRecoveryDbIfNeeded(unitType) {
46
- if (unitType !== "plan-slice")
47
- return true;
48
- if (!isDbAvailable())
49
- return true;
50
- return refreshOpenDatabaseFromDisk();
45
+ export function shouldDegradeEmptyWorktreeToProjectRoot(worktreeClassification, projectRootClassification) {
46
+ return (worktreeClassification.kind === "greenfield" &&
47
+ projectRootClassification.kind !== "greenfield" &&
48
+ projectRootClassification.kind !== "invalid-repo");
51
49
  }
52
50
  // ─── Session timeout auto-resume state ────────────────────────────────────────
53
51
  let consecutiveSessionTimeouts = 0;
@@ -136,6 +134,74 @@ async function closeoutAndStop(ctx, pi, s, deps, reason) {
136
134
  }
137
135
  await deps.stopAuto(ctx, pi, reason);
138
136
  }
137
+ async function stopOnPostflightRecoveryNeeded(ic, result, milestoneId) {
138
+ if (!result.needsManualRecovery)
139
+ return null;
140
+ const { ctx, pi, deps } = ic;
141
+ const reason = `Post-merge stash restore failed for milestone ${milestoneId}`;
142
+ ctx.ui.notify(`${reason}. Resolve the working tree before resuming auto-mode. ${result.message}`, "error");
143
+ await deps.stopAuto(ctx, pi, reason);
144
+ return { action: "break", reason: "postflight-stash-restore-failed" };
145
+ }
146
+ async function restorePreflightStashOrStop(ic, preflight, milestoneId) {
147
+ if (!preflight.stashPushed)
148
+ return null;
149
+ const { ctx, s, deps } = ic;
150
+ const result = deps.postflightPopStash(s.originalBasePath || s.basePath, milestoneId, preflight.stashMarker, ctx.ui.notify.bind(ctx.ui));
151
+ return stopOnPostflightRecoveryNeeded(ic, result, milestoneId);
152
+ }
153
+ /**
154
+ * Run a milestone merge surrounded by preflight stash + always-on postflight
155
+ * pop. The previous code popped the stash only after a successful merge, which
156
+ * leaked `gsd-preflight-stash:M00x:*` entries whenever `mergeAndExit` threw —
157
+ * leaving the user's pre-merge working tree silently stashed away after a
158
+ * merge-conflict or other merge error. This helper restores the stash on
159
+ * every exit path, then surfaces the merge or stash failure (in priority
160
+ * order) as the loop's stop reason.
161
+ *
162
+ * Returns a `break` action when auto-mode must stop, or `null` when the merge
163
+ * succeeded and the stash (if any) was restored cleanly.
164
+ */
165
+ export async function _runMilestoneMergeWithStashRestore(ic, milestoneId) {
166
+ const { ctx, pi, s, deps } = ic;
167
+ const preflight = deps.preflightCleanRoot(s.originalBasePath || s.basePath, milestoneId, ctx.ui.notify.bind(ctx.ui));
168
+ let mergeError = null;
169
+ try {
170
+ deps.resolver.mergeAndExit(milestoneId, ctx.ui);
171
+ s.milestoneMergedInPhases = true;
172
+ }
173
+ catch (mergeErr) {
174
+ mergeError = mergeErr;
175
+ }
176
+ // Always attempt to restore the stashed working tree, even on merge error.
177
+ // postflightPopStash itself does not throw; failures surface via the
178
+ // PostflightResult.needsManualRecovery flag.
179
+ let stashResult = null;
180
+ if (preflight.stashPushed) {
181
+ stashResult = deps.postflightPopStash(s.originalBasePath || s.basePath, milestoneId, preflight.stashMarker, ctx.ui.notify.bind(ctx.ui));
182
+ }
183
+ // Merge failure takes priority over stash recovery — the merge is the
184
+ // authoritative gate. If the stash also needed manual recovery, the user
185
+ // already saw the postflightPopStash notify above.
186
+ if (mergeError) {
187
+ if (mergeError instanceof MergeConflictError) {
188
+ ctx.ui.notify(`Merge conflict: ${mergeError.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
189
+ await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${milestoneId}`);
190
+ return { action: "break", reason: "merge-conflict" };
191
+ }
192
+ logError("engine", "Milestone merge failed with non-conflict error", {
193
+ milestone: milestoneId,
194
+ error: String(mergeError),
195
+ });
196
+ ctx.ui.notify(`Merge failed: ${mergeError instanceof Error ? mergeError.message : String(mergeError)}. Resolve and run /gsd auto to resume.`, "error");
197
+ await deps.stopAuto(ctx, pi, `Merge error on milestone ${milestoneId}: ${String(mergeError)}`);
198
+ return { action: "break", reason: "merge-failed" };
199
+ }
200
+ if (stashResult) {
201
+ return stopOnPostflightRecoveryNeeded(ic, stashResult, milestoneId);
202
+ }
203
+ return null;
204
+ }
139
205
  async function emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, errorContext) {
140
206
  ic.deps.emitJournalEvent({
141
207
  ts: new Date().toISOString(),
@@ -477,28 +543,12 @@ export async function runPreDispatch(ic, loopState) {
477
543
  s.unitLifetimeDispatches.clear();
478
544
  loopState.recentUnits.length = 0;
479
545
  loopState.stuckRecoveryAttempts = 0;
480
- // Worktree lifecycle on milestone transition — merge current, enter next
481
- // #2909: preflight warn + stash dirty working tree before merge
482
- const preflightTransition = deps.preflightCleanRoot(s.originalBasePath || s.basePath, s.currentMilestoneId, ctx.ui.notify.bind(ctx.ui));
483
- try {
484
- deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
485
- }
486
- catch (mergeErr) {
487
- if (mergeErr instanceof MergeConflictError) {
488
- // Real code conflicts — stop the loop instead of retrying forever (#2330)
489
- ctx.ui.notify(`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
490
- await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
491
- return { action: "break", reason: "merge-conflict" };
492
- }
493
- // Non-conflict merge errors — stop auto to avoid advancing with unmerged work
494
- logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
495
- ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
496
- await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
497
- return { action: "break", reason: "merge-failed" };
498
- }
499
- // #2909: postflight — restore stashed changes after successful merge
500
- if (preflightTransition.stashPushed) {
501
- deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightTransition.stashMarker, ctx.ui.notify.bind(ctx.ui));
546
+ // Worktree lifecycle on milestone transition — merge current, enter next.
547
+ // #2909 / #5538-followup: preflight stash + always-on postflight pop.
548
+ {
549
+ const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
550
+ if (stop)
551
+ return stop;
502
552
  }
503
553
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
504
554
  deps.invalidateAllCaches();
@@ -552,30 +602,12 @@ export async function runPreDispatch(ic, loopState) {
552
602
  }
553
603
  const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
554
604
  if (incomplete.length === 0 && state.registry.length > 0) {
555
- // All milestones complete — merge milestone branch before stopping
605
+ // All milestones complete — merge milestone branch before stopping.
556
606
  if (s.currentMilestoneId) {
557
- // #2909: preflight warn + stash dirty working tree before merge
558
- const preflightAllComplete = deps.preflightCleanRoot(s.originalBasePath || s.basePath, s.currentMilestoneId, ctx.ui.notify.bind(ctx.ui));
559
- try {
560
- deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
561
- // Prevent stopAuto from attempting the same merge (#2645)
562
- s.milestoneMergedInPhases = true;
563
- }
564
- catch (mergeErr) {
565
- if (mergeErr instanceof MergeConflictError) {
566
- ctx.ui.notify(`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
567
- await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
568
- return { action: "break", reason: "merge-conflict" };
569
- }
570
- logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
571
- ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
572
- await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
573
- return { action: "break", reason: "merge-failed" };
574
- }
575
- // #2909: postflight — restore stashed changes after successful merge
576
- if (preflightAllComplete.stashPushed) {
577
- deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightAllComplete.stashMarker, ctx.ui.notify.bind(ctx.ui));
578
- }
607
+ // #2909 / #5538-followup: preflight stash + always-on postflight pop.
608
+ const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
609
+ if (stop)
610
+ return stop;
579
611
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
580
612
  }
581
613
  deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone", basename(s.originalBasePath || s.basePath));
@@ -638,30 +670,12 @@ export async function runPreDispatch(ic, loopState) {
638
670
  }
639
671
  // Terminal: complete
640
672
  if (state.phase === "complete") {
641
- // Milestone merge on complete (before closeout so branch state is clean)
673
+ // Milestone merge on complete (before closeout so branch state is clean).
642
674
  if (s.currentMilestoneId) {
643
- // #2909: preflight warn + stash dirty working tree before merge
644
- const preflightComplete = deps.preflightCleanRoot(s.originalBasePath || s.basePath, s.currentMilestoneId, ctx.ui.notify.bind(ctx.ui));
645
- try {
646
- deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
647
- // Prevent stopAuto from attempting the same merge (#2645)
648
- s.milestoneMergedInPhases = true;
649
- }
650
- catch (mergeErr) {
651
- if (mergeErr instanceof MergeConflictError) {
652
- ctx.ui.notify(`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
653
- await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
654
- return { action: "break", reason: "merge-conflict" };
655
- }
656
- logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
657
- ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
658
- await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
659
- return { action: "break", reason: "merge-failed" };
660
- }
661
- // #2909: postflight — restore stashed changes after successful merge
662
- if (preflightComplete.stashPushed) {
663
- deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightComplete.stashMarker, ctx.ui.notify.bind(ctx.ui));
664
- }
675
+ // #2909 / #5538-followup: preflight stash + always-on postflight pop.
676
+ const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
677
+ if (stop)
678
+ return stop;
665
679
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
666
680
  }
667
681
  deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone", basename(s.originalBasePath || s.basePath));
@@ -754,6 +768,48 @@ export async function runDispatch(ic, preData, loopState) {
754
768
  let unitId = dispatchResult.unitId;
755
769
  let prompt = dispatchResult.prompt;
756
770
  const pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
771
+ // Resolve hooks and prior-slice gating before health/stuck accounting so
772
+ // those checks run against the final dispatch unit.
773
+ const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
774
+ if (preDispatchResult.firedHooks.length > 0) {
775
+ ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
776
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
777
+ }
778
+ if (preDispatchResult.action === "skip") {
779
+ ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
780
+ await new Promise((r) => setImmediate(r));
781
+ return { action: "continue" };
782
+ }
783
+ if (preDispatchResult.action === "replace") {
784
+ prompt = preDispatchResult.prompt ?? prompt;
785
+ if (preDispatchResult.unitType)
786
+ unitType = preDispatchResult.unitType;
787
+ }
788
+ else if (preDispatchResult.prompt) {
789
+ prompt = preDispatchResult.prompt;
790
+ }
791
+ const guardBasePath = _resolveDispatchGuardBasePath(s);
792
+ const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(guardBasePath, deps.getMainBranch(guardBasePath), unitType, unitId);
793
+ if (priorSliceBlocker) {
794
+ await deps.stopAuto(ctx, pi, priorSliceBlocker);
795
+ debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
796
+ return { action: "break", reason: "prior-slice-blocker" };
797
+ }
798
+ // Execute-task needs a real writable checkout. The same health check also
799
+ // exists in runUnitPhase, but the stuck-window detector runs before that
800
+ // phase. Check here too so repeated derivations of a broken worktree stop
801
+ // with the actionable worktree error instead of the generic stuck-loop error.
802
+ if (s.basePath && unitType === "execute-task") {
803
+ const gitMarker = join(s.basePath, ".git");
804
+ const hasGit = deps.existsSync(gitMarker);
805
+ if (!hasGit) {
806
+ const msg = `Worktree health check failed: ${s.basePath} has no .git — refusing to dispatch ${unitType} ${unitId}`;
807
+ debugLog("autoLoop", { phase: "dispatch-worktree-health-fail", basePath: s.basePath, hasGit });
808
+ ctx.ui.notify(msg, "error");
809
+ await deps.stopAuto(ctx, pi, msg);
810
+ return { action: "break", reason: "worktree-invalid" };
811
+ }
812
+ }
757
813
  // ── Sliding-window stuck detection with graduated recovery ──
758
814
  const derivedKey = `${unitType}/${unitId}`;
759
815
  // Always record this dispatch in the sliding window so detectStuck() has
@@ -795,11 +851,18 @@ export async function runDispatch(ic, preData, loopState) {
795
851
  level: 1,
796
852
  action: "artifact-found",
797
853
  });
798
- ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
799
- if (!refreshPlanSliceRecoveryDbIfNeeded(unitType)) {
800
- ctx.ui.notify(`Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed. Keeping stuck state for retry.`, "warning");
854
+ const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
855
+ if (!recoveryDb.ok) {
856
+ ctx.ui.notify(recoveryDb.fatal
857
+ ? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
858
+ : `${recoveryDb.message} Keeping stuck state for retry.`, "warning");
859
+ if (recoveryDb.fatal) {
860
+ await deps.pauseAuto(ctx, pi);
861
+ return { action: "break", reason: recoveryDb.reason };
862
+ }
801
863
  return { action: "continue" };
802
864
  }
865
+ ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
803
866
  deps.invalidateAllCaches();
804
867
  loopState.recentUnits.length = 0;
805
868
  loopState.stuckRecoveryAttempts = 0;
@@ -818,13 +881,20 @@ export async function runDispatch(ic, preData, loopState) {
818
881
  level: 2,
819
882
  action: "artifact-found",
820
883
  });
821
- ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`, "info");
822
- if (refreshPlanSliceRecoveryDbIfNeeded(unitType)) {
884
+ const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
885
+ if (recoveryDb.ok) {
886
+ ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`, "info");
823
887
  loopState.recentUnits.length = 0;
824
888
  loopState.stuckRecoveryAttempts = 0;
825
889
  return { action: "continue" };
826
890
  }
827
- ctx.ui.notify(`Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed. Stopping for manual recovery.`, "warning");
891
+ ctx.ui.notify(recoveryDb.fatal
892
+ ? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
893
+ : `${recoveryDb.message} Stopping for manual recovery.`, "warning");
894
+ if (recoveryDb.fatal) {
895
+ await deps.pauseAuto(ctx, pi);
896
+ return { action: "break", reason: recoveryDb.reason };
897
+ }
828
898
  }
829
899
  debugLog("autoLoop", {
830
900
  phase: "stuck-detected",
@@ -856,32 +926,6 @@ export async function runDispatch(ic, preData, loopState) {
856
926
  }
857
927
  }
858
928
  }
859
- // Pre-dispatch hooks
860
- const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
861
- if (preDispatchResult.firedHooks.length > 0) {
862
- ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
863
- deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
864
- }
865
- if (preDispatchResult.action === "skip") {
866
- ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
867
- await new Promise((r) => setImmediate(r));
868
- return { action: "continue" };
869
- }
870
- if (preDispatchResult.action === "replace") {
871
- prompt = preDispatchResult.prompt ?? prompt;
872
- if (preDispatchResult.unitType)
873
- unitType = preDispatchResult.unitType;
874
- }
875
- else if (preDispatchResult.prompt) {
876
- prompt = preDispatchResult.prompt;
877
- }
878
- const guardBasePath = _resolveDispatchGuardBasePath(s);
879
- const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(guardBasePath, deps.getMainBranch(guardBasePath), unitType, unitId);
880
- if (priorSliceBlocker) {
881
- await deps.stopAuto(ctx, pi, priorSliceBlocker);
882
- debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
883
- return { action: "break", reason: "prior-slice-blocker" };
884
- }
885
929
  return {
886
930
  action: "next",
887
931
  data: {
@@ -1111,6 +1155,25 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1111
1155
  }
1112
1156
  }
1113
1157
  else if (projectClassification.kind === "greenfield") {
1158
+ const projectRoot = s.canonicalProjectRoot;
1159
+ if (!isSamePathLocal(s.basePath, projectRoot)) {
1160
+ const projectRootClassification = classifyProject(projectRoot);
1161
+ if (shouldDegradeEmptyWorktreeToProjectRoot(projectClassification, projectRootClassification)) {
1162
+ debugLog("runUnitPhase", {
1163
+ phase: "worktree-health-degrade-to-project-root",
1164
+ worktreePath: s.basePath,
1165
+ projectRoot,
1166
+ worktreeClassification: projectClassification,
1167
+ projectRootClassification,
1168
+ });
1169
+ ctx.ui.notify(`Warning: ${s.basePath} has no project content, but ${projectRoot} does. Continuing in project root because the milestone worktree cannot represent untracked project files.`, "warning");
1170
+ s.basePath = projectRoot;
1171
+ s.isolationDegraded = true;
1172
+ projectClassification = projectRootClassification;
1173
+ }
1174
+ }
1175
+ }
1176
+ if (projectClassification.kind === "greenfield") {
1114
1177
  debugLog("runUnitPhase", { phase: "worktree-health-greenfield", basePath: s.basePath, classification: projectClassification });
1115
1178
  ctx.ui.notify(`Warning: ${s.basePath} has no project content yet — proceeding as greenfield project`, "warning");
1116
1179
  }
@@ -1672,6 +1735,13 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
1672
1735
  }
1673
1736
  // Both pre and post verification completed without timeout — reset counter
1674
1737
  loopState.consecutiveFinalizeTimeouts = 0;
1738
+ if (preUnitSnapshot) {
1739
+ writeUnitRuntimeRecord(s.basePath, preUnitSnapshot.type, preUnitSnapshot.id, preUnitSnapshot.startedAt, {
1740
+ phase: "finalized",
1741
+ lastProgressAt: Date.now(),
1742
+ lastProgressKind: "finalize-success",
1743
+ });
1744
+ }
1675
1745
  s.currentUnit = null;
1676
1746
  clearCurrentPhase();
1677
1747
  // Surface accumulated workflow-logger issues for this unit to the user.
@@ -17,6 +17,7 @@ import { bumpTurnGeneration } from "./turn-epoch.js";
17
17
  // scoped pendingResolve + pendingAgentEndQueue pattern.
18
18
  let _currentResolve = null;
19
19
  let _sessionSwitchInFlight = false;
20
+ let _pendingSwitchCancellation = null;
20
21
  // ─── Setters (needed for cross-module mutation) ─────────────────────────────
21
22
  export function _setCurrentResolve(fn) {
22
23
  _currentResolve = fn;
@@ -27,6 +28,11 @@ export function _setSessionSwitchInFlight(v) {
27
28
  export function _clearCurrentResolve() {
28
29
  _currentResolve = null;
29
30
  }
31
+ export function _consumePendingSwitchCancellation() {
32
+ const pending = _pendingSwitchCancellation;
33
+ _pendingSwitchCancellation = null;
34
+ return pending;
35
+ }
30
36
  // ─── resolveAgentEnd ─────────────────────────────────────────────────────────
31
37
  /**
32
38
  * Called from the agent_end event handler in index.ts to resolve the
@@ -90,8 +96,18 @@ export function resolveAgentEndCancelled(errorContext) {
90
96
  debugLog("resolveAgentEndCancelled", { status: "resolving-cancelled" });
91
97
  const r = _currentResolve;
92
98
  _currentResolve = null;
99
+ _pendingSwitchCancellation = null;
93
100
  r({ status: "cancelled", ...(errorContext ? { errorContext } : {}) });
101
+ return true;
102
+ }
103
+ if (_sessionSwitchInFlight) {
104
+ bumpTurnGeneration(`cancelled-during-switch:${errorContext?.category ?? "unknown"}`);
105
+ _pendingSwitchCancellation = errorContext ? { errorContext } : {};
106
+ debugLog("resolveAgentEndCancelled", { status: "queued-during-switch" });
107
+ return false;
94
108
  }
109
+ debugLog("resolveAgentEndCancelled", { status: "no-pending-resolve" });
110
+ return false;
95
111
  }
96
112
  // ─── resetPendingResolve (test helper) ───────────────────────────────────────
97
113
  /**
@@ -101,6 +117,7 @@ export function resolveAgentEndCancelled(errorContext) {
101
117
  export function _resetPendingResolve() {
102
118
  _currentResolve = null;
103
119
  _sessionSwitchInFlight = false;
120
+ _pendingSwitchCancellation = null;
104
121
  }
105
122
  export function _hasPendingResolveForTest() {
106
123
  return _currentResolve !== null;
@@ -1,6 +1,6 @@
1
1
  // GSD-2 + src/resources/extensions/gsd/auto/run-unit.ts - Runs one GSD auto-mode unit from session creation through agent completion.
2
2
  import { NEW_SESSION_TIMEOUT_MS } from "./session.js";
3
- import { _clearCurrentResolve, _setCurrentResolve, _setSessionSwitchInFlight } from "./resolve.js";
3
+ import { _clearCurrentResolve, _consumePendingSwitchCancellation, _setCurrentResolve, _setSessionSwitchInFlight, } from "./resolve.js";
4
4
  import { getCurrentTurnGeneration, runWithTurnGeneration, } from "./turn-epoch.js";
5
5
  import { debugLog } from "../debug-logger.js";
6
6
  import { logWarning } from "../workflow-logger.js";
@@ -19,43 +19,21 @@ let sessionSwitchGeneration = 0;
19
19
  */
20
20
  export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
21
21
  debugLog("runUnit", { phase: "start", unitType, unitId });
22
- // Ensure cwd matches basePath BEFORE newSession() captures it. The new
23
- // session reads process.cwd() during construction to anchor its tool
24
- // runtime and system prompt; if cwd has drifted (async_bash, background
25
- // jobs, prior unit cleanup), the session would otherwise be rooted to
26
- // the wrong directory. Must be synchronous — no awaits between chdir
27
- // and newSession (#1389, #4762 follow-up).
28
- try {
29
- if (process.cwd() !== s.basePath) {
30
- process.chdir(s.basePath);
31
- }
32
- }
33
- catch (e) {
34
- const msg = `Failed to chdir to basePath before newSession (basePath: ${s.basePath}): ${String(e)}`;
35
- logWarning("engine", msg, { basePath: s.basePath, error: String(e) });
36
- return {
37
- status: "cancelled",
38
- errorContext: {
39
- message: msg,
40
- category: "session-failed",
41
- isTransient: true,
42
- },
43
- };
44
- }
45
22
  // ── Session creation with timeout ──
46
23
  debugLog("runUnit", { phase: "session-create", unitType, unitId });
47
24
  let sessionResult;
48
25
  let sessionTimeoutHandle;
49
26
  const mySessionSwitchGeneration = ++sessionSwitchGeneration;
50
- // #3731: Cancellation controller for newSession(). When the session-creation
51
- // timeout fires, we abort this controller so that the still-in-flight
52
- // newSession() discards itself after await this.abort() completes, preventing
53
- // it from capturing the (now-root) process.cwd() and rebuilding the tool
54
- // runtime with the wrong cwd.
27
+ // #3731: Cancellation controller for newSession(). When session creation
28
+ // times out, abort before a late session switch can rebuild the tool runtime
29
+ // against a stale workspace root.
55
30
  const sessionAbortController = new AbortController();
56
31
  _setSessionSwitchInFlight(true);
57
32
  try {
58
- const sessionPromise = s.cmdCtx.newSession({ abortSignal: sessionAbortController.signal }).finally(() => {
33
+ const sessionPromise = s.cmdCtx.newSession({
34
+ abortSignal: sessionAbortController.signal,
35
+ workspaceRoot: s.basePath,
36
+ }).finally(() => {
59
37
  if (sessionSwitchGeneration === mySessionSwitchGeneration) {
60
38
  _setSessionSwitchInFlight(false);
61
39
  }
@@ -71,6 +49,7 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
71
49
  catch (sessionErr) {
72
50
  if (sessionTimeoutHandle)
73
51
  clearTimeout(sessionTimeoutHandle);
52
+ _consumePendingSwitchCancellation();
74
53
  const msg = sessionErr instanceof Error ? sessionErr.message : String(sessionErr);
75
54
  debugLog("runUnit", {
76
55
  phase: "session-error",
@@ -83,15 +62,18 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
83
62
  if (sessionTimeoutHandle)
84
63
  clearTimeout(sessionTimeoutHandle);
85
64
  if (sessionResult.cancelled) {
65
+ _consumePendingSwitchCancellation();
86
66
  debugLog("runUnit-session-timeout", { unitType, unitId });
87
67
  return { status: "cancelled", errorContext: { message: "Session creation timed out", category: "timeout", isTransient: true } };
88
68
  }
89
69
  if (!s.active) {
70
+ _consumePendingSwitchCancellation();
90
71
  return { status: "cancelled" };
91
72
  }
92
73
  if (s.currentUnitModel && typeof pi.setModel === "function") {
93
74
  const restored = await pi.setModel(s.currentUnitModel, { persist: false });
94
75
  if (!restored) {
76
+ _consumePendingSwitchCancellation();
95
77
  const message = `Failed to restore configured model ${s.currentUnitModel.provider}/${s.currentUnitModel.id} after session creation`;
96
78
  ctx.ui.notify(`${message}. Cancelling unit before dispatch.`, "warning");
97
79
  return {
@@ -111,6 +93,14 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
111
93
  const unitPromise = new Promise((resolve) => {
112
94
  _setCurrentResolve(resolve);
113
95
  });
96
+ const pendingSwitchCancellation = _consumePendingSwitchCancellation();
97
+ if (pendingSwitchCancellation) {
98
+ _clearCurrentResolve();
99
+ return {
100
+ status: "cancelled",
101
+ ...(pendingSwitchCancellation.errorContext ? { errorContext: pendingSwitchCancellation.errorContext } : {}),
102
+ };
103
+ }
114
104
  // ── Provider request-readiness pre-check (#4555) ──
115
105
  // Verify the provider can accept requests before dispatching. If the token
116
106
  // has expired since bootstrap, return cancelled immediately so the unit is
@@ -106,6 +106,13 @@ export class AutoSession {
106
106
  * stale context bleeding into unrelated slices.
107
107
  */
108
108
  lastPreExecFailure = null;
109
+ /**
110
+ * Tracks how many consecutive times each slice unit has failed pre-execution
111
+ * checks. Keyed by unitId (e.g. "M001/S01"). Used to break the infinite
112
+ * plan-slice → pre-exec fail → re-dispatch loop when the planner cannot fix
113
+ * the issues after MAX_PRE_EXEC_RETRIES re-attempts.
114
+ */
115
+ preExecRetryCount = new Map();
109
116
  // ── Tool invocation errors (#2883) ──────────────────────────────────
110
117
  /** Set when a GSD tool execution ends with isError due to malformed/truncated
111
118
  * JSON arguments. Checked by postUnitPreVerification to break retry loops. */
@@ -258,6 +265,7 @@ export class AutoSession {
258
265
  this.rewriteAttemptCount = 0;
259
266
  this.consecutiveCompleteBootstraps = 0;
260
267
  this.lastPreExecFailure = null;
268
+ this.preExecRetryCount.clear();
261
269
  this.lastToolInvocationError = null;
262
270
  this.lastUnitAgentEndMessages = null;
263
271
  this.lastGitActionFailure = null;
@@ -11,7 +11,6 @@ import { buildResearchSlicePrompt, buildResearchMilestonePrompt, buildPlanSliceP
11
11
  import { loadEffectiveGSDPreferences } from "./preferences.js";
12
12
  import { pauseAuto } from "./auto.js";
13
13
  import { resolveCanonicalMilestoneRoot } from "./worktree-manager.js";
14
- import { logWarning } from "./workflow-logger.js";
15
14
  import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit, } from "./workflow-mcp.js";
16
15
  export async function dispatchDirectPhase(ctx, pi, phase, base) {
17
16
  const state = await deriveState(base);
@@ -231,36 +230,10 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
231
230
  return;
232
231
  }
233
232
  ctx.ui.notify(`Dispatching ${unitType} for ${unitId}...`, "info");
234
- const originalCwd = process.cwd();
235
- try {
236
- // Ensure cwd matches dispatchBase BEFORE newSession() captures it. Synchronous —
237
- // no awaits between chdir and newSession.
238
- try {
239
- if (process.cwd() !== dispatchBase) {
240
- process.chdir(dispatchBase);
241
- }
242
- }
243
- catch (err) {
244
- const msg = `Failed to chdir before direct-dispatch newSession (basePath: ${dispatchBase}): ${err instanceof Error ? err.message : String(err)}`;
245
- logWarning("engine", msg, { file: "auto-direct-dispatch.ts", basePath: dispatchBase, error: err instanceof Error ? err.message : String(err) });
246
- ctx.ui.notify(`${msg}. Cancelling dispatch to avoid running in the wrong directory.`, "error");
247
- return;
248
- }
249
- const result = await ctx.newSession();
250
- if (result.cancelled) {
251
- ctx.ui.notify("Session creation cancelled.", "warning");
252
- return;
253
- }
254
- pi.sendMessage({ customType: "gsd-dispatch", content: prompt, display: false }, { triggerTurn: true });
255
- }
256
- finally {
257
- try {
258
- if (process.cwd() !== originalCwd) {
259
- process.chdir(originalCwd);
260
- }
261
- }
262
- catch (err) {
263
- logWarning("engine", `Failed to restore cwd after direct dispatch: ${err instanceof Error ? err.message : String(err)}`, { file: "auto-direct-dispatch.ts", basePath: originalCwd });
264
- }
233
+ const result = await ctx.newSession({ workspaceRoot: dispatchBase });
234
+ if (result.cancelled) {
235
+ ctx.ui.notify("Session creation cancelled.", "warning");
236
+ return;
265
237
  }
238
+ pi.sendMessage({ customType: "gsd-dispatch", content: prompt, display: false }, { triggerTurn: true });
266
239
  }
@@ -791,6 +791,22 @@ export const DISPATCH_RULES = [
791
791
  const unitId = `${mid}/${sid}`;
792
792
  let priorPreExecFailure;
793
793
  if (session?.lastPreExecFailure?.unitId === unitId) {
794
+ // Circuit breaker: stop re-dispatching after 2 failed retries. The
795
+ // planner has had multiple attempts with injected failure context and
796
+ // still cannot produce a valid plan — human review is required.
797
+ const MAX_PRE_EXEC_RETRIES = 2;
798
+ const retryCount = session.preExecRetryCount?.get(unitId) ?? 0;
799
+ if (retryCount >= MAX_PRE_EXEC_RETRIES) {
800
+ const findings = session.lastPreExecFailure.blockingFindings.join("; ");
801
+ session.lastPreExecFailure = null;
802
+ session.preExecRetryCount?.delete(unitId);
803
+ return {
804
+ action: "stop",
805
+ reason: `Pre-execution checks failed ${retryCount} times for ${unitId} — manual intervention required. Blocking findings: ${findings}. Fix the plan manually, then run /gsd auto to resume.`,
806
+ level: "error",
807
+ matchedRule: "planning → plan-slice",
808
+ };
809
+ }
794
810
  priorPreExecFailure = {
795
811
  blockingFindings: session.lastPreExecFailure.blockingFindings,
796
812
  verdictExcerpt: session.lastPreExecFailure.verdictExcerpt,