@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
@@ -29,6 +29,7 @@ function isPlainObject(value) {
29
29
  // built via a template string so TypeScript's NodeNext resolver treats them as
30
30
  // `any` and skips static checking.
31
31
  const MCP_PKG = '@modelcontextprotocol/sdk';
32
+ import { sanitizeSchemaForMoonshot } from '@gsd/pi-ai';
32
33
  export function mcpSdkSpecifier(subpath) {
33
34
  return `${MCP_PKG}/${subpath}.js`;
34
35
  }
@@ -65,7 +66,7 @@ export async function startMcpServer(options) {
65
66
  tools: tools.map((t) => ({
66
67
  name: t.name,
67
68
  description: t.description,
68
- inputSchema: t.parameters,
69
+ inputSchema: sanitizeSchemaForMoonshot(t.parameters),
69
70
  })),
70
71
  }));
71
72
  // tools/call — execute the requested tool and return content blocks.
@@ -1 +1 @@
1
- c63bf325c75b710f
1
+ 298c30be22e6452d
@@ -12,7 +12,7 @@
12
12
  import { debugCount, debugLog, debugTime } from "../debug-logger.js";
13
13
  import { reconcileBeforeDispatch } from "../state-reconciliation.js";
14
14
  import { isLegalEdge, IllegalPhaseTransitionError } from "../state-transition-matrix.js";
15
- import { resolveDispatch } from "../auto-dispatch.js";
15
+ import { hasPendingDeepStage, resolveDispatch } from "../auto-dispatch.js";
16
16
  import { classifyFailure } from "../recovery-classification.js";
17
17
  import { verifyExpectedArtifact, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
18
18
  import { invalidateAllCaches } from "../cache.js";
@@ -111,14 +111,25 @@ function shouldAdoptActiveMilestone(state, activeSession, activeDispatchBasePath
111
111
  export async function decideOrchestratorDispatch(ctx, pi, dispatchBasePath, session, input) {
112
112
  const state = input.stateSnapshot;
113
113
  const active = state.activeMilestone;
114
- if (!active)
115
- return null;
116
114
  const activeSession = input.session ?? session;
117
115
  const activeDispatchBasePath = activeSession?.basePath || dispatchBasePath;
118
- if (activeSession && shouldAdoptActiveMilestone(state, activeSession, activeDispatchBasePath)) {
116
+ const prefs = loadEffectiveGSDPreferences(activeDispatchBasePath)?.preferences;
117
+ if (!active) {
118
+ if (state.phase !== "pre-planning")
119
+ return null;
120
+ if (!hasPendingDeepStage(prefs, activeDispatchBasePath)) {
121
+ return {
122
+ kind: "blocked",
123
+ reason: state.nextAction || "No active milestone. Run /gsd unpark <id> or create a new milestone.",
124
+ action: "stop",
125
+ };
126
+ }
127
+ }
128
+ if (active && activeSession && shouldAdoptActiveMilestone(state, activeSession, activeDispatchBasePath)) {
119
129
  activeSession.currentMilestoneId = active.id;
120
130
  }
121
- const prefs = loadEffectiveGSDPreferences(activeDispatchBasePath)?.preferences;
131
+ const dispatchMid = active?.id ?? activeSession?.currentMilestoneId ?? "";
132
+ const dispatchMidTitle = active?.title ?? "";
122
133
  // Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
123
134
  // (#5789). Prefer caller-supplied values when present so test harnesses and
124
135
  // alternative wirings can inject deterministic snapshots; otherwise pull from
@@ -146,8 +157,15 @@ export async function decideOrchestratorDispatch(ctx, pi, dispatchBasePath, sess
146
157
  })
147
158
  ? "true"
148
159
  : "false");
160
+ // Only replay a milestone-scoped verification retry when a milestone is
161
+ // active. Pre-PR (#712 fix), `!active` returned null before reaching this
162
+ // block, so the retry was preserved for a future tick. The new
163
+ // pre-planning + deep-pending fall-through must keep that contract:
164
+ // otherwise a stale execute-task / complete-slice / complete-milestone
165
+ // retry whose target milestone has since been parked would preempt
166
+ // project-level deep rules like `discuss-project`.
149
167
  const pendingRetry = session?.pendingVerificationRetryDispatch;
150
- if (session && pendingRetry) {
168
+ if (session && pendingRetry && active) {
151
169
  session.pendingVerificationRetryDispatch = null;
152
170
  const alreadyClosedReason = getAlreadyClosedDispatchReason(pendingRetry.unitType, pendingRetry.unitId);
153
171
  if (alreadyClosedReason) {
@@ -165,8 +183,8 @@ export async function decideOrchestratorDispatch(ctx, pi, dispatchBasePath, sess
165
183
  }
166
184
  const action = await resolveDispatch({
167
185
  basePath: activeDispatchBasePath,
168
- mid: active.id,
169
- midTitle: active.title,
186
+ mid: dispatchMid,
187
+ midTitle: dispatchMidTitle,
170
188
  state,
171
189
  prefs,
172
190
  session: activeSession,
@@ -211,8 +229,8 @@ export async function decideOrchestratorDispatch(ctx, pi, dispatchBasePath, sess
211
229
  prompt: action.prompt,
212
230
  pauseAfterUatDispatch: action.pauseAfterDispatch ?? false,
213
231
  state,
214
- mid: active.id,
215
- midTitle: active.title,
232
+ mid: dispatchMid,
233
+ midTitle: dispatchMidTitle,
216
234
  };
217
235
  session.pendingOrchestrationDispatch = pending;
218
236
  }
@@ -11,7 +11,7 @@
11
11
  import { importExtensionModule } from "@gsd/pi-coding-agent";
12
12
  import { USER_DRIVEN_DEEP_UNITS, isAwaitingUserInput, } from "../auto-post-unit.js";
13
13
  import { lastAssistantText } from "../consent-question.js";
14
- import { resolveEffectiveUnitIsolationMode } from "../preferences.js";
14
+ import { resolveEffectiveUnitIsolationMode, getIsolationMode } from "../preferences.js";
15
15
  import { MAX_RECOVERY_CHARS, BUDGET_THRESHOLDS, MAX_FINALIZE_TIMEOUTS, } from "./types.js";
16
16
  import { detectStuck } from "./detect-stuck.js";
17
17
  import { STUCK_WINDOW_SIZE } from "./dispatch-history.js";
@@ -35,9 +35,10 @@ import { writeUnitRuntimeRecord } from "../unit-runtime.js";
35
35
  import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
36
36
  import { getEligibleSlices } from "../slice-parallel-eligibility.js";
37
37
  import { isSliceParallelActive, startSliceParallel } from "../slice-parallel-orchestrator.js";
38
- import { isDbAvailable, getMilestoneSlices, getSlice, getTask } from "../gsd-db.js";
38
+ import { isDbAvailable, getMilestone, getMilestoneSlices, getSlice, getTask } from "../gsd-db.js";
39
39
  import { refreshWorkflowDatabaseFromDisk } from "../db-workspace.js";
40
40
  import { isClosedStatus } from "../status-guards.js";
41
+ import { findUnmergedCompletedMilestones } from "../unmerged-milestone-guard.js";
41
42
  import { setRuntimeKv } from "../db/runtime-kv.js";
42
43
  import { getLatestForUnit } from "../db/unit-dispatches.js";
43
44
  import { reconcileBeforeSpawn } from "../state-reconciliation.js";
@@ -50,7 +51,8 @@ import { parseUnitId } from "../unit-id.js";
50
51
  import { createCheckpoint, cleanupCheckpoint, rollbackToCheckpoint } from "../safety/git-checkpoint.js";
51
52
  import { resolveSafetyHarnessConfig } from "../safety/safety-harness.js";
52
53
  import { getContextPauseAction } from "../auto-budget.js";
53
- import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit, supportsStructuredQuestions, } from "../workflow-mcp.js";
54
+ import { supportsStructuredQuestions } from "../workflow-mcp.js";
55
+ import { getUnitWorkflowDispatchReadinessError } from "../tool-contract.js";
54
56
  import { prepareWorkflowMcpForProject } from "../workflow-mcp-auto-prep.js";
55
57
  import { applyThinkingLevelForModel, floorThinkingLevelForUnit, getRegisteredToolSnapshot, getToolBaselineSnapshot, } from "../auto-model-selection.js";
56
58
  import { resolveManifest } from "../unit-context-manifest.js";
@@ -600,6 +602,36 @@ async function failClosedOnFinalizeTimeout(ic, iterData, loopState, stage, start
600
602
  drainLogs();
601
603
  return { action: "break", reason: progressKind };
602
604
  }
605
+ export async function shouldSkipTerminalMilestoneCloseout(s, state, mid) {
606
+ const closeoutMilestoneId = mid ?? s.currentMilestoneId ?? state.lastCompletedMilestone?.id;
607
+ if (s.completionStopInProgress) {
608
+ return { skip: true, milestoneId: closeoutMilestoneId };
609
+ }
610
+ if (!closeoutMilestoneId) {
611
+ return { skip: false };
612
+ }
613
+ if (isDbAvailable())
614
+ refreshWorkflowDatabaseFromDisk();
615
+ const closeoutBasePath = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
616
+ let closeoutMergePending = false;
617
+ if (getIsolationMode(closeoutBasePath) !== "none") {
618
+ try {
619
+ const blockers = await findUnmergedCompletedMilestones(closeoutBasePath);
620
+ closeoutMergePending = blockers.some((blocker) => blocker.milestoneId === closeoutMilestoneId);
621
+ }
622
+ catch {
623
+ // Fail open: without git/DB inspection we cannot safely treat closeout as done.
624
+ closeoutMergePending = true;
625
+ }
626
+ }
627
+ const milestoneAlreadyClosedOut = isDbAvailable()
628
+ && isClosedStatus(getMilestone(closeoutMilestoneId)?.status ?? "")
629
+ && !closeoutMergePending;
630
+ if (milestoneAlreadyClosedOut) {
631
+ return { skip: true, milestoneId: closeoutMilestoneId };
632
+ }
633
+ return { skip: false, milestoneId: closeoutMilestoneId };
634
+ }
603
635
  // ─── runPreDispatch ───────────────────────────────────────────────────────────
604
636
  /**
605
637
  * Phase 1: Pre-dispatch — resource guard, health gate, state derivation,
@@ -962,6 +994,13 @@ export async function runPreDispatch(ic, loopState) {
962
994
  deps.setActiveMilestoneId(s.basePath, mid);
963
995
  }
964
996
  // ── Terminal conditions ──────────────────────────────────────────────
997
+ if (state.phase === "complete") {
998
+ const closeoutSkip = await shouldSkipTerminalMilestoneCloseout(s, state, mid);
999
+ if (closeoutSkip.skip) {
1000
+ debugLog("autoLoop", { phase: "complete", reason: "milestone-already-closed", milestoneId: closeoutSkip.milestoneId });
1001
+ return { action: "break", reason: "milestone-complete" };
1002
+ }
1003
+ }
965
1004
  if (!mid) {
966
1005
  if (s.currentUnit) {
967
1006
  await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
@@ -1769,7 +1808,8 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1769
1808
  s.currentDispatchedModelId = s.currentUnitModel
1770
1809
  ? `${s.currentUnitModel.provider ?? ""}/${s.currentUnitModel.id ?? ""}`
1771
1810
  : null;
1772
- const compatibilityError = getWorkflowTransportSupportError(s.currentUnitModel?.provider ?? ctx.model?.provider, getRequiredWorkflowToolsForAutoUnit(unitType), {
1811
+ const compatibilityError = getUnitWorkflowDispatchReadinessError({
1812
+ provider: s.currentUnitModel?.provider ?? ctx.model?.provider,
1773
1813
  projectRoot: s.basePath,
1774
1814
  surface: "auto-mode",
1775
1815
  unitType,
@@ -1848,6 +1888,9 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1848
1888
  causedBy: "unit-start",
1849
1889
  });
1850
1890
  s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
1891
+ if (nextDispatchCount <= 1) {
1892
+ s.toolUnavailableRetries = 0;
1893
+ }
1851
1894
  const unitStartSeq = ic.nextSeq();
1852
1895
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
1853
1896
  deps.captureAvailableSkills();
@@ -126,6 +126,8 @@ export class AutoSession {
126
126
  /** Set when a GSD tool execution ends with isError due to malformed/truncated
127
127
  * JSON arguments. Checked by postUnitPreVerification to break retry loops. */
128
128
  lastToolInvocationError = null;
129
+ /** Consecutive tool-unavailable retries for the current unit (MCP startup race). */
130
+ toolUnavailableRetries = 0;
129
131
  /** Agent-end messages from the just-finished unit, consumed during finalize. */
130
132
  lastUnitAgentEndMessages = null;
131
133
  /** Set when turn-level git action fails during closeout. */
@@ -306,6 +308,7 @@ export class AutoSession {
306
308
  this.lastPreExecFailure = null;
307
309
  this.preExecRetryCount.clear();
308
310
  this.lastToolInvocationError = null;
311
+ this.toolUnavailableRetries = 0;
309
312
  this.lastUnitAgentEndMessages = null;
310
313
  this.lastGitActionFailure = null;
311
314
  this.lastGitActionStatus = null;
@@ -10,7 +10,7 @@ import { buildResearchSlicePrompt, buildResearchMilestonePrompt, buildPlanSliceP
10
10
  import { loadEffectiveGSDPreferences } from "./preferences.js";
11
11
  import { pauseAuto } from "./auto.js";
12
12
  import { resolveCanonicalMilestoneRoot } from "./worktree-manager.js";
13
- import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit, } from "./workflow-mcp.js";
13
+ import { getUnitWorkflowDispatchReadinessError } from "./tool-contract.js";
14
14
  export async function dispatchDirectPhase(ctx, pi, phase, base) {
15
15
  const state = await deriveState(base);
16
16
  const mid = state.activeMilestone?.id;
@@ -201,7 +201,8 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
201
201
  ctx.ui.notify(`Unknown phase "${phase}". Valid phases: research, plan, execute, complete, validate, reassess, uat, replan.`, "warning");
202
202
  return;
203
203
  }
204
- const compatibilityError = getWorkflowTransportSupportError(ctx.model?.provider, getRequiredWorkflowToolsForAutoUnit(unitType), {
204
+ const compatibilityError = getUnitWorkflowDispatchReadinessError({
205
+ provider: ctx.model?.provider,
205
206
  projectRoot,
206
207
  surface: "direct phase dispatch",
207
208
  unitType,
@@ -24,7 +24,8 @@ import { isAutoActive } from "./auto.js";
24
24
  import { hostWriteGateAdapter } from "./bootstrap/write-gate.js";
25
25
  import { ensureWorkflowPreferencesCaptured } from "./planning-depth.js";
26
26
  import { MILESTONE_ID_RE } from "./milestone-ids.js";
27
- import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit, resolveWorkflowMcpProjectRoot, } from "./workflow-mcp.js";
27
+ import { resolveWorkflowMcpProjectRoot } from "./workflow-mcp.js";
28
+ import { getUnitWorkflowDispatchReadinessError } from "./tool-contract.js";
28
29
  import { prepareBrowserDaemonForUat } from "./browser-daemon-auto-prep.js";
29
30
  import { PROJECT_RESEARCH_INFLIGHT_MARKER, } from "./project-research-policy.js";
30
31
  import { isWorkflowPrefsCaptured, resolveDeepProjectSetupState, } from "./deep-project-setup-policy.js";
@@ -512,7 +513,15 @@ export const DISPATCH_RULES = [
512
513
  // Transport preflight: verify required MCP tools are actually connected
513
514
  // before consuming a retry attempt. Fixes tool-starved sessions burning
514
515
  // all MAX_UAT_ATTEMPTS before stopping (#477).
515
- const transportError = getWorkflowTransportSupportError(sessionProvider, getRequiredWorkflowToolsForAutoUnit("run-uat"), { projectRoot: basePath, surface: "auto-mode", unitType: "run-uat", authMode: sessionAuthMode, baseUrl: sessionBaseUrl, activeTools });
516
+ const transportError = getUnitWorkflowDispatchReadinessError({
517
+ provider: sessionProvider,
518
+ projectRoot: basePath,
519
+ surface: "auto-mode",
520
+ unitType: "run-uat",
521
+ authMode: sessionAuthMode,
522
+ baseUrl: sessionBaseUrl,
523
+ activeTools,
524
+ });
516
525
  if (transportError) {
517
526
  return { action: "stop", reason: transportError, level: "warning" };
518
527
  }
@@ -13,7 +13,7 @@ import { getSessionModelOverride } from "./session-model-override.js";
13
13
  import { logWarning } from "./workflow-logger.js";
14
14
  import { resolveUokFlags } from "./uok/flags.js";
15
15
  import { applyModelPolicyFilter } from "./uok/model-policy.js";
16
- import { isModelBlocked } from "./blocked-models.js";
16
+ import { isModelBlocked, isModelTemporarilyUnavailable } from "./blocked-models.js";
17
17
  import { getRequiredWorkflowToolsForAutoUnit, isWorkflowMcpSurfaceTool } from "./workflow-mcp.js";
18
18
  /**
19
19
  * Thrown when the model-policy gate rejects every candidate model for a unit
@@ -218,6 +218,10 @@ function buildModelPolicyBlockReasons(policyDenyReasons, availableModels, routin
218
218
  reason: `configured model(s) did not resolve against policy-eligible registry [${eligibleSummary}]`,
219
219
  }];
220
220
  }
221
+ function isModelUnavailable(basePath, provider, id) {
222
+ return isModelBlocked(basePath, provider, id) ||
223
+ isModelTemporarilyUnavailable(basePath, provider, id);
224
+ }
221
225
  function restoreToolBaseline(pi) {
222
226
  const key = pi;
223
227
  const baseline = TOOL_BASELINE.get(key);
@@ -657,8 +661,8 @@ autoModeStartThinkingLevel) {
657
661
  // (issue #4513). The block is persisted in .gsd/runtime/blocked-models.json
658
662
  // so it survives /gsd auto restarts — without this, the same dead model
659
663
  // gets reselected after every restart.
660
- if (isModelBlocked(basePath, model.provider, model.id)) {
661
- ctx.ui.notify(`Skipping blocked model ${model.provider}/${model.id} (provider rejected it for this account).`, "warning");
664
+ if (isModelUnavailable(basePath, model.provider, model.id)) {
665
+ ctx.ui.notify(`Skipping unavailable model ${model.provider}/${model.id}.`, "warning");
662
666
  continue;
663
667
  }
664
668
  // Warn if the ID is ambiguous across providers
@@ -724,7 +728,7 @@ autoModeStartThinkingLevel) {
724
728
  const key = `${model.provider.toLowerCase()}/${model.id.toLowerCase()}`;
725
729
  if (!policyAllowedModelKeys.has(key))
726
730
  continue;
727
- if (isModelBlocked(basePath, model.provider, model.id))
731
+ if (isModelUnavailable(basePath, model.provider, model.id))
728
732
  continue;
729
733
  const ok = await pi.setModel(model, { persist: false });
730
734
  if (!ok)
@@ -746,16 +750,16 @@ autoModeStartThinkingLevel) {
746
750
  // No model preference for this unit type — re-apply the model captured
747
751
  // at auto-mode start to prevent bleed from shared global settings.json (#650).
748
752
  const availableModels = buildModelPolicyCandidates(ctx, autoModeStartModel, effectiveSessionModelOverride);
749
- const startBlocked = isModelBlocked(basePath, autoModeStartModel.provider, autoModeStartModel.id);
753
+ const startBlocked = isModelUnavailable(basePath, autoModeStartModel.provider, autoModeStartModel.id);
750
754
  if (startBlocked) {
751
- ctx.ui.notify(`Auto-mode start model ${autoModeStartModel.provider}/${autoModeStartModel.id} is blocked for this account. Using current session model instead.`, "warning");
755
+ ctx.ui.notify(`Auto-mode start model ${autoModeStartModel.provider}/${autoModeStartModel.id} is unavailable. Using current session model instead.`, "warning");
752
756
  }
753
757
  else {
754
758
  const startModel = availableModels.find(m => m.provider === autoModeStartModel.provider && m.id === autoModeStartModel.id);
755
759
  if (startModel) {
756
760
  const ok = await pi.setModel(startModel, { persist: false });
757
761
  if (!ok) {
758
- const byId = availableModels.find(m => m.id === autoModeStartModel.id && !isModelBlocked(basePath, m.provider, m.id));
762
+ const byId = availableModels.find(m => m.id === autoModeStartModel.id && !isModelUnavailable(basePath, m.provider, m.id));
759
763
  if (byId) {
760
764
  const fallbackOk = await pi.setModel(byId, { persist: false });
761
765
  if (fallbackOk) {
@@ -1704,13 +1704,24 @@ export async function postUnitPreVerification(pctx, opts) {
1704
1704
  }
1705
1705
  else if (!triggerArtifactVerified) {
1706
1706
  if (s.lastToolInvocationError && isToolUnavailableError(s.lastToolInvocationError)) {
1707
- // Tool-unavailable is the one transient invocation error: the
1708
- // workflow MCP server registers its surface asynchronously, so a
1709
- // Unit's first call can race the registration. Fall through to the
1710
- // bounded verification retry instead of pausing.
1711
- debugLog("postUnit", { phase: "tool-unavailable-retry", unitType: s.currentUnit.type, unitId: s.currentUnit.id, error: s.lastToolInvocationError });
1712
- ctx.ui.notify(`Tool unavailable for ${s.currentUnit.type}: ${s.lastToolInvocationError}. The tool surface may still be registering — retrying.`, "warning");
1707
+ // Tool-unavailable is transient: the workflow MCP server registers
1708
+ // its surface asynchronously, so a Unit's first call can race the
1709
+ // registration. Retry with escalating delay, bounded at 3 attempts.
1710
+ // ponytail: MAX constant so the guard, log, and display all agree
1711
+ const MAX_TOOL_UNAVAIL_RETRIES = 3;
1712
+ if (s.toolUnavailableRetries >= MAX_TOOL_UNAVAIL_RETRIES) {
1713
+ debugLog("postUnit", { phase: "tool-unavailable-exhausted", unitType: s.currentUnit.type, unitId: s.currentUnit.id, retries: s.toolUnavailableRetries });
1714
+ ctx.ui.notify(`Tool unavailable for ${s.currentUnit.type} after ${MAX_TOOL_UNAVAIL_RETRIES} retries: ${s.lastToolInvocationError}. MCP server may not be starting — pausing auto-mode.`, "error");
1715
+ s.lastToolInvocationError = null;
1716
+ await pauseAuto(ctx, pi);
1717
+ return "dispatched";
1718
+ }
1719
+ s.toolUnavailableRetries++;
1720
+ const delayMs = s.toolUnavailableRetries * 1000;
1721
+ debugLog("postUnit", { phase: "tool-unavailable-retry", unitType: s.currentUnit.type, unitId: s.currentUnit.id, error: s.lastToolInvocationError, attempt: s.toolUnavailableRetries, delayMs });
1722
+ ctx.ui.notify(`Tool unavailable for ${s.currentUnit.type}: ${s.lastToolInvocationError}. Waiting ${delayMs}ms for MCP server — retry ${s.toolUnavailableRetries}/${MAX_TOOL_UNAVAIL_RETRIES}.`, "warning");
1713
1723
  s.lastToolInvocationError = null;
1724
+ await new Promise(r => setTimeout(r, delayMs));
1714
1725
  }
1715
1726
  else if (s.lastToolInvocationError) {
1716
1727
  const isUserSkip = /queued user message/i.test(s.lastToolInvocationError);
@@ -1825,6 +1836,7 @@ export async function postUnitPreVerification(pctx, opts) {
1825
1836
  if (s.pendingVerificationRetry?.unitId === s.currentUnit.id) {
1826
1837
  s.pendingVerificationRetry = null;
1827
1838
  }
1839
+ s.toolUnavailableRetries = 0;
1828
1840
  s.verificationRetryCount.delete(retryKey);
1829
1841
  s.verificationRetryFailureHashes.delete(retryKey);
1830
1842
  s.exhaustedVerificationUnits.delete(retryKey);
@@ -36,17 +36,17 @@ export function isSuspiciousGhostCompletion(ctx, startedAt, maxElapsedMs = GHOST
36
36
  activity.assistantMessages === 0);
37
37
  }
38
38
  /**
39
- * Snapshot metrics, save activity log, and fire-and-forget memory extraction
40
- * for a completed unit. Returns the activity log file path (if any).
39
+ * Snapshot metrics, save activity log, extract memories, and record the git
40
+ * transaction for a completed auto-mode unit.
41
41
  */
42
- export async function closeoutUnit(ctx, basePath, unitType, unitId, startedAt, opts) {
43
- const modelId = ctx.model?.id ?? "unknown";
44
- snapshotUnitMetrics(ctx, unitType, unitId, startedAt, modelId, opts);
45
- const activityFile = saveActivityLog(ctx, basePath, unitType, unitId);
42
+ export async function closeoutAutoUnit(request) {
43
+ const modelId = request.ctx.model?.id ?? "unknown";
44
+ snapshotUnitMetrics(request.ctx, request.unitType, request.unitId, request.startedAt, modelId, request.opts);
45
+ const activityFile = saveActivityLog(request.ctx, request.basePath, request.unitType, request.unitId);
46
46
  if (activityFile) {
47
47
  try {
48
- const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
49
- const llmCallFn = buildMemoryLLMCall(ctx);
48
+ const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import("./memory-extractor.js");
49
+ const llmCallFn = buildMemoryLLMCall(request.ctx);
50
50
  if (llmCallFn) {
51
51
  // Awaited: a fire-and-forget here lets memory-extractor writes land in
52
52
  // .gsd/ after closeoutUnit returns but before the milestone merge
@@ -55,10 +55,10 @@ export async function closeoutUnit(ctx, basePath, unitType, unitId, startedAt, o
55
55
  // bounded by the extractor's LLM call, which is the acceptable price
56
56
  // for not racing the merge boundary.
57
57
  try {
58
- await extractMemoriesFromUnit(activityFile, unitType, unitId, llmCallFn);
58
+ await extractMemoriesFromUnit(activityFile, request.unitType, request.unitId, llmCallFn);
59
59
  }
60
60
  catch (err) {
61
- logWarning("engine", `memory extraction failed for ${unitType}/${unitId}: ${err.message}`);
61
+ logWarning("engine", `memory extraction failed for ${request.unitType}/${request.unitId}: ${err.message}`);
62
62
  }
63
63
  }
64
64
  }
@@ -66,22 +66,46 @@ export async function closeoutUnit(ctx, basePath, unitType, unitId, startedAt, o
66
66
  logWarning("engine", `operation failed: ${err instanceof Error ? err.message : String(err)}`);
67
67
  }
68
68
  }
69
- if (opts?.traceId && opts.turnId && opts.gitAction && opts.gitStatus) {
69
+ const gitTransaction = resolveGitTransactionOptions(request.opts);
70
+ if (gitTransaction) {
70
71
  writeTurnGitTransaction({
71
- basePath,
72
- traceId: opts.traceId,
73
- turnId: opts.turnId,
74
- unitType,
75
- unitId,
72
+ basePath: request.basePath,
73
+ traceId: gitTransaction.traceId,
74
+ turnId: gitTransaction.turnId,
75
+ unitType: request.unitType,
76
+ unitId: request.unitId,
76
77
  stage: "record",
77
- action: opts.gitAction,
78
- push: opts.gitPush === true,
79
- status: opts.gitStatus,
80
- error: opts.gitError,
78
+ action: gitTransaction.gitAction,
79
+ push: gitTransaction.gitPush === true,
80
+ status: gitTransaction.gitStatus,
81
+ error: gitTransaction.gitError,
81
82
  metadata: {
82
83
  activityFile,
83
84
  },
84
85
  });
85
86
  }
86
- return activityFile ?? undefined;
87
+ return {
88
+ ...(activityFile ? { activityFile } : {}),
89
+ gitTransactionRecorded: Boolean(gitTransaction),
90
+ };
91
+ }
92
+ function resolveGitTransactionOptions(opts) {
93
+ if (!opts?.traceId || !opts.turnId || !opts.gitAction || !opts.gitStatus)
94
+ return null;
95
+ return {
96
+ traceId: opts.traceId,
97
+ turnId: opts.turnId,
98
+ gitAction: opts.gitAction,
99
+ gitStatus: opts.gitStatus,
100
+ gitPush: opts.gitPush,
101
+ gitError: opts.gitError,
102
+ };
103
+ }
104
+ /**
105
+ * Compatibility wrapper for existing auto-loop callers. New code should prefer
106
+ * closeoutAutoUnit so the closeout request and result stay explicit.
107
+ */
108
+ export async function closeoutUnit(ctx, basePath, unitType, unitId, startedAt, opts) {
109
+ const result = await closeoutAutoUnit({ ctx, basePath, unitType, unitId, startedAt, opts });
110
+ return result.activityFile;
87
111
  }
@@ -23,6 +23,7 @@ import { getSlice } from "./gsd-db.js";
23
23
  import { getLedger } from "./metrics.js";
24
24
  import { getUnitCostSpikeAction, resolveUnitCostSpikeMultiplier } from "./auto-budget.js";
25
25
  import { formatPostUnitStatusCard } from "./auto-status-message.js";
26
+ import { detectWebApp } from "./web-app-uat.js";
26
27
  function getCurrentUnitCostStats(unitId) {
27
28
  const ledger = getLedger();
28
29
  if (!ledger || !Array.isArray(ledger.units) || ledger.units.length === 0) {
@@ -617,14 +618,25 @@ export async function runPostUnitVerification(vctx, pauseAuto) {
617
618
  s.pendingVerificationRetry = null;
618
619
  return "continue";
619
620
  }
621
+ else if (verdict.reason === "no-host-checks" &&
622
+ taskAlreadyComplete &&
623
+ detectWebApp(s.basePath) &&
624
+ !result.runtimeErrors?.some((e) => e.blocking)) {
625
+ s.verificationRetryCount.delete(retryKey);
626
+ s.verificationRetryFailureHashes.delete(retryKey);
627
+ s.pendingVerificationRetry = null;
628
+ ctx.ui.notify("No task-level host verification command was found for a completed browser-facing task; continuing so slice UAT can verify the UI with browser tools.", "warning");
629
+ return "continue";
630
+ }
620
631
  else if (verdict.reason === "no-host-checks") {
621
632
  s.verificationRetryCount.delete(retryKey);
622
633
  s.verificationRetryFailureHashes.delete(retryKey);
623
634
  s.pendingVerificationRetry = null;
624
- ctx.ui.notify("Verification gate FAILED no runnable host-owned verification checks were discovered. Pausing for human review.", "error");
635
+ const pauseMessage = `Verification failed: ${verdict.failureContext}`;
636
+ ctx.ui.notify(`Verification gate FAILED — ${verdict.failureContext}`, "error");
625
637
  process.stderr.write(`verification-gate: ${verdict.failureContext}\n`);
626
638
  await pauseAuto(ctx, pi, {
627
- message: "Verification failed: no runnable host-owned verification checks were discovered.",
639
+ message: pauseMessage,
628
640
  category: "unknown",
629
641
  });
630
642
  return "pause";
@@ -557,6 +557,13 @@ export function getAutoModeStartModel() {
557
557
  export function setCurrentDispatchedModelId(model) {
558
558
  s.currentDispatchedModelId = model ? `${model.provider}/${model.id}` : null;
559
559
  }
560
+ /**
561
+ * Update the active unit model after runtime recovery switches models mid-unit.
562
+ * The next session restore path reads this field before dispatching again.
563
+ */
564
+ export function setCurrentUnitModelForRecovery(model) {
565
+ s.currentUnitModel = model;
566
+ }
560
567
  // Tool tracking — delegates to auto-tool-tracking.ts
561
568
  export function markToolStart(toolCallId, toolName) {
562
569
  _markToolStart(toolCallId, s.active, toolName);
@@ -623,6 +630,35 @@ export function stopAutoRemote(projectRoot) {
623
630
  return { found: false, error: err.message };
624
631
  }
625
632
  }
633
+ /**
634
+ * Force-stop a remote auto-mode session before stealing its lock.
635
+ * The normal stop path stays SIGTERM-only so cooperative sessions can clean up;
636
+ * this path is only for the explicit "Force start" action.
637
+ */
638
+ export function forceStopAutoRemote(projectRoot) {
639
+ const lock = readCrashLock(projectRoot);
640
+ if (!lock)
641
+ return { found: false };
642
+ if (lock.pid === process.pid) {
643
+ clearLock(projectRoot);
644
+ return { found: false };
645
+ }
646
+ if (!isLockProcessAlive(lock)) {
647
+ clearLock(projectRoot);
648
+ return { found: false };
649
+ }
650
+ try {
651
+ process.kill(lock.pid, "SIGTERM");
652
+ if (isLockProcessAlive(lock)) {
653
+ process.kill(lock.pid, "SIGKILL");
654
+ }
655
+ clearLock(projectRoot);
656
+ return { found: true, pid: lock.pid };
657
+ }
658
+ catch (err) {
659
+ return { found: false, error: err.message };
660
+ }
661
+ }
626
662
  /**
627
663
  * Check if a remote auto-mode session is running (from a different process).
628
664
  * Reads the crash lock, checks PID liveness, and returns session details.
@@ -1824,7 +1860,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1824
1860
  if (freshStartAssessment.classification === "running") {
1825
1861
  const pid = freshStartAssessment.lock?.pid;
1826
1862
  ctx.ui.notify(pid
1827
- ? `Another auto-mode session (PID ${pid}) appears to be running.\nStop it with \`kill ${pid}\` before starting a new session.`
1863
+ ? `Another auto-mode session (PID ${pid}) appears to be running.\nRun \`/gsd stop\` for graceful shutdown, or choose "Force start" from \`/gsd auto\` to terminate it.`
1828
1864
  : "Another auto-mode session appears to be running.", "error");
1829
1865
  return;
1830
1866
  }
@@ -9,12 +9,16 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
  import { gsdRoot } from "./paths.js";
11
11
  import { withFileLockSync } from "./file-lock.js";
12
+ const temporaryBlockedModels = new Map();
12
13
  function blockedModelsPath(basePath) {
13
14
  return join(gsdRoot(basePath), "runtime", "blocked-models.json");
14
15
  }
15
16
  function modelKey(provider, id) {
16
17
  return `${provider.toLowerCase()}/${id.toLowerCase()}`;
17
18
  }
19
+ function temporaryModelKey(basePath, provider, id) {
20
+ return `${basePath}:${modelKey(provider, id)}`;
21
+ }
18
22
  function readFileSafe(path) {
19
23
  if (!existsSync(path))
20
24
  return { version: 1, blocked: [] };
@@ -41,6 +45,30 @@ export function isModelBlocked(basePath, provider, id) {
41
45
  const target = modelKey(provider, id);
42
46
  return loadBlockedModels(basePath).some((e) => modelKey(e.provider, e.id) === target);
43
47
  }
48
+ export function blockModelUntil(basePath, provider, id, blockedUntil, reason) {
49
+ const key = temporaryModelKey(basePath, provider, id);
50
+ if (blockedUntil <= Date.now()) {
51
+ temporaryBlockedModels.delete(key);
52
+ return;
53
+ }
54
+ temporaryBlockedModels.set(key, { provider, id, reason, blockedUntil });
55
+ }
56
+ export function isModelTemporarilyUnavailable(basePath, provider, id, now = Date.now()) {
57
+ if (!provider || !id)
58
+ return false;
59
+ const key = temporaryModelKey(basePath, provider, id);
60
+ const entry = temporaryBlockedModels.get(key);
61
+ if (!entry)
62
+ return false;
63
+ if (entry.blockedUntil <= now) {
64
+ temporaryBlockedModels.delete(key);
65
+ return false;
66
+ }
67
+ return true;
68
+ }
69
+ export function clearTemporaryModelBlocksForTest() {
70
+ temporaryBlockedModels.clear();
71
+ }
44
72
  export function blockModel(basePath, provider, id, reason) {
45
73
  const path = blockedModelsPath(basePath);
46
74
  mkdirSync(dirname(path), { recursive: true });