gsd-pi 2.80.0-dev.cf9433f56 → 2.80.0-dev.d4fc28e6b

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 (237) hide show
  1. package/dist/cli.js +0 -19
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +29 -0
  4. package/dist/resources/extensions/gsd/auto/loop.js +71 -8
  5. package/dist/resources/extensions/gsd/auto/phases.js +150 -94
  6. package/dist/resources/extensions/gsd/auto/resolve.js +12 -0
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +10 -30
  8. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  9. package/dist/resources/extensions/gsd/auto/workflow-dispatch-claim.js +33 -1
  10. package/dist/resources/extensions/gsd/auto/workflow-worker-heartbeat.js +9 -1
  11. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +5 -32
  12. package/dist/resources/extensions/gsd/auto-dispatch.js +16 -0
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +17 -4
  14. package/dist/resources/extensions/gsd/auto-prompts.js +90 -15
  15. package/dist/resources/extensions/gsd/auto-start.js +197 -6
  16. package/dist/resources/extensions/gsd/auto-worktree.js +111 -1
  17. package/dist/resources/extensions/gsd/auto.js +18 -22
  18. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +86 -19
  19. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +49 -36
  20. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +15 -5
  21. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +9 -3
  22. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +7 -1
  23. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +9 -3
  24. package/dist/resources/extensions/gsd/bootstrap/query-tools.js +8 -2
  25. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +298 -54
  26. package/dist/resources/extensions/gsd/bootstrap/system-context.js +82 -23
  27. package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
  28. package/dist/resources/extensions/gsd/commands-handlers.js +23 -9
  29. package/dist/resources/extensions/gsd/db/unit-dispatches.js +53 -0
  30. package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +2 -0
  31. package/dist/resources/extensions/gsd/guided-flow.js +47 -28
  32. package/dist/resources/extensions/gsd/native-git-bridge.js +32 -8
  33. package/dist/resources/extensions/gsd/orphan-stash-audit.js +101 -0
  34. package/dist/resources/extensions/gsd/parallel-orchestrator.js +13 -3
  35. package/dist/resources/extensions/gsd/pre-execution-checks.js +15 -0
  36. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  37. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
  39. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  40. package/dist/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  41. package/dist/resources/extensions/gsd/workflow-protocol.js +131 -0
  42. package/dist/resources/extensions/gsd/worktree-resolver.js +35 -4
  43. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  44. package/dist/web/standalone/.next/BUILD_ID +1 -1
  45. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  46. package/dist/web/standalone/.next/build-manifest.json +2 -2
  47. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  48. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.html +1 -1
  65. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  72. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/dist/welcome-screen.d.ts +2 -0
  77. package/dist/welcome-screen.js +9 -7
  78. package/package.json +1 -1
  79. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  80. package/packages/pi-agent-core/dist/agent-loop.js +4 -1
  81. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  82. package/packages/pi-agent-core/dist/agent.d.ts +5 -0
  83. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  84. package/packages/pi-agent-core/dist/agent.js +2 -0
  85. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  86. package/packages/pi-agent-core/dist/index.d.ts +1 -0
  87. package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
  88. package/packages/pi-agent-core/dist/index.js +2 -0
  89. package/packages/pi-agent-core/dist/index.js.map +1 -1
  90. package/packages/pi-agent-core/dist/token-audit.d.ts +47 -0
  91. package/packages/pi-agent-core/dist/token-audit.d.ts.map +1 -0
  92. package/packages/pi-agent-core/dist/token-audit.js +221 -0
  93. package/packages/pi-agent-core/dist/token-audit.js.map +1 -0
  94. package/packages/pi-agent-core/dist/types.d.ts +9 -0
  95. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  96. package/packages/pi-agent-core/dist/types.js.map +1 -1
  97. package/packages/pi-agent-core/src/agent-loop.test.ts +128 -0
  98. package/packages/pi-agent-core/src/agent-loop.ts +4 -1
  99. package/packages/pi-agent-core/src/agent.ts +8 -0
  100. package/packages/pi-agent-core/src/index.ts +2 -0
  101. package/packages/pi-agent-core/src/token-audit.test.ts +189 -0
  102. package/packages/pi-agent-core/src/token-audit.ts +287 -0
  103. package/packages/pi-agent-core/src/types.ts +14 -0
  104. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  105. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +18 -0
  106. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
  108. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  109. package/packages/pi-coding-agent/dist/core/agent-session.js +36 -7
  110. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -0
  113. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
  115. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -6
  117. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +3 -3
  119. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +32 -1
  121. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js +2 -0
  124. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts +2 -0
  126. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts.map +1 -0
  127. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js +46 -0
  128. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js.map +1 -0
  129. package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -2
  130. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/sdk.js +74 -2
  132. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  133. package/packages/pi-coding-agent/dist/core/skill-tool.test.js +22 -0
  134. package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +6 -7
  136. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -3
  138. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  139. package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +25 -0
  140. package/packages/pi-coding-agent/src/core/agent-session.ts +40 -7
  141. package/packages/pi-coding-agent/src/core/extensions/loader.ts +10 -0
  142. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +3 -3
  143. package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -5
  144. package/packages/pi-coding-agent/src/core/extensions/types.ts +35 -1
  145. package/packages/pi-coding-agent/src/core/hooks-runner.test.ts +2 -0
  146. package/packages/pi-coding-agent/src/core/sdk-tool-filter.test.ts +60 -0
  147. package/packages/pi-coding-agent/src/core/sdk.ts +85 -3
  148. package/packages/pi-coding-agent/src/core/skill-tool.test.ts +28 -0
  149. package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -10
  150. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  151. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +30 -0
  152. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +26 -0
  153. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
  154. package/src/resources/extensions/gsd/auto/loop.ts +84 -8
  155. package/src/resources/extensions/gsd/auto/phases.ts +218 -154
  156. package/src/resources/extensions/gsd/auto/resolve.ts +19 -0
  157. package/src/resources/extensions/gsd/auto/run-unit.ts +10 -29
  158. package/src/resources/extensions/gsd/auto/session.ts +8 -0
  159. package/src/resources/extensions/gsd/auto/workflow-dispatch-claim.ts +63 -1
  160. package/src/resources/extensions/gsd/auto/workflow-worker-heartbeat.ts +14 -1
  161. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +8 -34
  162. package/src/resources/extensions/gsd/auto-dispatch.ts +16 -0
  163. package/src/resources/extensions/gsd/auto-post-unit.ts +18 -4
  164. package/src/resources/extensions/gsd/auto-prompts.ts +95 -14
  165. package/src/resources/extensions/gsd/auto-start.ts +230 -9
  166. package/src/resources/extensions/gsd/auto-worktree.ts +123 -0
  167. package/src/resources/extensions/gsd/auto.ts +18 -18
  168. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +100 -18
  169. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +50 -36
  170. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +16 -5
  171. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +10 -3
  172. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +8 -1
  173. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +10 -3
  174. package/src/resources/extensions/gsd/bootstrap/query-tools.ts +9 -2
  175. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +347 -54
  176. package/src/resources/extensions/gsd/bootstrap/system-context.ts +90 -22
  177. package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
  178. package/src/resources/extensions/gsd/commands-handlers.ts +34 -15
  179. package/src/resources/extensions/gsd/db/unit-dispatches.ts +66 -0
  180. package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +3 -0
  181. package/src/resources/extensions/gsd/guided-flow.ts +52 -35
  182. package/src/resources/extensions/gsd/native-git-bridge.ts +39 -6
  183. package/src/resources/extensions/gsd/orphan-stash-audit.ts +117 -0
  184. package/src/resources/extensions/gsd/parallel-orchestrator.ts +13 -3
  185. package/src/resources/extensions/gsd/pre-execution-checks.ts +16 -0
  186. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  187. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  188. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
  189. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  190. package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  191. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +2 -2
  192. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +361 -10
  193. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +168 -6
  194. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
  195. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +31 -0
  196. package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +3 -2
  197. package/src/resources/extensions/gsd/tests/context-store.test.ts +7 -1
  198. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
  199. package/src/resources/extensions/gsd/tests/execute-task-rendering.test.ts +5 -2
  200. package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +219 -0
  201. package/src/resources/extensions/gsd/tests/finalize-survivor-branch.test.ts +132 -0
  202. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +6 -3
  203. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +5 -1
  204. package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +32 -0
  205. package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -0
  206. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -0
  207. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +242 -0
  208. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +34 -2
  209. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -0
  210. package/src/resources/extensions/gsd/tests/orphan-merge-bootstrap.test.ts +133 -0
  211. package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +201 -0
  212. package/src/resources/extensions/gsd/tests/parallel-orchestrator-fast-forward.test.ts +113 -0
  213. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +7 -5
  214. package/src/resources/extensions/gsd/tests/prompt-duplication-cuts.test.ts +230 -0
  215. package/src/resources/extensions/gsd/tests/query-tools-db-open.test.ts +3 -3
  216. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +38 -17
  217. package/src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts +96 -0
  218. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +77 -0
  219. package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +166 -0
  220. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
  221. package/src/resources/extensions/gsd/tests/system-context-memory.test.ts +112 -0
  222. package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +7 -9
  223. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +291 -0
  224. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +50 -1
  225. package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +5 -4
  226. package/src/resources/extensions/gsd/tests/workflow-dispatch-claim.test.ts +142 -0
  227. package/src/resources/extensions/gsd/tests/workflow-protocol-excerpt.test.ts +99 -0
  228. package/src/resources/extensions/gsd/tests/workflow-worker-heartbeat.test.ts +32 -1
  229. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +1 -0
  230. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +22 -19
  231. package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +66 -0
  232. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +104 -3
  233. package/src/resources/extensions/gsd/workflow-protocol.ts +160 -0
  234. package/src/resources/extensions/gsd/worktree-resolver.ts +49 -4
  235. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +0 -97
  236. /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_buildManifest.js +0 -0
  237. /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -760,25 +760,6 @@ if (!process.stdin.isTTY || !process.stdout.isTTY) {
760
760
  : 'stdout is';
761
761
  printNonTtyErrorAndExit(missing, true);
762
762
  }
763
- // Welcome screen — shown on every fresh interactive session before TUI takes over.
764
- // Skip when the first-run banner was already printed in loader.ts (prevents double banner).
765
- if (!process.env.GSD_FIRST_RUN_BANNER) {
766
- const { printWelcomeScreen } = await import('./welcome-screen.js');
767
- let remoteChannel;
768
- try {
769
- const { resolveRemoteConfig } = await import('./resources/extensions/remote-questions/config.js');
770
- const rc = resolveRemoteConfig();
771
- if (rc)
772
- remoteChannel = rc.channel;
773
- }
774
- catch { /* non-fatal */ }
775
- printWelcomeScreen({
776
- version: process.env.GSD_VERSION || '0.0.0',
777
- modelName: settingsManager.getDefaultModel() || undefined,
778
- provider: settingsManager.getDefaultProvider() || undefined,
779
- remoteChannel,
780
- });
781
- }
782
763
  const interactiveMode = new InteractiveMode(session);
783
764
  markStartup('InteractiveMode');
784
765
  printStartupTimings();
@@ -1 +1 @@
1
- 9ae4402464ddbafa
1
+ 83a1a81197451245
@@ -261,6 +261,26 @@ function makeErrorMessage(model, errorMsg) {
261
261
  timestamp: Date.now(),
262
262
  };
263
263
  }
264
+ export function isClaudeCodeAbortErrorMessage(message) {
265
+ if (!message)
266
+ return false;
267
+ return /\b(?:claude code process aborted by user|request aborted by user|process aborted by user)\b/i.test(message);
268
+ }
269
+ function isBareClaudeCodeAbortErrorMessage(message) {
270
+ if (!message)
271
+ return false;
272
+ const normalized = message.trim().replace(/\s+/g, " ").toLowerCase();
273
+ return normalized === "claude code process aborted by user"
274
+ || normalized === "request aborted by user"
275
+ || normalized === "process aborted by user";
276
+ }
277
+ export function resolveClaudeCodeAbortedMessageText(errorMsg, lastTextContent) {
278
+ const trimmedError = errorMsg.trim();
279
+ if (trimmedError && !isBareClaudeCodeAbortErrorMessage(trimmedError)) {
280
+ return trimmedError;
281
+ }
282
+ return lastTextContent;
283
+ }
264
284
  /**
265
285
  * Generator exhaustion without a terminal result means the SDK stream was
266
286
  * interrupted mid-turn. Surface it as an error so downstream recovery logic
@@ -1512,6 +1532,15 @@ async function pumpSdkMessages(model, context, options, stream) {
1512
1532
  }
1513
1533
  catch (err) {
1514
1534
  const errorMsg = err instanceof Error ? err.message : String(err);
1535
+ if (options?.signal?.aborted || isClaudeCodeAbortErrorMessage(errorMsg)) {
1536
+ const abortedText = resolveClaudeCodeAbortedMessageText(errorMsg, lastTextContent);
1537
+ stream.push({
1538
+ type: "error",
1539
+ reason: "aborted",
1540
+ error: makeAbortedMessage(modelId, abortedText),
1541
+ });
1542
+ return;
1543
+ }
1515
1544
  stream.push({
1516
1545
  type: "error",
1517
1546
  reason: "error",
@@ -16,7 +16,7 @@ import { ModelPolicyDispatchBlockedError } from "../auto-model-selection.js";
16
16
  import { resolveEngine } from "../engine-resolver.js";
17
17
  import { logWarning } from "../workflow-logger.js";
18
18
  import { recordDispatchClaim, markRunning as markDispatchRunning, markCompleted as markDispatchCompleted, markFailed as markDispatchFailed, getRecentForUnit as getRecentDispatchesForUnit, getRecentUnitKeysForProjectRoot, } from "../db/unit-dispatches.js";
19
- import { refreshMilestoneLease } from "../db/milestone-leases.js";
19
+ import { claimMilestoneLease, refreshMilestoneLease } from "../db/milestone-leases.js";
20
20
  import { heartbeatAutoWorker } from "../db/auto-workers.js";
21
21
  import { getRuntimeKv, setRuntimeKv } from "../db/runtime-kv.js";
22
22
  import { resolveUokFlags } from "../uok/flags.js";
@@ -27,7 +27,7 @@ import { hydrateCustomVerifyRetryCounts, saveCustomVerifyRetryCounts, } from "./
27
27
  import { settleDispatchCompleted, settleDispatchFailed, } from "./workflow-dispatch-ledger.js";
28
28
  import { emitOpenUnitEndForUnit } from "../crash-recovery.js";
29
29
  import { writeUnitRuntimeRecord } from "../unit-runtime.js";
30
- import { openDispatchClaim } from "./workflow-dispatch-claim.js";
30
+ import { ensureDispatchLease, openDispatchClaim } from "./workflow-dispatch-claim.js";
31
31
  import { completeWorkflowIteration } from "./workflow-iteration-completion.js";
32
32
  import { createWorkflowJournalReporter } from "./workflow-journal-reporter.js";
33
33
  import { createWorkflowPhaseReporter } from "./workflow-phase-reporter.js";
@@ -105,6 +105,18 @@ function logDispatchClaimFailed(err) {
105
105
  error: err instanceof Error ? err.message : String(err),
106
106
  });
107
107
  }
108
+ function logDispatchLeaseRecovered(details) {
109
+ debugLog("autoLoop", {
110
+ phase: details.recovered ? "dispatch-lease-recovered" : "dispatch-lease-acquired",
111
+ ...details,
112
+ });
113
+ }
114
+ function logDispatchLeaseRecoveryFailed(details) {
115
+ debugLog("autoLoop", {
116
+ phase: "dispatch-lease-recovery-failed",
117
+ ...details,
118
+ });
119
+ }
108
120
  function logCustomVerifyRetryLoadFailure(err) {
109
121
  debugLog("autoLoop", {
110
122
  phase: "load-custom-verify-retries-failed",
@@ -186,6 +198,10 @@ export async function autoLoop(ctx, pi, s, deps, options) {
186
198
  phase: "heartbeat-failed",
187
199
  error: err instanceof Error ? err.message : String(err),
188
200
  }),
201
+ logLeaseRefreshMiss: details => debugLog("autoLoop", {
202
+ phase: "lease-refresh-missed",
203
+ ...details,
204
+ }),
189
205
  });
190
206
  // ── Journal: per-iteration flow grouping ──
191
207
  const flowId = randomUUID();
@@ -581,23 +597,70 @@ export async function autoLoop(ctx, pi, s, deps, options) {
581
597
  await enforceMinRequestInterval(s, prefs);
582
598
  // Phase B: claim a unit_dispatches row before invoking the unit. The
583
599
  // partial unique index idx_unit_dispatches_active_per_unit prevents
584
- // a second worker from claiming the same unit concurrently. Returns
585
- // null when DB unavailable, no worker registered, or no active lease
586
- // those degraded paths fall through to the existing single-worker
587
- // semantics with no ledger entry, preserving back-compat.
588
- const dispatchClaim = openDispatchClaim(s, flowId, turnId, iterData, {
600
+ // a second worker from claiming the same unit concurrently. When this
601
+ // process has a worker identity, make the milestone lease explicit before
602
+ // claiming so a step-mode handoff cannot leave us running with a stale
603
+ // in-memory token and no backing lease row.
604
+ const leaseBeforeClaim = ensureDispatchLease(s, iterData.mid, {
605
+ claimMilestoneLease,
606
+ logLeaseRecovered: logDispatchLeaseRecovered,
607
+ logLeaseRecoveryFailed: logDispatchLeaseRecoveryFailed,
608
+ });
609
+ if (leaseBeforeClaim.kind === "blocked" || leaseBeforeClaim.kind === "failed") {
610
+ const msg = `Lost milestone lease for ${iterData.mid ?? "unknown"} before dispatching ${iterData.unitType} ${iterData.unitId}: ${leaseBeforeClaim.reason}`;
611
+ ctx.ui.notify(msg, "error");
612
+ finishTurn("stopped", "execution", msg);
613
+ await deps.stopAuto(ctx, pi, msg);
614
+ break;
615
+ }
616
+ let dispatchClaim = openDispatchClaim(s, flowId, turnId, iterData, {
589
617
  getRecentDispatchesForUnit,
590
618
  recordDispatchClaim,
591
619
  markDispatchRunning,
592
620
  logClaimRejected: logDispatchClaimRejected,
593
621
  logClaimFailed: logDispatchClaimFailed,
594
622
  });
595
- const dispatchDecision = decideDispatchClaim(dispatchClaim.kind === "opened"
623
+ let dispatchDecision = decideDispatchClaim(dispatchClaim.kind === "opened"
596
624
  ? { kind: "opened", dispatchId: dispatchClaim.dispatchId }
597
625
  : dispatchClaim.kind === "skip"
598
626
  ? { kind: "skip", reason: dispatchClaim.reason }
599
627
  : { kind: "degraded" });
628
+ if (dispatchDecision.action === "skip" && dispatchDecision.reason === "stale-lease") {
629
+ const leaseRecovery = ensureDispatchLease(s, iterData.mid, {
630
+ claimMilestoneLease,
631
+ logLeaseRecovered: logDispatchLeaseRecovered,
632
+ logLeaseRecoveryFailed: logDispatchLeaseRecoveryFailed,
633
+ }, { forceReclaim: true });
634
+ if (leaseRecovery.kind === "ready") {
635
+ dispatchClaim = openDispatchClaim(s, flowId, turnId, iterData, {
636
+ getRecentDispatchesForUnit,
637
+ recordDispatchClaim,
638
+ markDispatchRunning,
639
+ logClaimRejected: logDispatchClaimRejected,
640
+ logClaimFailed: logDispatchClaimFailed,
641
+ });
642
+ dispatchDecision = decideDispatchClaim(dispatchClaim.kind === "opened"
643
+ ? { kind: "opened", dispatchId: dispatchClaim.dispatchId }
644
+ : dispatchClaim.kind === "skip"
645
+ ? { kind: "skip", reason: dispatchClaim.reason }
646
+ : { kind: "degraded" });
647
+ }
648
+ else {
649
+ const msg = `Lost milestone lease for ${iterData.mid ?? "unknown"} while claiming ${iterData.unitType} ${iterData.unitId}: ${leaseRecovery.reason}`;
650
+ ctx.ui.notify(msg, "error");
651
+ finishTurn("stopped", "execution", msg);
652
+ await deps.stopAuto(ctx, pi, msg);
653
+ break;
654
+ }
655
+ }
600
656
  if (dispatchDecision.action === "skip") {
657
+ if (dispatchDecision.reason === "stale-lease") {
658
+ const msg = `Lost milestone lease for ${iterData.mid ?? "unknown"} while claiming ${iterData.unitType} ${iterData.unitId}; dispatch claim still failed after recovery.`;
659
+ ctx.ui.notify(msg, "error");
660
+ finishTurn("stopped", "execution", msg);
661
+ await deps.stopAuto(ctx, pi, msg);
662
+ break;
663
+ }
601
664
  finishTurn("skipped", "execution", dispatchDecision.reason);
602
665
  continue;
603
666
  }
@@ -42,6 +42,11 @@ import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit,
42
42
  function isSamePathLocal(a, b) {
43
43
  return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
44
44
  }
45
+ export function shouldDegradeEmptyWorktreeToProjectRoot(worktreeClassification, projectRootClassification) {
46
+ return (worktreeClassification.kind === "greenfield" &&
47
+ projectRootClassification.kind !== "greenfield" &&
48
+ projectRootClassification.kind !== "invalid-repo");
49
+ }
45
50
  // ─── Session timeout auto-resume state ────────────────────────────────────────
46
51
  let consecutiveSessionTimeouts = 0;
47
52
  const MAX_SESSION_TIMEOUT_AUTO_RESUMES = 3;
@@ -129,6 +134,74 @@ async function closeoutAndStop(ctx, pi, s, deps, reason) {
129
134
  }
130
135
  await deps.stopAuto(ctx, pi, reason);
131
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
+ }
132
205
  async function emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, errorContext) {
133
206
  ic.deps.emitJournalEvent({
134
207
  ts: new Date().toISOString(),
@@ -470,28 +543,12 @@ export async function runPreDispatch(ic, loopState) {
470
543
  s.unitLifetimeDispatches.clear();
471
544
  loopState.recentUnits.length = 0;
472
545
  loopState.stuckRecoveryAttempts = 0;
473
- // Worktree lifecycle on milestone transition — merge current, enter next
474
- // #2909: preflight warn + stash dirty working tree before merge
475
- const preflightTransition = deps.preflightCleanRoot(s.originalBasePath || s.basePath, s.currentMilestoneId, ctx.ui.notify.bind(ctx.ui));
476
- try {
477
- deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
478
- }
479
- catch (mergeErr) {
480
- if (mergeErr instanceof MergeConflictError) {
481
- // Real code conflicts — stop the loop instead of retrying forever (#2330)
482
- ctx.ui.notify(`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
483
- await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
484
- return { action: "break", reason: "merge-conflict" };
485
- }
486
- // Non-conflict merge errors — stop auto to avoid advancing with unmerged work
487
- logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
488
- ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
489
- await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
490
- return { action: "break", reason: "merge-failed" };
491
- }
492
- // #2909: postflight — restore stashed changes after successful merge
493
- if (preflightTransition.stashPushed) {
494
- 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;
495
552
  }
496
553
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
497
554
  deps.invalidateAllCaches();
@@ -545,30 +602,12 @@ export async function runPreDispatch(ic, loopState) {
545
602
  }
546
603
  const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
547
604
  if (incomplete.length === 0 && state.registry.length > 0) {
548
- // All milestones complete — merge milestone branch before stopping
605
+ // All milestones complete — merge milestone branch before stopping.
549
606
  if (s.currentMilestoneId) {
550
- // #2909: preflight warn + stash dirty working tree before merge
551
- const preflightAllComplete = deps.preflightCleanRoot(s.originalBasePath || s.basePath, s.currentMilestoneId, ctx.ui.notify.bind(ctx.ui));
552
- try {
553
- deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
554
- // Prevent stopAuto from attempting the same merge (#2645)
555
- s.milestoneMergedInPhases = true;
556
- }
557
- catch (mergeErr) {
558
- if (mergeErr instanceof MergeConflictError) {
559
- ctx.ui.notify(`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
560
- await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
561
- return { action: "break", reason: "merge-conflict" };
562
- }
563
- logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
564
- ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
565
- await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
566
- return { action: "break", reason: "merge-failed" };
567
- }
568
- // #2909: postflight — restore stashed changes after successful merge
569
- if (preflightAllComplete.stashPushed) {
570
- deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightAllComplete.stashMarker, ctx.ui.notify.bind(ctx.ui));
571
- }
607
+ // #2909 / #5538-followup: preflight stash + always-on postflight pop.
608
+ const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
609
+ if (stop)
610
+ return stop;
572
611
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
573
612
  }
574
613
  deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone", basename(s.originalBasePath || s.basePath));
@@ -631,30 +670,12 @@ export async function runPreDispatch(ic, loopState) {
631
670
  }
632
671
  // Terminal: complete
633
672
  if (state.phase === "complete") {
634
- // Milestone merge on complete (before closeout so branch state is clean)
673
+ // Milestone merge on complete (before closeout so branch state is clean).
635
674
  if (s.currentMilestoneId) {
636
- // #2909: preflight warn + stash dirty working tree before merge
637
- const preflightComplete = deps.preflightCleanRoot(s.originalBasePath || s.basePath, s.currentMilestoneId, ctx.ui.notify.bind(ctx.ui));
638
- try {
639
- deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
640
- // Prevent stopAuto from attempting the same merge (#2645)
641
- s.milestoneMergedInPhases = true;
642
- }
643
- catch (mergeErr) {
644
- if (mergeErr instanceof MergeConflictError) {
645
- ctx.ui.notify(`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
646
- await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
647
- return { action: "break", reason: "merge-conflict" };
648
- }
649
- logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
650
- ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
651
- await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
652
- return { action: "break", reason: "merge-failed" };
653
- }
654
- // #2909: postflight — restore stashed changes after successful merge
655
- if (preflightComplete.stashPushed) {
656
- deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightComplete.stashMarker, ctx.ui.notify.bind(ctx.ui));
657
- }
675
+ // #2909 / #5538-followup: preflight stash + always-on postflight pop.
676
+ const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
677
+ if (stop)
678
+ return stop;
658
679
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
659
680
  }
660
681
  deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone", basename(s.originalBasePath || s.basePath));
@@ -747,6 +768,48 @@ export async function runDispatch(ic, preData, loopState) {
747
768
  let unitId = dispatchResult.unitId;
748
769
  let prompt = dispatchResult.prompt;
749
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
+ }
750
813
  // ── Sliding-window stuck detection with graduated recovery ──
751
814
  const derivedKey = `${unitType}/${unitId}`;
752
815
  // Always record this dispatch in the sliding window so detectStuck() has
@@ -863,32 +926,6 @@ export async function runDispatch(ic, preData, loopState) {
863
926
  }
864
927
  }
865
928
  }
866
- // Pre-dispatch hooks
867
- const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
868
- if (preDispatchResult.firedHooks.length > 0) {
869
- ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
870
- deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
871
- }
872
- if (preDispatchResult.action === "skip") {
873
- ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
874
- await new Promise((r) => setImmediate(r));
875
- return { action: "continue" };
876
- }
877
- if (preDispatchResult.action === "replace") {
878
- prompt = preDispatchResult.prompt ?? prompt;
879
- if (preDispatchResult.unitType)
880
- unitType = preDispatchResult.unitType;
881
- }
882
- else if (preDispatchResult.prompt) {
883
- prompt = preDispatchResult.prompt;
884
- }
885
- const guardBasePath = _resolveDispatchGuardBasePath(s);
886
- const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(guardBasePath, deps.getMainBranch(guardBasePath), unitType, unitId);
887
- if (priorSliceBlocker) {
888
- await deps.stopAuto(ctx, pi, priorSliceBlocker);
889
- debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
890
- return { action: "break", reason: "prior-slice-blocker" };
891
- }
892
929
  return {
893
930
  action: "next",
894
931
  data: {
@@ -1118,6 +1155,25 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1118
1155
  }
1119
1156
  }
1120
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") {
1121
1177
  debugLog("runUnitPhase", { phase: "worktree-health-greenfield", basePath: s.basePath, classification: projectClassification });
1122
1178
  ctx.ui.notify(`Warning: ${s.basePath} has no project content yet — proceeding as greenfield project`, "warning");
1123
1179
  }
@@ -18,6 +18,8 @@ import { bumpTurnGeneration } from "./turn-epoch.js";
18
18
  let _currentResolve = null;
19
19
  let _sessionSwitchInFlight = false;
20
20
  let _pendingSwitchCancellation = null;
21
+ let _sessionSwitchAbortGraceUntil = 0;
22
+ const DEFAULT_SESSION_SWITCH_ABORT_GRACE_MS = 2_000;
21
23
  // ─── Setters (needed for cross-module mutation) ─────────────────────────────
22
24
  export function _setCurrentResolve(fn) {
23
25
  _currentResolve = fn;
@@ -25,6 +27,15 @@ export function _setCurrentResolve(fn) {
25
27
  export function _setSessionSwitchInFlight(v) {
26
28
  _sessionSwitchInFlight = v;
27
29
  }
30
+ export function _markSessionSwitchAbortGraceWindow(durationMs = DEFAULT_SESSION_SWITCH_ABORT_GRACE_MS) {
31
+ _sessionSwitchAbortGraceUntil = Math.max(_sessionSwitchAbortGraceUntil, Date.now() + durationMs);
32
+ }
33
+ export function _clearSessionSwitchAbortGraceWindow() {
34
+ _sessionSwitchAbortGraceUntil = 0;
35
+ }
36
+ export function isSessionSwitchAbortGraceActive(now = Date.now()) {
37
+ return now < _sessionSwitchAbortGraceUntil;
38
+ }
28
39
  export function _clearCurrentResolve() {
29
40
  _currentResolve = null;
30
41
  }
@@ -118,6 +129,7 @@ export function _resetPendingResolve() {
118
129
  _currentResolve = null;
119
130
  _sessionSwitchInFlight = false;
120
131
  _pendingSwitchCancellation = null;
132
+ _sessionSwitchAbortGraceUntil = 0;
121
133
  }
122
134
  export function _hasPendingResolveForTest() {
123
135
  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, _consumePendingSwitchCancellation, _setCurrentResolve, _setSessionSwitchInFlight, } from "./resolve.js";
3
+ import { _clearCurrentResolve, _consumePendingSwitchCancellation, _markSessionSwitchAbortGraceWindow, _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,44 +19,23 @@ 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) {
38
+ _markSessionSwitchAbortGraceWindow();
60
39
  _setSessionSwitchInFlight(false);
61
40
  }
62
41
  });
@@ -111,6 +90,7 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
111
90
  // ── Create the agent_end promise (per-unit one-shot) ──
112
91
  // This happens after newSession completes so session-switch agent_end events
113
92
  // from the previous session cannot resolve the new unit.
93
+ _markSessionSwitchAbortGraceWindow();
114
94
  _setSessionSwitchInFlight(false);
115
95
  const unitPromise = new Promise((resolve) => {
116
96
  _setCurrentResolve(resolve);
@@ -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;