@opengsd/gsd-pi 1.2.0-dev.d6c5343c → 1.2.0-dev.ddc97c10

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 (225) hide show
  1. package/dist/mcp-server.js +2 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/orchestrator.js +28 -10
  4. package/dist/resources/extensions/gsd/auto/phases.js +47 -4
  5. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +3 -2
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +11 -2
  8. package/dist/resources/extensions/gsd/auto-model-selection.js +11 -7
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +18 -6
  10. package/dist/resources/extensions/gsd/auto-unit-closeout.js +45 -21
  11. package/dist/resources/extensions/gsd/auto-verification.js +14 -2
  12. package/dist/resources/extensions/gsd/auto.js +37 -1
  13. package/dist/resources/extensions/gsd/blocked-models.js +28 -0
  14. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +26 -6
  15. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
  16. package/dist/resources/extensions/gsd/closeout-wizard.js +92 -0
  17. package/dist/resources/extensions/gsd/commands/context.js +16 -2
  18. package/dist/resources/extensions/gsd/commands-handlers.js +46 -3
  19. package/dist/resources/extensions/gsd/consent-question.js +16 -0
  20. package/dist/resources/extensions/gsd/crash-recovery.js +8 -3
  21. package/dist/resources/extensions/gsd/doctor-engine-checks.js +3 -3
  22. package/dist/resources/extensions/gsd/doctor-git-checks.js +2 -18
  23. package/dist/resources/extensions/gsd/gsd-command-home.js +22 -12
  24. package/dist/resources/extensions/gsd/gsd-db.js +2 -1
  25. package/dist/resources/extensions/gsd/guided-flow.js +6 -3
  26. package/dist/resources/extensions/gsd/milestone-closeout.js +73 -2
  27. package/dist/resources/extensions/gsd/milestone-planning-persistence.js +2 -2
  28. package/dist/resources/extensions/gsd/projection-flush.js +7 -0
  29. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  30. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  31. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  33. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -1
  34. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  36. package/dist/resources/extensions/gsd/prompts/replan-slice.md +1 -1
  37. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  39. package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  40. package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -1
  41. package/dist/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  42. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +1 -1
  43. package/dist/resources/extensions/gsd/roadmap-slices.js +25 -3
  44. package/dist/resources/extensions/gsd/session-lock.js +1 -1
  45. package/dist/resources/extensions/gsd/tool-contract.js +14 -3
  46. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -2
  47. package/dist/resources/extensions/gsd/tools/complete-slice.js +2 -2
  48. package/dist/resources/extensions/gsd/tools/complete-task.js +3 -2
  49. package/dist/resources/extensions/gsd/tools/plan-slice.js +2 -2
  50. package/dist/resources/extensions/gsd/tools/plan-task.js +2 -2
  51. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +2 -2
  52. package/dist/resources/extensions/gsd/tools/reopen-milestone.js +2 -2
  53. package/dist/resources/extensions/gsd/tools/reopen-slice.js +2 -2
  54. package/dist/resources/extensions/gsd/tools/reopen-task.js +2 -2
  55. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -2
  56. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +67 -2
  57. package/dist/resources/extensions/gsd/verification-verdict.js +2 -1
  58. package/dist/resources/extensions/shared/gsd-browser-cli.js +21 -2
  59. package/dist/resources/shared/gsd-browser-path-sync.js +214 -0
  60. package/dist/resources/shared/package-manager-detection.js +1 -1
  61. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  62. package/dist/update-check.d.ts +2 -0
  63. package/dist/update-check.js +24 -1
  64. package/dist/update-cmd.js +20 -3
  65. package/dist/web/standalone/.next/BUILD_ID +1 -1
  66. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  67. package/dist/web/standalone/.next/build-manifest.json +2 -2
  68. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  69. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/index.html +1 -1
  86. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  93. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  94. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  96. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  97. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  98. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  99. package/package.json +1 -1
  100. package/packages/cloud-mcp-gateway/package.json +2 -2
  101. package/packages/contracts/package.json +1 -1
  102. package/packages/daemon/package.json +4 -4
  103. package/packages/gsd-agent-core/package.json +5 -5
  104. package/packages/gsd-agent-modes/package.json +7 -7
  105. package/packages/mcp-server/dist/cli.js +10 -5
  106. package/packages/mcp-server/dist/cli.js.map +1 -1
  107. package/packages/mcp-server/dist/moonshot-tool-schema.d.ts +29 -0
  108. package/packages/mcp-server/dist/moonshot-tool-schema.d.ts.map +1 -0
  109. package/packages/mcp-server/dist/moonshot-tool-schema.js +50 -0
  110. package/packages/mcp-server/dist/moonshot-tool-schema.js.map +1 -0
  111. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  112. package/packages/mcp-server/dist/server.js +4 -0
  113. package/packages/mcp-server/dist/server.js.map +1 -1
  114. package/packages/mcp-server/dist/workflow-tools.d.ts +18 -18
  115. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  116. package/packages/mcp-server/dist/workflow-tools.js +99 -38
  117. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  118. package/packages/mcp-server/package.json +5 -4
  119. package/packages/native/package.json +1 -1
  120. package/packages/pi-agent-core/package.json +1 -1
  121. package/packages/pi-ai/dist/index.d.ts +2 -0
  122. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  123. package/packages/pi-ai/dist/index.js +2 -0
  124. package/packages/pi-ai/dist/index.js.map +1 -1
  125. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  126. package/packages/pi-ai/dist/providers/anthropic.js +12 -7
  127. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  128. package/packages/pi-ai/dist/providers/google-shared.d.ts +5 -0
  129. package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
  130. package/packages/pi-ai/dist/providers/google-shared.js +12 -3
  131. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  132. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  133. package/packages/pi-ai/dist/providers/openai-completions.js +7 -3
  134. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  135. package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts +9 -0
  136. package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts.map +1 -0
  137. package/packages/pi-ai/dist/utils/moonshot-tool-schema.js +34 -0
  138. package/packages/pi-ai/dist/utils/moonshot-tool-schema.js.map +1 -0
  139. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  140. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +6 -2
  141. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  142. package/packages/pi-ai/package.json +1 -1
  143. package/packages/pi-coding-agent/package.json +7 -7
  144. package/packages/pi-tui/package.json +2 -2
  145. package/packages/rpc-client/package.json +2 -2
  146. package/pkg/package.json +1 -1
  147. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +11 -0
  148. package/src/resources/extensions/gsd/auto/orchestrator.ts +28 -10
  149. package/src/resources/extensions/gsd/auto/phases.ts +63 -24
  150. package/src/resources/extensions/gsd/auto/session.ts +3 -0
  151. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +10 -16
  152. package/src/resources/extensions/gsd/auto-dispatch.ts +11 -10
  153. package/src/resources/extensions/gsd/auto-model-selection.ts +16 -7
  154. package/src/resources/extensions/gsd/auto-post-unit.ts +21 -6
  155. package/src/resources/extensions/gsd/auto-unit-closeout.ts +83 -28
  156. package/src/resources/extensions/gsd/auto-verification.ts +18 -2
  157. package/src/resources/extensions/gsd/auto.ts +44 -1
  158. package/src/resources/extensions/gsd/blocked-models.ts +49 -0
  159. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +34 -5
  160. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
  161. package/src/resources/extensions/gsd/closeout-wizard.ts +102 -0
  162. package/src/resources/extensions/gsd/commands/context.ts +16 -2
  163. package/src/resources/extensions/gsd/commands-handlers.ts +46 -3
  164. package/src/resources/extensions/gsd/consent-question.ts +15 -0
  165. package/src/resources/extensions/gsd/crash-recovery.ts +10 -2
  166. package/src/resources/extensions/gsd/doctor-engine-checks.ts +3 -3
  167. package/src/resources/extensions/gsd/doctor-git-checks.ts +2 -19
  168. package/src/resources/extensions/gsd/gsd-command-home.ts +13 -3
  169. package/src/resources/extensions/gsd/gsd-db.ts +4 -3
  170. package/src/resources/extensions/gsd/guided-flow.ts +21 -26
  171. package/src/resources/extensions/gsd/milestone-closeout.ts +97 -2
  172. package/src/resources/extensions/gsd/milestone-planning-persistence.ts +2 -2
  173. package/src/resources/extensions/gsd/projection-flush.ts +20 -0
  174. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  175. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  176. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  177. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  178. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -1
  179. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  180. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  181. package/src/resources/extensions/gsd/prompts/replan-slice.md +1 -1
  182. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  183. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  184. package/src/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  185. package/src/resources/extensions/gsd/prompts/run-uat.md +1 -1
  186. package/src/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  187. package/src/resources/extensions/gsd/prompts/validate-milestone.md +1 -1
  188. package/src/resources/extensions/gsd/roadmap-slices.ts +28 -3
  189. package/src/resources/extensions/gsd/session-lock.ts +1 -1
  190. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +69 -0
  191. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +97 -0
  192. package/src/resources/extensions/gsd/tests/auto-remote-session-lock-cleanup.test.ts +65 -3
  193. package/src/resources/extensions/gsd/tests/blocked-models.test.ts +19 -0
  194. package/src/resources/extensions/gsd/tests/consent-question.test.ts +15 -0
  195. package/src/resources/extensions/gsd/tests/doctor-git-checks-terminal.test.ts +73 -0
  196. package/src/resources/extensions/gsd/tests/gsd-command-home.test.ts +120 -0
  197. package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +2 -6
  198. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +95 -4
  199. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
  200. package/src/resources/extensions/gsd/tests/phases-terminal-complete-idempotent.test.ts +242 -0
  201. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +63 -2
  202. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +68 -0
  203. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +19 -1
  204. package/src/resources/extensions/gsd/tests/tool-unavailable-retry.test.ts +33 -0
  205. package/src/resources/extensions/gsd/tests/transport-gate-double-complete.test.ts +139 -0
  206. package/src/resources/extensions/gsd/tests/verification-verdict.test.ts +2 -0
  207. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +273 -38
  208. package/src/resources/extensions/gsd/tool-contract.ts +38 -3
  209. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
  210. package/src/resources/extensions/gsd/tools/complete-slice.ts +2 -2
  211. package/src/resources/extensions/gsd/tools/complete-task.ts +3 -2
  212. package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -2
  213. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -2
  214. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +2 -2
  215. package/src/resources/extensions/gsd/tools/reopen-milestone.ts +2 -2
  216. package/src/resources/extensions/gsd/tools/reopen-slice.ts +2 -2
  217. package/src/resources/extensions/gsd/tools/reopen-task.ts +2 -2
  218. package/src/resources/extensions/gsd/tools/replan-slice.ts +2 -2
  219. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +81 -2
  220. package/src/resources/extensions/gsd/verification-verdict.ts +4 -2
  221. package/src/resources/extensions/shared/gsd-browser-cli.ts +23 -2
  222. package/src/resources/shared/gsd-browser-path-sync.ts +273 -0
  223. package/src/resources/shared/package-manager-detection.ts +1 -1
  224. /package/dist/web/standalone/.next/static/{jmTLg6xZmAuq_LIqKOxrH → McokybTayhff1xEVc-d3T}/_buildManifest.js +0 -0
  225. /package/dist/web/standalone/.next/static/{jmTLg6xZmAuq_LIqKOxrH → McokybTayhff1xEVc-d3T}/_ssgManifest.js +0 -0
@@ -2,7 +2,7 @@
2
2
  import { logWarning } from "../workflow-logger.js";
3
3
  import { checkDeepProjectSetupAfterTurn, checkAutoStartAfterDiscuss, maybeHandleReadyPhraseWithoutFiles, maybeHandleEmptyIntentTurn, resetEmptyTurnCounter, } from "../guided-flow.js";
4
4
  import { clearPathCache } from "../paths.js";
5
- import { getAutoDashboardData, getAutoModeStartModel, isAutoActive, isAutoCompletionStopInProgress, pauseAuto, setCurrentDispatchedModelId, } from "../auto.js";
5
+ import { getAutoDashboardData, getAutoModeStartModel, isAutoActive, isAutoCompletionStopInProgress, pauseAuto, setCurrentDispatchedModelId, setCurrentUnitModelForRecovery, } from "../auto.js";
6
6
  import { getNextFallbackModel, resolveModelWithFallbacksForUnit } from "../preferences.js";
7
7
  import { pauseAutoForProviderError } from "../provider-error-pause.js";
8
8
  import { isSessionSwitchAbortGraceActive, isSessionSwitchInFlight, resolveAgentEnd, resolveAgentEndCancelled, } from "../auto/resolve.js";
@@ -13,7 +13,7 @@ import { clearDiscussionFlowState } from "./write-gate.js";
13
13
  import { clearGuidedUnitContext } from "../guided-unit-context.js";
14
14
  import { resumeAutoAfterProviderDelay } from "./provider-error-resume.js";
15
15
  import { classifyError, createRetryState, resetRetryState, isTransient, } from "../error-classifier.js";
16
- import { blockModel, isModelBlocked } from "../blocked-models.js";
16
+ import { blockModel, blockModelUntil, isModelBlocked, isModelTemporarilyUnavailable } from "../blocked-models.js";
17
17
  import { getProjectGSDPreferencesPath } from "../preferences.js";
18
18
  import { resolveProviderErrorGuidance } from "../provider-error-guidance.js";
19
19
  import { formatGuidance } from "../guidance.js";
@@ -96,9 +96,12 @@ async function tryProviderModelFallback(params) {
96
96
  if (!nextModelId)
97
97
  break;
98
98
  const candidate = resolveModelId(nextModelId, availableModels, rejectedProvider);
99
- if (candidate && !isModelBlocked(basePath, candidate.provider, candidate.id)) {
99
+ if (candidate &&
100
+ !isModelBlocked(basePath, candidate.provider, candidate.id) &&
101
+ !isModelTemporarilyUnavailable(basePath, candidate.provider, candidate.id)) {
100
102
  const ok = await pi.setModel(candidate, { persist: false });
101
103
  if (ok) {
104
+ setCurrentUnitModelForRecovery(candidate);
102
105
  setCurrentDispatchedModelId({ provider: candidate.provider, id: candidate.id });
103
106
  switchedNotify(`${candidate.provider}/${candidate.id}`);
104
107
  pi.sendMessage({ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
@@ -111,11 +114,13 @@ async function tryProviderModelFallback(params) {
111
114
  const sessionModel = getAutoModeStartModel();
112
115
  if (sessionModel &&
113
116
  !(sessionModel.provider === rejectedProvider && sessionModel.id === rejectedId) &&
114
- !isModelBlocked(basePath, sessionModel.provider, sessionModel.id)) {
117
+ !isModelBlocked(basePath, sessionModel.provider, sessionModel.id) &&
118
+ !isModelTemporarilyUnavailable(basePath, sessionModel.provider, sessionModel.id)) {
115
119
  const startModel = availableModels.find((m) => m.provider === sessionModel.provider && m.id === sessionModel.id);
116
120
  if (startModel) {
117
121
  const ok = await pi.setModel(startModel, { persist: false });
118
122
  if (ok) {
123
+ setCurrentUnitModelForRecovery(startModel);
119
124
  setCurrentDispatchedModelId({ provider: startModel.provider, id: startModel.id });
120
125
  switchedNotify(`${startModel.provider}/${startModel.id}`);
121
126
  pi.sendMessage({ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
@@ -533,6 +538,10 @@ export async function handleAgentEnd(pi, event, ctx) {
533
538
  if (currentProvider === "openai-codex" || currentProvider === "google-gemini-cli") {
534
539
  cls.retryAfterMs = Math.min(cls.retryAfterMs, 30_000);
535
540
  }
541
+ const dash = getAutoDashboardData();
542
+ if (dash.basePath && ctx.model?.provider && ctx.model?.id) {
543
+ blockModelUntil(dash.basePath, ctx.model.provider, ctx.model.id, Date.now() + cls.retryAfterMs, rawErrorMsg || displayMsg || "rate limit");
544
+ }
536
545
  }
537
546
  // ── 2. Decide & Act ──────────────────────────────────────────────────
538
547
  // --- Network errors: same-model retry with backoff ---
@@ -572,9 +581,14 @@ export async function handleAgentEnd(pi, event, ctx) {
572
581
  retryState.networkRetryCount = 0;
573
582
  retryState.currentRetryModelId = undefined;
574
583
  const modelToSet = resolveModelId(nextModelId, availableModels, ctx.model?.provider);
575
- if (modelToSet) {
584
+ const modelUnavailable = dash.basePath && modelToSet
585
+ ? isModelBlocked(dash.basePath, modelToSet.provider, modelToSet.id) ||
586
+ isModelTemporarilyUnavailable(dash.basePath, modelToSet.provider, modelToSet.id)
587
+ : false;
588
+ if (modelToSet && !modelUnavailable) {
576
589
  const ok = await pi.setModel(modelToSet, { persist: false });
577
590
  if (ok) {
591
+ setCurrentUnitModelForRecovery(modelToSet);
578
592
  setCurrentDispatchedModelId({ provider: modelToSet.provider, id: modelToSet.id });
579
593
  ctx.ui.notify(`Model error${errorDetail}. Switched to fallback: ${nextModelId} and resuming.`, "warning");
580
594
  pi.sendMessage({ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
@@ -587,11 +601,17 @@ export async function handleAgentEnd(pi, event, ctx) {
587
601
  // Try restoring session model
588
602
  const sessionModel = getAutoModeStartModel();
589
603
  if (sessionModel) {
590
- if (ctx.model?.id !== sessionModel.id || ctx.model?.provider !== sessionModel.provider) {
604
+ const dash = getAutoDashboardData();
605
+ const sessionModelUnavailable = dash.basePath
606
+ ? isModelBlocked(dash.basePath, sessionModel.provider, sessionModel.id) ||
607
+ isModelTemporarilyUnavailable(dash.basePath, sessionModel.provider, sessionModel.id)
608
+ : false;
609
+ if (!sessionModelUnavailable && (ctx.model?.id !== sessionModel.id || ctx.model?.provider !== sessionModel.provider)) {
591
610
  const startModel = ctx.modelRegistry.getAvailable().find((m) => m.provider === sessionModel.provider && m.id === sessionModel.id);
592
611
  if (startModel) {
593
612
  const ok = await pi.setModel(startModel, { persist: false });
594
613
  if (ok) {
614
+ setCurrentUnitModelForRecovery(startModel);
595
615
  setCurrentDispatchedModelId({ provider: startModel.provider, id: startModel.id });
596
616
  retryState.networkRetryCount = 0;
597
617
  retryState.currentRetryModelId = undefined;
@@ -114,8 +114,8 @@ export function registerExecTools(pi) {
114
114
  ],
115
115
  parameters: Type.Object({
116
116
  query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
117
- runtime: Type.Optional(Type.Union([Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")], {
118
- description: "Restrict to one runtime.",
117
+ runtime: Type.Optional(Type.String({
118
+ description: "Restrict to one runtime: bash, node, or python.",
119
119
  })),
120
120
  failing_only: Type.Optional(Type.Boolean({ description: "Only non-zero exit codes and timeouts." })),
121
121
  limit: Type.Optional(Type.Number({ description: "Max results (default 20, cap 200)", minimum: 1, maximum: 200 })),
@@ -1,22 +1,107 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Shared closeout detection and merge actions for /gsd home and smart entry.
3
+ import { existsSync, readdirSync, statSync } from "node:fs";
4
+ import { join } from "node:path";
3
5
  import { setAutoOutcomeWidget } from "./auto-dashboard.js";
4
6
  import { invalidateAllCaches } from "./cache.js";
7
+ import { isDbAvailable } from "./db/engine.js";
8
+ import { getMilestone } from "./db/queries.js";
9
+ import { MILESTONE_ID_RE } from "./milestone-ids.js";
5
10
  import { mergeCompletedMilestone } from "./parallel-merge.js";
6
11
  import { cleanupQuickBranch, detectStrandedQuickBranch } from "./quick.js";
12
+ import { isClosedStatus } from "./status-guards.js";
7
13
  import { findUnmergedCompletedMilestones, } from "./unmerged-milestone-guard.js";
8
14
  import { appendRequirementsBacklogToSummary } from "./requirements-backlog.js";
15
+ import { nativeBranchList, nativeIsRepo } from "./native-git-bridge.js";
16
+ import { allWorktreesDirs } from "./worktree-manager.js";
9
17
  const MILESTONE_MERGE_CLOSEOUT_COMMANDS = [
10
18
  "/gsd status for overview",
11
19
  "/gsd visualize to inspect",
12
20
  "/gsd notifications for history",
13
21
  "/gsd start for new work",
14
22
  ];
23
+ function listMilestoneWorktreeIds(basePath) {
24
+ const ids = new Set();
25
+ for (const wtDir of allWorktreesDirs(basePath)) {
26
+ if (!existsSync(wtDir))
27
+ continue;
28
+ for (const entry of readdirSync(wtDir)) {
29
+ if (!MILESTONE_ID_RE.test(entry))
30
+ continue;
31
+ try {
32
+ if (statSync(join(wtDir, entry)).isDirectory())
33
+ ids.add(entry);
34
+ }
35
+ catch {
36
+ // skip unreadable entries
37
+ }
38
+ }
39
+ }
40
+ return [...ids].sort();
41
+ }
42
+ function listMilestoneBranchIds(basePath) {
43
+ try {
44
+ return nativeBranchList(basePath, "milestone/*")
45
+ .map((branch) => branch.replace(/^milestone\//, ""))
46
+ .filter((id) => MILESTONE_ID_RE.test(id))
47
+ .sort();
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ /**
54
+ * A milestone ID is "stranded residue" only when its worktree/branch artifacts
55
+ * exist for a milestone the DB does not consider currently in flight — i.e. the
56
+ * row is closed (complete/done/skipped/closed) or absent. Active, pending,
57
+ * blocked, parked, queued, and deferred rows describe normal in-flight or
58
+ * intentionally-preserved state, never residue. Returning `false` skips the ID;
59
+ * returning `true` keeps it in the hint.
60
+ */
61
+ function isStrandedMilestoneId(milestoneId) {
62
+ if (!isDbAvailable())
63
+ return true;
64
+ const row = getMilestone(milestoneId);
65
+ if (!row)
66
+ return true;
67
+ return isClosedStatus(row.status);
68
+ }
69
+ /** Surface stranded milestone git residue when closeout guards did not classify it. */
70
+ export function detectIdleMilestoneResidueHint(basePath) {
71
+ if (!nativeIsRepo(basePath))
72
+ return null;
73
+ const gsdDir = join(basePath, ".gsd");
74
+ const dbPath = join(gsdDir, "gsd.db");
75
+ if (!existsSync(gsdDir) || !existsSync(dbPath)) {
76
+ return {
77
+ milestoneIds: [],
78
+ message: "This git repo has no local GSD workflow database (.gsd/gsd.db). " +
79
+ "Workflow state may live in an external worktree, or run /gsd new-project to initialize here.",
80
+ };
81
+ }
82
+ const worktreeIds = listMilestoneWorktreeIds(basePath);
83
+ const branchIds = listMilestoneBranchIds(basePath);
84
+ const candidateIds = [...new Set([...worktreeIds, ...branchIds])].sort();
85
+ const milestoneIds = candidateIds.filter(isStrandedMilestoneId);
86
+ if (milestoneIds.length === 0)
87
+ return null;
88
+ const listed = milestoneIds.join(", ");
89
+ const recovery = milestoneIds.length === 1
90
+ ? `/gsd dispatch complete-milestone ${milestoneIds[0]}`
91
+ : "/gsd doctor --fix";
92
+ return {
93
+ milestoneIds,
94
+ message: `Stranded milestone git residue detected (${listed}: worktree dir and/or milestone/* branch). ` +
95
+ `Run ${recovery} or /gsd status to recover closeout before starting new work.`,
96
+ };
97
+ }
15
98
  export async function loadCloseoutContext(basePath) {
16
99
  const unmergedMilestones = await findUnmergedCompletedMilestones(basePath);
100
+ const idleResidueHint = unmergedMilestones.length === 0 ? detectIdleMilestoneResidueHint(basePath) : null;
17
101
  return {
18
102
  strandedQuick: detectStrandedQuickBranch(basePath),
19
103
  unmergedMilestones,
104
+ idleResidueHint,
20
105
  };
21
106
  }
22
107
  export function getPrimaryCloseoutRecommendation(closeout) {
@@ -62,6 +147,13 @@ export function buildIdleMenuSummary(state, closeout) {
62
147
  `${blocker.milestoneId} is complete but not merged into ${blocker.integrationBranch}.`,
63
148
  ];
64
149
  }
150
+ // Surface idle residue before the completion summary so smart entry shows
151
+ // the same recovery text /gsd home would: a closed/unknown milestone with
152
+ // lingering worktree/branch artifacts must not be hidden behind the
153
+ // "all milestones complete" message.
154
+ if (closeout.idleResidueHint) {
155
+ return [closeout.idleResidueHint.message];
156
+ }
65
157
  if (state.phase === "complete") {
66
158
  const last = state.lastCompletedMilestone;
67
159
  return appendRequirementsBacklogToSummary(state, [
@@ -1,4 +1,4 @@
1
- import { checkRemoteAutoSession, isAutoActive, isAutoPaused, stopAutoRemote } from "../auto.js";
1
+ import { checkRemoteAutoSession, forceStopAutoRemote, isAutoActive, isAutoPaused, stopAutoRemote } from "../auto.js";
2
2
  import { validateDirectory } from "../validate-directory.js";
3
3
  import { resolveProjectRoot } from "../worktree.js";
4
4
  import { showNextAction } from "../../shared/tui.js";
@@ -135,5 +135,19 @@ export async function guardRemoteSession(ctx, pi) {
135
135
  }
136
136
  return false;
137
137
  }
138
- return choice === "force";
138
+ if (choice === "force") {
139
+ const result = forceStopAutoRemote(projectRoot());
140
+ if (result.error) {
141
+ ctx.ui.notify(`Failed to force-stop remote auto-mode: ${result.error}`, "error");
142
+ return false;
143
+ }
144
+ if (result.found) {
145
+ ctx.ui.notify(`Force-stopped auto-mode session (PID ${result.pid}). Starting a new session.`, "warning");
146
+ }
147
+ else {
148
+ ctx.ui.notify("Remote session is no longer running. Starting a new session.", "info");
149
+ }
150
+ return true;
151
+ }
152
+ return false;
139
153
  }
@@ -7,7 +7,7 @@
7
7
  import { existsSync, readFileSync, mkdirSync } from "node:fs";
8
8
  import { execFileSync } from "node:child_process";
9
9
  import { createRequire } from "node:module";
10
- import { join, resolve as resolvePath, sep } from "node:path";
10
+ import { join, resolve as resolvePath, sep, win32 as pathWin32 } from "node:path";
11
11
  import { homedir } from "node:os";
12
12
  import { deriveState } from "./state.js";
13
13
  import { gsdRoot } from "./paths.js";
@@ -20,6 +20,7 @@ import { getAutoWorktreePath } from "./auto-worktree.js";
20
20
  import { currentDirectoryRoot, projectRoot } from "./commands/context.js";
21
21
  import { loadPrompt } from "./prompt-loader.js";
22
22
  import { buildClaudeRuntimeFloorAdvisory } from "../../shared/claude-runtime-floor.js";
23
+ import { reconcileGsdBrowserPathAfterInstall } from "../../shared/gsd-browser-path-sync.js";
23
24
  import { isPnpmInstall } from "../../shared/package-manager-detection.js";
24
25
  import { buildDoctorHealIssuePayload, buildDoctorHealSummary, buildWorkflowDispatchContent, } from "./workflow-protocol.js";
25
26
  import { restoreGsdWorkflowTools, scopeGsdWorkflowToolsForDispatch, } from "./bootstrap/register-hooks.js";
@@ -51,8 +52,31 @@ function resolveInstallCommand(pkg) {
51
52
  return `bun add -g ${pkg}`;
52
53
  if (isPnpmInstall())
53
54
  return `pnpm add -g ${pkg}`;
55
+ const npmPrefix = resolveWindowsNpmGlobalPrefix();
56
+ if (npmPrefix)
57
+ return `npm --prefix ${quoteWindowsArg(npmPrefix)} install -g ${pkg}`;
54
58
  return `npm install -g ${pkg}`;
55
59
  }
60
+ function resolveWindowsNpmGlobalPrefix(argv1 = process.argv[1], platform = process.platform) {
61
+ if (platform !== "win32" || !argv1)
62
+ return null;
63
+ const normalized = pathWin32.normalize(argv1);
64
+ const marker = `${pathWin32.sep}node_modules${pathWin32.sep}`;
65
+ const index = normalized.toLowerCase().lastIndexOf(marker);
66
+ if (index <= 0)
67
+ return null;
68
+ const prefix = normalized.slice(0, index);
69
+ // Verify this is a real npm global prefix: such a directory always contains
70
+ // npm's own bin shim (`npm.cmd`) as a sibling of `node_modules/`. Local
71
+ // project `node_modules/`, npx caches, and other non-global layouts do not,
72
+ // so without this check `--prefix` would target the wrong directory.
73
+ if (!existsSync(pathWin32.join(prefix, "npm.cmd")))
74
+ return null;
75
+ return prefix;
76
+ }
77
+ function quoteWindowsArg(value) {
78
+ return `"${value.replace(/"/g, '\\"')}"`;
79
+ }
56
80
  function notifyClaudeRuntimeFloorAdvisory(ctx) {
57
81
  let advisory = null;
58
82
  try {
@@ -484,11 +508,30 @@ export async function handleUpdate(ctx, args = "") {
484
508
  execSync(installCmd, {
485
509
  stdio: ["ignore", "pipe", "ignore"],
486
510
  });
511
+ let reconcile = null;
512
+ if (browserUpdate) {
513
+ try {
514
+ reconcile = reconcileGsdBrowserPathAfterInstall({
515
+ latestVersion: latest,
516
+ compareSemver: compareSemverLocal,
517
+ resolvePathVersion: resolveGsdBrowserPathVersionForCommand,
518
+ });
519
+ }
520
+ catch {
521
+ // Reconciliation is best-effort: the install above already succeeded,
522
+ // so a reconcile failure must not flip the result to "Update failed".
523
+ reconcile = null;
524
+ }
525
+ }
487
526
  const newPathVersion = browserUpdate ? resolveGsdBrowserPathVersionForCommand() : null;
488
- const pathReady = !browserUpdate || (!!newPathVersion && compareSemverLocal(newPathVersion, latest) >= 0);
527
+ const pathNote = browserUpdate && !(newPathVersion && compareSemverLocal(newPathVersion, latest) >= 0)
528
+ ? (reconcile?.message
529
+ ?? "Ensure the npm global bin directory is on your PATH so MCP automation uses the updated binary.")
530
+ : "";
489
531
  ctx.ui.notify(browserUpdate
490
532
  ? `Updated gsd-browser to v${latest}. Restart your GSD session to use the new browser automation version.` +
491
- (pathReady ? "" : "\nNote: Ensure the npm global bin directory is on your PATH so MCP automation uses the updated binary.")
533
+ (reconcile?.action === "synced" && reconcile.message ? `\n${reconcile.message}` : "") +
534
+ (pathNote ? `\nNote: ${pathNote}` : "")
492
535
  : `Updated to v${latest}. Restart your GSD session to use the new version.`, "info");
493
536
  if (!browserUpdate)
494
537
  notifyClaudeRuntimeFloorAdvisory(ctx);
@@ -62,6 +62,20 @@ export function hasApprovalQuestion(text) {
62
62
  export function hasResearchDecisionQuestion(text) {
63
63
  return hasQuestionMatching(text, [RESEARCH_DECISION_QUESTION_RE]);
64
64
  }
65
+ /**
66
+ * Detect a plain-text "Next steps:" menu — numbered options with an "Other"
67
+ * choice — emitted as prose instead of a structured ask_user_questions call.
68
+ * Without this, auto-mode treats the menu as informational and loops on its
69
+ * own turn until tokens are exhausted (#454).
70
+ */
71
+ export function hasPlainTextNextStepsMenu(lines) {
72
+ const nextStepsIndex = lines.findIndex((line) => /^next steps\s*:?$/i.test(line));
73
+ if (nextStepsIndex < 0)
74
+ return false;
75
+ const menuLines = lines.slice(nextStepsIndex + 1);
76
+ const numberedOptions = menuLines.filter((line) => /^\d+[.)]\s+\S/.test(line));
77
+ return numberedOptions.length >= 2 && numberedOptions.some((line) => /\bother\b/i.test(line));
78
+ }
65
79
  // ── Message text extraction (moved from user-input-boundary) ────────────────
66
80
  function extractMessageText(msg, includeThinking) {
67
81
  if (!msg || typeof msg !== "object")
@@ -273,6 +287,8 @@ export function isAwaitingUserInput(messages) {
273
287
  const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
274
288
  if (lines.some((line) => line.endsWith("?")))
275
289
  return true;
290
+ if (hasPlainTextNextStepsMenu(lines))
291
+ return true;
276
292
  return hasApprovalQuestion(text);
277
293
  }
278
294
  export function isAwaitingApprovalBoundary(messages) {
@@ -186,6 +186,7 @@ export function writeLock(basePath, unitType, unitId, sessionFile) {
186
186
  * stale session-file pointer.
187
187
  */
188
188
  export function clearLock(basePath) {
189
+ const legacyLock = readLegacyLock(basePath);
189
190
  clearLegacyLockFile(basePath);
190
191
  if (!isDbAvailable())
191
192
  return;
@@ -198,9 +199,13 @@ export function clearLock(basePath) {
198
199
  deleteRuntimeKv("worker", staleWorker.worker_id, SESSION_FILE_KV_KEY);
199
200
  return;
200
201
  }
201
- const lock = readLegacyLock(basePath);
202
- if (lock?.pid)
203
- markWorkerStoppingByPid(projectRoot, lock.pid);
202
+ if (legacyLock?.pid) {
203
+ markWorkerStoppingByPid(projectRoot, legacyLock.pid);
204
+ const workerByLegacyPid = getAllAutoWorkers().find((w) => w.pid === legacyLock.pid
205
+ && normalizeRealPath(w.project_root_realpath) === projectRoot);
206
+ if (workerByLegacyPid)
207
+ forceReleaseLeasesForWorker(workerByLegacyPid.worker_id);
208
+ }
204
209
  const worker = findActiveWorkerForCurrentProcess(projectRoot);
205
210
  if (worker)
206
211
  deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
@@ -5,7 +5,7 @@ import { isAfter, latestExplicitReopenAt } from "./milestone-reopen-events.js";
5
5
  import { resolveGsdPathContract, resolveMilestoneFile } from "./paths.js";
6
6
  import { deriveState } from "./state.js";
7
7
  import { readEvents } from "./workflow-events.js";
8
- import { renderAllProjections } from "./workflow-projections.js";
8
+ import { flushWorkflowProjections } from "./projection-flush.js";
9
9
  export async function checkEngineHealth(basePath, issues, fixesApplied) {
10
10
  const dbPath = resolveGsdPathContract(basePath).projectDb;
11
11
  if (!isDbAvailable() && existsSync(dbPath)) {
@@ -239,7 +239,7 @@ export async function checkEngineHealth(basePath, issues, fixesApplied) {
239
239
  const roadmapPath = resolveMilestoneFile(basePath, milestone.id, "ROADMAP");
240
240
  if (!roadmapPath || !existsSync(roadmapPath)) {
241
241
  try {
242
- await renderAllProjections(basePath, milestone.id);
242
+ await flushWorkflowProjections(basePath, { milestoneId: milestone.id });
243
243
  fixesApplied.push(`re-rendered missing projections for ${milestone.id}`);
244
244
  }
245
245
  catch {
@@ -250,7 +250,7 @@ export async function checkEngineHealth(basePath, issues, fixesApplied) {
250
250
  const projectionMtime = statSync(roadmapPath).mtimeMs;
251
251
  if (lastEventTs > projectionMtime) {
252
252
  try {
253
- await renderAllProjections(basePath, milestone.id);
253
+ await flushWorkflowProjections(basePath, { milestoneId: milestone.id });
254
254
  fixesApplied.push(`re-rendered stale projections for ${milestone.id}`);
255
255
  }
256
256
  catch {
@@ -3,10 +3,9 @@ import { spawnSync } from "node:child_process";
3
3
  import { cpSync, existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { loadFile } from "./files.js";
6
- import { parseRoadmap as parseLegacyRoadmap } from "./parsers-legacy.js";
7
- import { isDbAvailable, getMilestone } from "./gsd-db.js";
8
6
  import { resolveMilestoneFile } from "./paths.js";
9
- import { deriveState, isMilestoneComplete } from "./state.js";
7
+ import { isCompletedMilestoneTerminal } from "./milestone-closeout.js";
8
+ import { deriveState } from "./state.js";
10
9
  import { allWorktreesDirs, createWorktree, listWorktrees, resolveGitDir } from "./worktree-manager.js";
11
10
  import { abortAndReset } from "./git-self-heal.js";
12
11
  import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
@@ -141,21 +140,6 @@ function getSnapshotDiffCheckFailure(basePath) {
141
140
  }
142
141
  return failures.length > 0 ? failures.join("\n") : null;
143
142
  }
144
- async function isCompletedMilestoneTerminal(basePath, milestoneId) {
145
- const summaryPath = resolveMilestoneFile(basePath, milestoneId, "SUMMARY");
146
- if (!summaryPath)
147
- return false;
148
- if (isDbAvailable()) {
149
- const milestone = getMilestone(milestoneId);
150
- return !!milestone && milestone.status === "complete";
151
- }
152
- const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
153
- const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
154
- if (!roadmapContent)
155
- return false;
156
- const roadmap = parseLegacyRoadmap(roadmapContent);
157
- return isMilestoneComplete(roadmap);
158
- }
159
143
  export async function checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode = "none") {
160
144
  // Degrade gracefully if not a git repo
161
145
  if (!nativeIsRepo(basePath)) {
@@ -28,13 +28,17 @@ export function buildGsdHomeModel(state, closeout) {
28
28
  const workLabel = activeWorkLabel(state);
29
29
  const strandedQuick = closeout?.strandedQuick ?? null;
30
30
  const unmergedMilestone = closeout?.unmergedMilestones?.[0];
31
+ const idleResidueHint = closeout?.idleResidueHint ?? null;
32
+ const hasIdleResidue = Boolean(idleResidueHint);
31
33
  const nextReason = complete
32
34
  ? "all milestones are complete"
33
35
  : blocked
34
36
  ? "the active milestone is blocked"
35
- : !hasActiveWork
36
- ? "there is no active milestone"
37
- : "";
37
+ : hasIdleResidue
38
+ ? "milestone git residue needs recovery"
39
+ : !hasActiveWork
40
+ ? "there is no active milestone"
41
+ : "";
38
42
  const canAdvance = hasActiveWork && !blocked && !complete;
39
43
  const unmappedActive = complete ? countUnmappedActiveRequirements() : 0;
40
44
  const recommended = strandedQuick
@@ -43,11 +47,13 @@ export function buildGsdHomeModel(state, closeout) {
43
47
  ? "finish_milestone"
44
48
  : blocked
45
49
  ? "fix_recover"
46
- : canAdvance
47
- ? "continue_step"
48
- : complete && unmappedActive > 0
49
- ? "review_requirements_backlog"
50
- : "start_configure";
50
+ : hasIdleResidue
51
+ ? "fix_recover"
52
+ : canAdvance
53
+ ? "continue_step"
54
+ : complete && unmappedActive > 0
55
+ ? "review_requirements_backlog"
56
+ : "start_configure";
51
57
  const completionSummary = complete
52
58
  ? appendRequirementsBacklogToSummary(state, [
53
59
  `All milestones complete${state.lastCompletedMilestone ? ` after ${state.lastCompletedMilestone.id}: ${state.lastCompletedMilestone.title}` : ""}.`,
@@ -57,7 +63,9 @@ export function buildGsdHomeModel(state, closeout) {
57
63
  ? [`Quick task Q${strandedQuick.taskNum} finished on ${strandedQuick.quickBranch} but is not merged to ${strandedQuick.originalBranch}.`]
58
64
  : unmergedMilestone
59
65
  ? [`${unmergedMilestone.milestoneId} is complete but not merged into ${unmergedMilestone.integrationBranch}.`]
60
- : completionSummary;
66
+ : idleResidueHint
67
+ ? [idleResidueHint.message]
68
+ : completionSummary;
61
69
  return {
62
70
  title: "GSD — What now?",
63
71
  summary: [
@@ -130,10 +138,12 @@ export function buildGsdHomeModel(state, closeout) {
130
138
  label: "Fix or recover",
131
139
  description: blocked
132
140
  ? "Review the blocker and recovery commands for the active milestone."
133
- : disabled("This becomes active when closeout, validation, or state recovery is needed.", "no blocker is active"),
134
- enabled: blocked,
141
+ : hasIdleResidue
142
+ ? "Review stranded milestone worktrees/branches and run the suggested recovery command."
143
+ : disabled("This becomes active when closeout, validation, or state recovery is needed.", "no blocker is active"),
144
+ enabled: blocked || hasIdleResidue,
135
145
  recommended: recommended === "fix_recover",
136
- disabledReason: blocked ? undefined : "no blocker is active",
146
+ disabledReason: blocked || hasIdleResidue ? undefined : "no blocker is active",
137
147
  },
138
148
  {
139
149
  id: "start_configure",
@@ -169,7 +169,7 @@ export function insertArtifact(a) {
169
169
  export function insertMilestone(m) {
170
170
  if (!getDbOrNull())
171
171
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
172
- getDbOrNull().prepare(`INSERT OR IGNORE INTO milestones (
172
+ const result = getDbOrNull().prepare(`INSERT OR IGNORE INTO milestones (
173
173
  id, title, status, depends_on, created_at,
174
174
  vision, success_criteria, key_risks, proof_strategy,
175
175
  verification_contract, verification_integration, verification_operational, verification_uat,
@@ -199,6 +199,7 @@ export function insertMilestone(m) {
199
199
  ":requirement_coverage": m.planning?.requirementCoverage ?? "",
200
200
  ":boundary_map_markdown": m.planning?.boundaryMapMarkdown ?? "",
201
201
  });
202
+ return (result.changes ?? 0) > 0;
202
203
  }
203
204
  export function upsertMilestonePlanning(milestoneId, planning) {
204
205
  if (!getDbOrNull())
@@ -44,7 +44,8 @@ import { buildCloseoutMenuActions, buildIdleMenuSummary, getPrimaryCloseoutRecom
44
44
  import { buildRequirementsBacklogDiscussContext, countUnmappedActiveRequirements, showRequirementsBacklogReview, } from "./requirements-backlog.js";
45
45
  import { selectAndApplyModel } from "./auto-model-selection.js";
46
46
  import { DISCUSS_TOOLS_ALLOWLIST } from "./constants.js";
47
- import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForGuidedUnit, supportsStructuredQuestions, } from "./workflow-mcp.js";
47
+ import { supportsStructuredQuestions } from "./workflow-mcp.js";
48
+ import { getUnitWorkflowDispatchReadinessError } from "./tool-contract.js";
48
49
  import { runPreparation, formatCodebaseBrief, formatPriorContextBrief, } from "./preparation.js";
49
50
  import { verifyExpectedArtifact } from "./auto-recovery.js";
50
51
  import { clearPendingGate, extractDepthVerificationMilestoneId, getPendingGate } from "./bootstrap/write-gate.js";
@@ -382,7 +383,8 @@ async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType,
382
383
  const projectRoot = resolveGuidedDispatchProjectRoot(resolvedOptions.basePath);
383
384
  const loadPreferences = resolvedOptions.deps?.loadPreferences ?? loadEffectiveGSDPreferences;
384
385
  const selectModel = resolvedOptions.deps?.selectModel ?? selectAndApplyModel;
385
- const getTransportSupportError = resolvedOptions.deps?.getTransportSupportError ?? getWorkflowTransportSupportError;
386
+ const getDispatchReadinessError = resolvedOptions.deps?.getDispatchReadinessError
387
+ ?? getUnitWorkflowDispatchReadinessError;
386
388
  // Route through the dynamic routing pipeline (complexity classification,
387
389
  // tier downgrade, fallback chains) — same path as auto-mode dispatches (#2958).
388
390
  if (ctx && unitType) {
@@ -396,7 +398,8 @@ async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType,
396
398
  routing: result.routing,
397
399
  });
398
400
  }
399
- const compatibilityError = getTransportSupportError(result.appliedModel?.provider ?? ctx.model?.provider, getRequiredWorkflowToolsForGuidedUnit(unitType), {
401
+ const compatibilityError = getDispatchReadinessError({
402
+ provider: result.appliedModel?.provider ?? ctx.model?.provider,
400
403
  projectRoot,
401
404
  surface: "guided flow",
402
405
  unitType,