@opengsd/gsd-pi 1.1.1-dev.9bb7453 → 1.1.1-dev.9f86580

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 (219) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +18 -2
  3. package/dist/resources/extensions/browser-tools/engine/selection.js +1 -1
  4. package/dist/resources/extensions/browser-tools/extension-manifest.json +1 -1
  5. package/dist/resources/extensions/browser-tools/index.js +29 -2
  6. package/dist/resources/extensions/browser-tools/web-app-detect.js +52 -0
  7. package/dist/resources/extensions/gsd/auto/phases.js +45 -3
  8. package/dist/resources/extensions/gsd/auto/session.js +2 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +21 -2
  10. package/dist/resources/extensions/gsd/auto-model-selection.js +26 -0
  11. package/dist/resources/extensions/gsd/auto-prompts.js +4 -0
  12. package/dist/resources/extensions/gsd/auto-recovery.js +3 -4
  13. package/dist/resources/extensions/gsd/auto-timers.js +24 -10
  14. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +18 -66
  15. package/dist/resources/extensions/gsd/auto-worktree.js +18 -5
  16. package/dist/resources/extensions/gsd/auto.js +26 -4
  17. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +16 -10
  18. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +48 -29
  19. package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
  20. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +18 -29
  21. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +61 -0
  22. package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -0
  23. package/dist/resources/extensions/gsd/commands-mcp-status.js +1 -1
  24. package/dist/resources/extensions/gsd/config-overlay.js +1 -0
  25. package/dist/resources/extensions/gsd/context-masker.js +129 -5
  26. package/dist/resources/extensions/gsd/guided-flow.js +93 -108
  27. package/dist/resources/extensions/gsd/milestone-closeout.js +3 -1
  28. package/dist/resources/extensions/gsd/pending-auto-start.js +0 -1
  29. package/dist/resources/extensions/gsd/planner-handoff.js +98 -0
  30. package/dist/resources/extensions/gsd/preferences-models.js +1 -0
  31. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/run-uat.md +5 -19
  33. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  34. package/dist/resources/extensions/gsd/recovery-classification.js +20 -0
  35. package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
  36. package/dist/resources/extensions/gsd/tool-contract.js +6 -1
  37. package/dist/resources/extensions/gsd/tool-presentation-plan.js +47 -7
  38. package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -1
  39. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +113 -8
  40. package/dist/resources/extensions/gsd/unit-tool-contracts.js +193 -0
  41. package/dist/resources/extensions/gsd/workflow-mcp.js +5 -78
  42. package/dist/resources/extensions/gsd/worktree-manager.js +26 -0
  43. package/dist/resources/extensions/gsd/worktree-reentry.js +96 -0
  44. package/dist/resources/extensions/shared/gsd-browser-cli.js +6 -0
  45. package/dist/web/standalone/.next/BUILD_ID +1 -1
  46. package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
  47. package/dist/web/standalone/.next/build-manifest.json +2 -2
  48. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  49. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.html +1 -1
  66. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
  73. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  74. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  76. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  77. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  78. package/package.json +1 -1
  79. package/packages/cloud-mcp-gateway/package.json +2 -2
  80. package/packages/contracts/package.json +1 -1
  81. package/packages/daemon/package.json +4 -4
  82. package/packages/gsd-agent-core/package.json +5 -5
  83. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  84. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +5 -0
  85. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  86. package/packages/gsd-agent-modes/package.json +7 -7
  87. package/packages/mcp-server/package.json +3 -3
  88. package/packages/native/package.json +1 -1
  89. package/packages/pi-agent-core/dist/agent-loop.js +4 -3
  90. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  91. package/packages/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
  92. package/packages/pi-agent-core/dist/harness/agent-harness.js +3 -1
  93. package/packages/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
  94. package/packages/pi-agent-core/dist/harness/types.d.ts +1 -0
  95. package/packages/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  96. package/packages/pi-agent-core/dist/harness/types.js.map +1 -1
  97. package/packages/pi-agent-core/dist/types.d.ts +3 -1
  98. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  99. package/packages/pi-agent-core/dist/types.js.map +1 -1
  100. package/packages/pi-agent-core/package.json +1 -1
  101. package/packages/pi-ai/dist/models.generated.d.ts +157 -18
  102. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  103. package/packages/pi-ai/dist/models.generated.js +159 -36
  104. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  105. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  106. package/packages/pi-ai/dist/providers/transform-messages.js +8 -1
  107. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  108. package/packages/pi-ai/package.json +1 -1
  109. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +3 -0
  110. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
  113. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/tools/edit.js +3 -2
  116. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts +1 -0
  118. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/tools/render-utils.js +6 -0
  120. package/packages/pi-coding-agent/dist/core/tools/render-utils.js.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/tools/write.js +3 -2
  123. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  124. package/packages/pi-coding-agent/package.json +7 -7
  125. package/packages/pi-tui/package.json +1 -1
  126. package/packages/rpc-client/package.json +2 -2
  127. package/pkg/package.json +1 -1
  128. package/scripts/install/handoff.js +16 -3
  129. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +21 -2
  130. package/src/resources/extensions/browser-tools/engine/selection.ts +1 -1
  131. package/src/resources/extensions/browser-tools/extension-manifest.json +1 -1
  132. package/src/resources/extensions/browser-tools/index.ts +36 -5
  133. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +2 -2
  134. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +37 -0
  135. package/src/resources/extensions/browser-tools/tests/web-app-detect.test.mjs +68 -0
  136. package/src/resources/extensions/browser-tools/web-app-detect.ts +63 -0
  137. package/src/resources/extensions/gsd/auto/phases.ts +48 -6
  138. package/src/resources/extensions/gsd/auto/session.ts +2 -0
  139. package/src/resources/extensions/gsd/auto-dispatch.ts +48 -2
  140. package/src/resources/extensions/gsd/auto-model-selection.ts +26 -0
  141. package/src/resources/extensions/gsd/auto-prompts.ts +4 -0
  142. package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
  143. package/src/resources/extensions/gsd/auto-timers.ts +25 -9
  144. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +43 -74
  145. package/src/resources/extensions/gsd/auto-worktree.ts +23 -5
  146. package/src/resources/extensions/gsd/auto.ts +28 -4
  147. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +16 -10
  148. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +63 -29
  149. package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
  150. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +50 -54
  151. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +137 -0
  152. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -0
  153. package/src/resources/extensions/gsd/commands-mcp-status.ts +1 -1
  154. package/src/resources/extensions/gsd/config-overlay.ts +1 -0
  155. package/src/resources/extensions/gsd/context-masker.ts +152 -5
  156. package/src/resources/extensions/gsd/guided-flow.ts +128 -135
  157. package/src/resources/extensions/gsd/milestone-closeout.ts +3 -1
  158. package/src/resources/extensions/gsd/pending-auto-start.ts +0 -2
  159. package/src/resources/extensions/gsd/planner-handoff.ts +149 -0
  160. package/src/resources/extensions/gsd/preferences-models.ts +1 -0
  161. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  162. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  163. package/src/resources/extensions/gsd/prompts/run-uat.md +5 -19
  164. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  165. package/src/resources/extensions/gsd/recovery-classification.ts +20 -0
  166. package/src/resources/extensions/gsd/skill-manifest.ts +12 -0
  167. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +99 -0
  168. package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +66 -4
  169. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +10 -2
  170. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +4 -1
  171. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +4 -0
  172. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +12 -2
  173. package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -0
  174. package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +9 -15
  175. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +26 -16
  176. package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +21 -0
  177. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +118 -0
  178. package/src/resources/extensions/gsd/tests/context-masker.test.ts +56 -1
  179. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +1 -0
  180. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +40 -1
  181. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +24 -0
  182. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +31 -79
  183. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +5 -3
  184. package/src/resources/extensions/gsd/tests/guided-flow-state-rebuild.test.ts +40 -4
  185. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +8 -0
  186. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +16 -0
  187. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +7 -1
  188. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +27 -0
  189. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -0
  190. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +7 -1
  191. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +1 -1
  192. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +63 -0
  193. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +10 -1
  194. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +9 -1
  195. package/src/resources/extensions/gsd/tests/planner-handoff.test.ts +100 -0
  196. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +147 -5
  197. package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
  198. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +44 -0
  199. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +4 -0
  200. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +56 -0
  201. package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
  202. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -4
  203. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
  204. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +409 -0
  205. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
  206. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
  207. package/src/resources/extensions/gsd/tool-contract.ts +7 -1
  208. package/src/resources/extensions/gsd/tool-presentation-plan.ts +82 -7
  209. package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -1
  210. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +146 -9
  211. package/src/resources/extensions/gsd/unit-tool-contracts.ts +210 -0
  212. package/src/resources/extensions/gsd/workflow-mcp.ts +5 -78
  213. package/src/resources/extensions/gsd/worktree-manager.ts +32 -0
  214. package/src/resources/extensions/gsd/worktree-reentry.ts +103 -0
  215. package/src/resources/extensions/shared/gsd-browser-cli.ts +6 -0
  216. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +0 -246
  217. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +0 -218
  218. /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → zzYMrKpPGfRQRxSFO32Jr}/_buildManifest.js +0 -0
  219. /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → zzYMrKpPGfRQRxSFO32Jr}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- ba1c57462cf67a0e
1
+ f692671bcb7f8bc4
@@ -435,11 +435,27 @@ function formatManagedBrowserError(toolName, error) {
435
435
  return [
436
436
  `gsd-browser engine or tool unavailable for ${toolName}: ${message}`,
437
437
  "",
438
- "GSD browser automation now uses the managed gsd-browser engine by default.",
438
+ "The managed gsd-browser engine is enabled for this session but is unavailable.",
439
439
  "Run /gsd doctor or reinstall dependencies so @opengsd/gsd-browser is available.",
440
- "Set GSD_BROWSER_ENGINE=legacy only when you intentionally need the Playwright compatibility engine.",
440
+ "Unset GSD_BROWSER_ENGINE or set GSD_BROWSER_ENGINE=playwright to use the default Playwright engine.",
441
441
  ].join("\n");
442
442
  }
443
+ /**
444
+ * Eagerly establish the managed gsd-browser connection so browser tools are
445
+ * ready before first use. Best-effort: returns the error instead of throwing so
446
+ * callers (e.g. session-start warm-up) can surface a warning without failing the
447
+ * session. Connecting only spawns the gsd-browser MCP daemon; it does not launch
448
+ * Chrome (that happens lazily on the first navigation).
449
+ */
450
+ export async function warmUpManagedGsdBrowser(ctx, signal) {
451
+ try {
452
+ await getOrConnectManagedGsdBrowser(ctx, signal);
453
+ return { ok: true };
454
+ }
455
+ catch (error) {
456
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
457
+ }
458
+ }
443
459
  export function registerManagedGsdBrowserTools(pi) {
444
460
  for (const tool of MANAGED_BROWSER_TOOLS) {
445
461
  pi.registerTool({
@@ -1,4 +1,4 @@
1
- const DEFAULT_BROWSER_ENGINE = "gsd-browser";
1
+ const DEFAULT_BROWSER_ENGINE = "legacy";
2
2
  export function resolveBrowserEngineMode(env = process.env) {
3
3
  const raw = env.GSD_BROWSER_ENGINE?.trim();
4
4
  if (!raw)
@@ -2,7 +2,7 @@
2
2
  "id": "browser-tools",
3
3
  "name": "Browser Tools",
4
4
  "version": "1.0.0",
5
- "description": "GSD browser automation contract adapter backed by the managed gsd-browser engine",
5
+ "description": "GSD browser automation contract adapter backed by Playwright with optional managed gsd-browser support",
6
6
  "tier": "bundled",
7
7
  "requires": { "platform": ">=2.29.0" },
8
8
  "provides": {
@@ -1,7 +1,8 @@
1
1
  /** browser-tools — Pi Browser Automation Contract adapter. */
2
2
  import { importExtensionModule } from "@gsd/pi-coding-agent";
3
- import { closeManagedGsdBrowser, registerManagedGsdBrowserTools } from "./engine/managed-gsd-browser.js";
3
+ import { closeManagedGsdBrowser, registerManagedGsdBrowserTools, warmUpManagedGsdBrowser } from "./engine/managed-gsd-browser.js";
4
4
  import { resolveBrowserEngineMode } from "./engine/selection.js";
5
+ import { detectWebApp } from "./web-app-detect.js";
5
6
  let legacyRegistrationPromise = null;
6
7
  let managedRegistrationPromise = null;
7
8
  let registeredEngine = null;
@@ -147,6 +148,29 @@ async function registerBrowserTools(pi) {
147
148
  throw error;
148
149
  }
149
150
  }
151
+ function isWarmUpDisabled() {
152
+ const value = process.env.GSD_BROWSER_WARMUP?.trim().toLowerCase();
153
+ return value === "0" || value === "false" || value === "off";
154
+ }
155
+ /**
156
+ * Auto-initialize the managed gsd-browser engine only when explicitly selected
157
+ * for a web app. Best-effort and non-blocking: warm-up runs in the background
158
+ * and only surfaces a warning if it fails.
159
+ */
160
+ function maybeWarmUpManagedEngine(pi, ctx) {
161
+ if (isWarmUpDisabled())
162
+ return;
163
+ if (resolveBrowserEngineMode() !== "gsd-browser")
164
+ return;
165
+ const projectRoot = ctx.cwd || process.cwd();
166
+ if (!detectWebApp(projectRoot))
167
+ return;
168
+ void warmUpManagedGsdBrowser(ctx).then((result) => {
169
+ if (!result.ok && ctx.hasUI) {
170
+ ctx.ui.notify(`gsd-browser auto-init failed: ${result.error}. Browser UAT tools will retry on first use; run /gsd doctor if this persists.`, "warning");
171
+ }
172
+ });
173
+ }
150
174
  async function closeActiveBrowserEngines() {
151
175
  await closeManagedGsdBrowser();
152
176
  if (legacyRegistrationPromise) {
@@ -157,12 +181,15 @@ async function closeActiveBrowserEngines() {
157
181
  export default function (pi) {
158
182
  pi.on("session_start", async (_event, ctx) => {
159
183
  if (ctx.hasUI) {
160
- void registerBrowserTools(pi).catch((error) => {
184
+ void registerBrowserTools(pi)
185
+ .then(() => maybeWarmUpManagedEngine(pi, ctx))
186
+ .catch((error) => {
161
187
  ctx.ui.notify(`browser-tools failed to load: ${error instanceof Error ? error.message : String(error)}`, "warning");
162
188
  });
163
189
  return;
164
190
  }
165
191
  await registerBrowserTools(pi);
192
+ maybeWarmUpManagedEngine(pi, ctx);
166
193
  });
167
194
  pi.on("session_shutdown", async () => {
168
195
  await closeActiveBrowserEngines();
@@ -0,0 +1,52 @@
1
+ /**
2
+ * web-app-detect — lightweight, synchronous heuristic for deciding whether the
3
+ * project under development is a web app. Used only when the optional managed
4
+ * gsd-browser engine is selected and can be warmed before first use.
5
+ */
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+ // Frontend frameworks / bundlers whose presence in dependencies indicates a
9
+ // browser-facing web app worth warming the optional managed engine for.
10
+ const WEB_DEPENDENCY_RE = /^(react|react-dom|next|nuxt|vue|@vue\/|svelte|@sveltejs\/|solid-js|astro|@remix-run\/|gatsby|preact|@angular\/core|vite|@vitejs\/|@builder\.io\/qwik|@web\/dev-server|@11ty\/eleventy)/;
11
+ // package.json scripts that imply a dev server / browser-facing build.
12
+ const WEB_SCRIPT_RE = /\b(vite|next|nuxt|astro|remix|webpack(-dev-server)?|parcel|ng serve|serve\b|http-server|live-server|gatsby)\b/;
13
+ function readPackageJson(projectRoot) {
14
+ const packageJsonPath = resolve(projectRoot, "package.json");
15
+ if (!existsSync(packageJsonPath))
16
+ return null;
17
+ try {
18
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
19
+ return parsed && typeof parsed === "object" ? parsed : null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ function dependencyNames(pkg) {
26
+ return [
27
+ ...Object.keys(pkg.dependencies ?? {}),
28
+ ...Object.keys(pkg.devDependencies ?? {}),
29
+ ...Object.keys(pkg.peerDependencies ?? {}),
30
+ ];
31
+ }
32
+ /**
33
+ * Returns true when the project looks like a browser-facing web app. Conservative
34
+ * and dependency-free: a false negative just means lazy connection (the prior
35
+ * behavior); a false positive only warms an idle engine connection.
36
+ */
37
+ export function detectWebApp(projectRoot) {
38
+ const pkg = readPackageJson(projectRoot);
39
+ if (pkg) {
40
+ if (dependencyNames(pkg).some((name) => WEB_DEPENDENCY_RE.test(name)))
41
+ return true;
42
+ const scriptValues = Object.values(pkg.scripts ?? {}).filter((value) => typeof value === "string");
43
+ if (scriptValues.some((script) => WEB_SCRIPT_RE.test(script)))
44
+ return true;
45
+ }
46
+ // No package.json signal — fall back to a top-level index.html (static sites).
47
+ if (existsSync(resolve(projectRoot, "index.html")))
48
+ return true;
49
+ if (existsSync(resolve(projectRoot, "public", "index.html")))
50
+ return true;
51
+ return false;
52
+ }
@@ -16,6 +16,8 @@ import { detectStuck } from "./detect-stuck.js";
16
16
  import { runUnit } from "./run-unit.js";
17
17
  import { debugLog } from "../debug-logger.js";
18
18
  import { resolveWorktreeProjectRoot, normalizeWorktreePathForCompare } from "../worktree-root.js";
19
+ import { buildManualValidationGuidance } from "../worktree-manager.js";
20
+ import { relSliceFile } from "../paths.js";
19
21
  import { classifyProject } from "../detection.js";
20
22
  import { MergeConflictError } from "../git-service.js";
21
23
  import { setCurrentPhase, clearCurrentPhase } from "../../shared/gsd-phase-state.js";
@@ -47,6 +49,7 @@ import { resolveSafetyHarnessConfig } from "../safety/safety-harness.js";
47
49
  import { getContextPauseAction } from "../auto-budget.js";
48
50
  import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit, supportsStructuredQuestions, } from "../workflow-mcp.js";
49
51
  import { prepareWorkflowMcpForProject } from "../workflow-mcp-auto-prep.js";
52
+ import { getToolBaselineSnapshot } from "../auto-model-selection.js";
50
53
  import { resolveManifest } from "../unit-context-manifest.js";
51
54
  import { createWorktreeSafetyModule } from "../worktree-safety.js";
52
55
  import { isSuspiciousGhostCompletion } from "../auto-unit-closeout.js";
@@ -302,6 +305,8 @@ async function validateSourceWriteWorktreeSafety(ic, unitType, unitId, milestone
302
305
  // ─── Session timeout auto-resume state ────────────────────────────────────────
303
306
  let consecutiveSessionTimeouts = 0;
304
307
  const MAX_SESSION_TIMEOUT_AUTO_RESUMES = 3;
308
+ /** Maximum zero-tool-call retries before pausing — context exhaustion is deterministic. */
309
+ const MAX_ZERO_TOOL_RETRIES = 1;
305
310
  export function resetSessionTimeoutState() {
306
311
  consecutiveSessionTimeouts = 0;
307
312
  }
@@ -1070,7 +1075,13 @@ export async function runDispatch(ic, preData, loopState) {
1070
1075
  const authMode = provider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
1071
1076
  ? ctx.modelRegistry.getProviderAuthMode(provider)
1072
1077
  : undefined;
1073
- const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
1078
+ // Use the baseline snapshot rather than the live active-tool set: a prior
1079
+ // unit's per-provider narrowing (hook overrides, Groq 128-tool cap, etc.)
1080
+ // can strip required MCP tools from the live set even though
1081
+ // selectAndApplyModel will restore them before the unit is dispatched.
1082
+ // Checking a stale-narrowed set causes false transport-preflight warnings
1083
+ // that repeat on every /gsd auto resume (#477 follow-up).
1084
+ const activeTools = getToolBaselineSnapshot(pi);
1074
1085
  // Deep planning intentionally keeps human checkpoints in plain chat. In
1075
1086
  // Claude Code/local MCP transports, structured question requests can be
1076
1087
  // cancelled outside the normal chat flow, which made approval gates easy to
@@ -1093,6 +1104,9 @@ export async function runDispatch(ic, preData, loopState) {
1093
1104
  sessionContextWindow: ctx.model?.contextWindow,
1094
1105
  sessionProvider: ctx.model?.provider,
1095
1106
  modelRegistry: ctx.modelRegistry,
1107
+ activeTools,
1108
+ sessionBaseUrl: ctx.model?.baseUrl,
1109
+ sessionAuthMode: authMode,
1096
1110
  });
1097
1111
  if (isUnhandledPhaseWarning(dispatchResult)) {
1098
1112
  deps.invalidateAllCaches();
@@ -1116,6 +1130,9 @@ export async function runDispatch(ic, preData, loopState) {
1116
1130
  sessionContextWindow: ctx.model?.contextWindow,
1117
1131
  sessionProvider: ctx.model?.provider,
1118
1132
  modelRegistry: ctx.modelRegistry,
1133
+ activeTools,
1134
+ sessionBaseUrl: ctx.model?.baseUrl,
1135
+ sessionAuthMode: authMode,
1119
1136
  });
1120
1137
  }
1121
1138
  if (dispatchResult.action === "stop") {
@@ -2059,13 +2076,23 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
2059
2076
  });
2060
2077
  }
2061
2078
  else {
2079
+ const zeroToolKey = `${unitType}/${unitId}`;
2080
+ const attempt = (s.zeroToolRetryCount.get(zeroToolKey) ?? 0) + 1;
2062
2081
  debugLog("runUnitPhase", {
2063
2082
  phase: "zero-tool-calls",
2064
2083
  unitType,
2065
2084
  unitId,
2085
+ attempt,
2066
2086
  warning: "Unit completed with 0 tool calls — likely context exhaustion, marking as failed",
2067
2087
  });
2068
- ctx.ui.notify(`${unitType} ${unitId} completed with 0 tool calls — context exhaustion, will retry`, "warning");
2088
+ if (attempt > MAX_ZERO_TOOL_RETRIES) {
2089
+ s.zeroToolRetryCount.delete(zeroToolKey);
2090
+ ctx.ui.notify(`${unitType} ${unitId} completed with 0 tool calls — context exhaustion, pausing auto-mode after ${MAX_ZERO_TOOL_RETRIES} retry.`, "error");
2091
+ await deps.pauseAuto(ctx, pi);
2092
+ return { action: "break", reason: "zero-tool-calls-exhausted" };
2093
+ }
2094
+ s.zeroToolRetryCount.set(zeroToolKey, attempt);
2095
+ ctx.ui.notify(`${unitType} ${unitId} completed with 0 tool calls — context exhaustion, will retry (attempt ${attempt}/${MAX_ZERO_TOOL_RETRIES})`, "warning");
2069
2096
  return {
2070
2097
  action: "retry",
2071
2098
  reason: "zero-tool-calls",
@@ -2087,6 +2114,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
2087
2114
  if (artifactVerified) {
2088
2115
  s.unitDispatchCount.delete(dispatchKey);
2089
2116
  s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
2117
+ s.zeroToolRetryCount.delete(dispatchKey);
2090
2118
  }
2091
2119
  // Write phase handoff anchor after successful research/planning completion
2092
2120
  const anchorPhases = new Set(["research-milestone", "research-slice", "plan-milestone", "plan-slice"]);
@@ -2232,7 +2260,21 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
2232
2260
  }
2233
2261
  }
2234
2262
  if (pauseAfterUatDispatch) {
2235
- ctx.ui.notify("UAT requires human execution. Auto-mode will pause after this unit writes the result file.", "info");
2263
+ const pauseMid = iterData.mid;
2264
+ const pauseSliceId = pauseMid && iterData.unitId.startsWith(`${pauseMid}/`)
2265
+ ? iterData.unitId.slice(pauseMid.length + 1)
2266
+ : undefined;
2267
+ const guidance = pauseMid
2268
+ ? buildManualValidationGuidance(s.basePath, pauseMid, {
2269
+ uatPath: pauseSliceId
2270
+ ? relSliceFile(s.basePath, pauseMid, pauseSliceId, "UAT")
2271
+ : undefined,
2272
+ })
2273
+ : null;
2274
+ const pauseMessage = guidance
2275
+ ? `UAT requires human execution. Auto-mode will pause after this unit writes the result file.\n\n${guidance}`
2276
+ : "UAT requires human execution. Auto-mode will pause after this unit writes the result file.";
2277
+ ctx.ui.notify(pauseMessage, "info");
2236
2278
  await deps.pauseAuto(ctx, pi);
2237
2279
  debugLog("autoLoop", { phase: "exit", reason: "uat-pause" });
2238
2280
  clearFinalizingUnit();
@@ -94,6 +94,7 @@ export class AutoSession {
94
94
  verificationRetryCount = new Map();
95
95
  verificationRetryFailureHashes = new Map();
96
96
  exhaustedVerificationUnits = new Set();
97
+ zeroToolRetryCount = new Map();
97
98
  pausedSessionFile = null;
98
99
  pausedUnitType = null;
99
100
  pausedUnitId = null;
@@ -266,6 +267,7 @@ export class AutoSession {
266
267
  this.verificationRetryCount.clear();
267
268
  this.verificationRetryFailureHashes.clear();
268
269
  this.exhaustedVerificationUnits.clear();
270
+ this.zeroToolRetryCount.clear();
269
271
  this.pausedSessionFile = null;
270
272
  this.pausedUnitType = null;
271
273
  this.pausedUnitId = null;
@@ -1,7 +1,7 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Declarative auto-mode dispatch rules and dispatch resolver.
3
3
  import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
4
- import { isDbAvailable, getMilestoneSlices, getPendingGates, markAllGatesOmitted, getMilestone, insertAssessment, setSliceSketchFlag, transaction, getAssessment } from "./gsd-db.js";
4
+ import { isDbAvailable, getMilestoneSlices, getPendingGates, markAllGatesOmitted, getMilestone, insertAssessment, setSliceSketchFlag, transaction, getAssessment, } from "./gsd-db.js";
5
5
  import { isClosedStatus } from "./status-guards.js";
6
6
  import { extractVerdict, isAcceptableUatVerdict } from "./verdict-parser.js";
7
7
  import { gsdRoot, resolveGsdPathContract, resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, relTaskFile, relSliceFile, buildMilestoneFileName, buildSliceFileName, buildTaskFileName, gsdProjectionRoot, } from "./paths.js";
@@ -21,6 +21,7 @@ import { isAutoActive } from "./auto.js";
21
21
  import { markDepthVerified } from "./bootstrap/write-gate.js";
22
22
  import { ensureWorkflowPreferencesCaptured } from "./planning-depth.js";
23
23
  import { MILESTONE_ID_RE } from "./milestone-ids.js";
24
+ import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit, } from "./workflow-mcp.js";
24
25
  import { PROJECT_RESEARCH_INFLIGHT_MARKER, } from "./project-research-policy.js";
25
26
  import { isWorkflowPrefsCaptured, resolveDeepProjectSetupState, } from "./deep-project-setup-policy.js";
26
27
  import { annotateBackgroundable } from "./delegation-policy.js";
@@ -35,6 +36,7 @@ import { probeGitConflictState } from "./git-conflict-state.js";
35
36
  import { runTurnGitAction } from "./git-service.js";
36
37
  import { parseUnitId } from "./unit-id.js";
37
38
  import { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";
39
+ import { checkCloseoutConsistencyGate, formatCloseoutConsistencyBlock, } from "./closeout-consistency-gate.js";
38
40
  function resolveExistingExpectedArtifact(unitType, unitId, basePath) {
39
41
  const artifactPath = resolveExpectedArtifactPath(unitType, unitId, basePath);
40
42
  return artifactPath && existsSync(artifactPath) ? artifactPath : null;
@@ -466,11 +468,18 @@ export const DISPATCH_RULES = [
466
468
  },
467
469
  {
468
470
  name: "run-uat (post-completion)",
469
- match: async ({ state, mid, basePath, prefs }) => {
471
+ match: async ({ state, mid, basePath, prefs, sessionProvider, sessionAuthMode, activeTools, sessionBaseUrl }) => {
470
472
  const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
471
473
  if (!needsRunUat)
472
474
  return null;
473
475
  const { sliceId, uatType } = needsRunUat;
476
+ // Transport preflight: verify required MCP tools are actually connected
477
+ // before consuming a retry attempt. Fixes tool-starved sessions burning
478
+ // all MAX_UAT_ATTEMPTS before stopping (#477).
479
+ const transportError = getWorkflowTransportSupportError(sessionProvider, getRequiredWorkflowToolsForAutoUnit("run-uat"), { projectRoot: basePath, surface: "auto-mode", unitType: "run-uat", authMode: sessionAuthMode, baseUrl: sessionBaseUrl, activeTools });
480
+ if (transportError) {
481
+ return { action: "stop", reason: transportError, level: "warning" };
482
+ }
474
483
  // Cap run-uat dispatch attempts to prevent infinite replay (#3624).
475
484
  // Check before incrementing so an exhausted counter cannot create a
476
485
  // no-progress skip loop that starves later dispatch rules.
@@ -1355,6 +1364,16 @@ export const DISPATCH_RULES = [
1355
1364
  prompt: await buildCompleteMilestonePrompt(mid, midTitle, basePath),
1356
1365
  };
1357
1366
  }
1367
+ if (milestone) {
1368
+ const closeoutGate = checkCloseoutConsistencyGate(mid, { refreshFromDisk: true });
1369
+ if (!closeoutGate.ok) {
1370
+ return {
1371
+ action: "stop",
1372
+ reason: formatCloseoutConsistencyBlock(closeoutGate),
1373
+ level: "warning",
1374
+ };
1375
+ }
1376
+ }
1358
1377
  }
1359
1378
  return {
1360
1379
  action: "stop",
@@ -63,6 +63,32 @@ const TOOL_BASELINE = new WeakMap();
63
63
  export function clearToolBaseline(pi) {
64
64
  TOOL_BASELINE.delete(pi);
65
65
  }
66
+ /**
67
+ * Return the union of the pre-dispatch baseline tool set and the current live
68
+ * active tools, or just the live tools when no baseline has been recorded yet.
69
+ *
70
+ * Use this instead of `pi.getActiveTools()` anywhere you need the full tool
71
+ * surface for a preflight/routing check that runs BEFORE `selectAndApplyModel`
72
+ * restores the baseline — e.g. in `runDispatch` and `decideNextUnit`.
73
+ *
74
+ * The union is intentional:
75
+ * - Baseline covers tools that a prior unit's per-provider narrowing (hook
76
+ * overrides, Groq 128-tool cap, etc.) has removed from the live set.
77
+ * Those tools will be restored by `selectAndApplyModel` before dispatch, so
78
+ * dropping them from the preflight check would be a false negative.
79
+ * - Live set covers tools connected after the baseline was first captured
80
+ * (e.g. MCP servers attached mid-session or after a paused resume).
81
+ * Without the live merge, a stale baseline permanently hides newly
82
+ * connected MCP tools and prevents transport-preflight from clearing on
83
+ * resume (#477 follow-up).
84
+ */
85
+ export function getToolBaselineSnapshot(pi) {
86
+ const live = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
87
+ const baseline = TOOL_BASELINE.get(pi);
88
+ if (baseline === undefined)
89
+ return live;
90
+ return [...new Set([...baseline, ...live])];
91
+ }
66
92
  /**
67
93
  * Models eligible for the pre-dispatch policy gate. Prefer registry-available
68
94
  * models; when that list is empty (common after worktree resume before registry
@@ -31,6 +31,7 @@ import { hasBrowserRequiredText } from "./browser-evidence.js";
31
31
  import { debugLog } from "./debug-logger.js";
32
32
  import { buildSkillActivationBlock, buildSkillDiscoveryVars } from "./skill-activation.js";
33
33
  import { findMilestoneIds } from "./milestone-ids.js";
34
+ import { buildRunUatPresentationForType, RUN_UAT_TOOL_PRESENTATION_PLAN_ID } from "./tool-presentation-plan.js";
34
35
  export { buildSkillActivationBlock, buildSkillDiscoveryVars };
35
36
  // ─── Preamble Cap ─────────────────────────────────────────────────────────────
36
37
  /**
@@ -2939,6 +2940,7 @@ export async function buildRunUatPrompt(mid, sliceId, uatPath, uatContent, base)
2939
2940
  emitPromptContextTelemetry("run-uat", contextTelemetry, inlinedContext);
2940
2941
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "ASSESSMENT"));
2941
2942
  const uatType = resolveEffectiveUatType(uatContent);
2943
+ const canonicalPresentation = JSON.stringify(buildRunUatPresentationForType(uatType), null, 2);
2942
2944
  return loadPrompt("run-uat", {
2943
2945
  workingDirectory: base,
2944
2946
  milestoneId: mid,
@@ -2946,6 +2948,8 @@ export async function buildRunUatPrompt(mid, sliceId, uatPath, uatContent, base)
2946
2948
  uatPath,
2947
2949
  uatResultPath,
2948
2950
  uatType,
2951
+ toolPresentationPlanId: RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
2952
+ canonicalPresentation,
2949
2953
  inlinedContext,
2950
2954
  skillActivation: buildSkillActivationBlock({
2951
2955
  base,
@@ -32,6 +32,7 @@ import { isGsdWorktreePath } from "./worktree-root.js";
32
32
  import { resolveCanonicalMilestoneRoot } from "./worktree-manager.js";
33
33
  import { hasImplementationArtifacts } from "./milestone-implementation-evidence.js";
34
34
  import { loadAllCaptures, loadPendingCaptures } from "./captures.js";
35
+ import { checkCloseoutConsistencyGate } from "./closeout-consistency-gate.js";
35
36
  // Re-export so existing consumers of auto-recovery.ts keep working.
36
37
  export { resolveExpectedArtifactPath, diagnoseExpectedArtifact };
37
38
  export { classifyMilestoneSummaryContent, } from "./milestone-summary-classifier.js";
@@ -571,10 +572,8 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
571
572
  return false;
572
573
  const { milestone: mid } = parseUnitId(unitId);
573
574
  if (mid && isDbAvailable()) {
574
- const dbMilestone = getMilestone(mid);
575
- if (!dbMilestone)
576
- return false;
577
- if (!isClosedStatus(dbMilestone.status) && summaryOutcome !== "success")
575
+ const closeoutGate = checkCloseoutConsistencyGate(mid, { refreshFromDisk: true });
576
+ if (!closeoutGate.ok)
578
577
  return false;
579
578
  }
580
579
  if (hasImplementationArtifacts(base, mid) === "absent")
@@ -104,6 +104,14 @@ export function startUnitSupervision(sctx) {
104
104
  const softTimeoutMs = supervisionTimeouts.softTimeoutMs;
105
105
  const idleTimeoutMs = supervisionTimeouts.idleTimeoutMs;
106
106
  const hardTimeoutMs = supervisionTimeouts.hardTimeoutMs;
107
+ // A single hung tool gets its own short budget, NOT the general idle window:
108
+ // a long-but-progressing session is not idle, but a tool stuck for minutes
109
+ // is. Falls back to the idle window only if misconfigured to zero. The
110
+ // hung-tool budget is intentionally not scaled by task estimate — a stuck
111
+ // tool call is stuck regardless of how long the overall task should take.
112
+ const stalledToolTimeoutMs = (supervisor.stalled_tool_timeout_minutes ?? 0) > 0
113
+ ? supervisor.stalled_tool_timeout_minutes * 60 * 1000
114
+ : idleTimeoutMs;
107
115
  // ── 1. Soft timeout warning ──
108
116
  s.wrapupWarningHandle = setTimeout(() => {
109
117
  s.wrapupWarningHandle = null;
@@ -144,10 +152,12 @@ export function startUnitSupervision(sctx) {
144
152
  const runtime = readUnitRuntimeRecord(s.basePath, unitType, unitId);
145
153
  if (!runtime)
146
154
  return;
147
- if (Date.now() - runtime.lastProgressAt < idleTimeoutMs)
148
- return;
149
- // Agent has tool calls currently executing not idle, just waiting.
150
- // But only suppress recovery if the tool started recently.
155
+ // In-flight tool handling runs on its own dedicated hung-tool budget,
156
+ // independent of the general idle gate below, so a genuinely stuck tool
157
+ // is caught in minutes instead of waiting out the (typically much longer)
158
+ // idle window (#2527, follow-up). A tool actively executing within budget
159
+ // is real progress, so refreshing lastProgressAt here also keeps the idle
160
+ // gate from firing during legitimate long-running tool calls.
151
161
  let stalledToolDetected = false;
152
162
  if (getInFlightToolCount() > 0) {
153
163
  // User-interactive tools (ask_user_questions, secure_env_collect) block
@@ -161,21 +171,25 @@ export function startUnitSupervision(sctx) {
161
171
  }
162
172
  const oldestStart = getOldestInFlightToolStart();
163
173
  const toolAgeMs = Date.now() - oldestStart;
164
- if (toolAgeMs < idleTimeoutMs) {
174
+ if (toolAgeMs < stalledToolTimeoutMs) {
165
175
  writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
166
176
  lastProgressAt: Date.now(),
167
177
  lastProgressKind: "tool-in-flight",
168
178
  });
169
179
  return;
170
180
  }
171
- // Tool has been in-flight longer than idle timeout — treat as hung.
172
- // Clear the stale entries so subsequent ticks don't re-detect them,
173
- // and set the flag so the filesystem-activity check below does not
174
- // override the stall verdict (#2527).
181
+ // Tool has been in-flight longer than the hung-tool budget — treat as
182
+ // hung. Clear the stale entries so subsequent ticks don't re-detect
183
+ // them, and set the flag so the idle gate and filesystem-activity check
184
+ // below do not override the stall verdict (#2527).
175
185
  stalledToolDetected = true;
176
186
  clearInFlightTools();
177
- ctx.ui.notify(`Stalled tool detected: a tool has been in-flight for ${Math.round(toolAgeMs / 60000)}min. Treating as hung — attempting idle recovery.`, "warning");
187
+ ctx.ui.notify(`Stalled tool detected: a tool has been in-flight for ${Math.round(toolAgeMs / 60000)}min (budget ${Math.round(stalledToolTimeoutMs / 60000)}min). Treating as hung — attempting idle recovery.`, "warning");
178
188
  }
189
+ // No hung tool — apply the general idle gate. A unit that has made
190
+ // meaningful progress within the idle window is not idle yet.
191
+ if (!stalledToolDetected && Date.now() - runtime.lastProgressAt < idleTimeoutMs)
192
+ return;
179
193
  // Check if the agent is producing work on disk.
180
194
  // Skip this when a stalled tool was just detected — filesystem changes
181
195
  // from earlier in the task should not override the stall verdict (#2527).
@@ -1,57 +1,6 @@
1
1
  import { parseUnitId } from "./unit-id.js";
2
- import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "./tool-presentation-plan.js";
3
- export const RUN_UAT_BROWSER_TOOL_NAMES = [
4
- "browser_navigate",
5
- "browser_click",
6
- "browser_type",
7
- "browser_fill_form",
8
- "browser_click_ref",
9
- "browser_fill_ref",
10
- "browser_wait_for",
11
- "browser_assert",
12
- "browser_verify",
13
- "browser_screenshot",
14
- "browser_snapshot_refs",
15
- "browser_find",
16
- "browser_get_console_logs",
17
- "browser_get_network_logs",
18
- "browser_evaluate",
19
- "browser_reload",
20
- "browser_batch",
21
- "browser_act",
22
- ];
23
- export const AUTO_UNIT_SCOPED_TOOLS = {
24
- "research-milestone": ["gsd_summary_save", "gsd_decision_save"],
25
- "plan-milestone": ["gsd_plan_milestone", "gsd_decision_save", "gsd_requirement_update"],
26
- "discuss-milestone": [
27
- "gsd_summary_save",
28
- "gsd_decision_save",
29
- "gsd_requirement_save",
30
- "gsd_requirement_update",
31
- "gsd_plan_milestone",
32
- "gsd_milestone_generate_id",
33
- ],
34
- "discuss-slice": ["gsd_summary_save", "gsd_decision_save"],
35
- "validate-milestone": ["gsd_validate_milestone", "gsd_reassess_roadmap", "subagent"],
36
- "complete-milestone": ["gsd_complete_milestone", "subagent"],
37
- "research-slice": ["gsd_summary_save", "gsd_decision_save"],
38
- "plan-slice": ["gsd_plan_slice", "gsd_plan_task", "gsd_decision_save"],
39
- "refine-slice": ["gsd_plan_slice", "gsd_plan_task", "gsd_decision_save"],
40
- "replan-slice": ["gsd_replan_slice", "gsd_plan_task", "gsd_decision_save"],
41
- "complete-slice": ["gsd_slice_complete", "gsd_task_reopen", "gsd_replan_slice", "gsd_decision_save", "gsd_requirement_update", "subagent"],
42
- "reassess-roadmap": ["gsd_reassess_roadmap"],
43
- "execute-task": ["gsd_task_complete", "gsd_decision_save"],
44
- "execute-task-simple": ["gsd_task_complete", "gsd_decision_save"],
45
- "reactive-execute": ["gsd_task_complete", "gsd_decision_save"],
46
- "run-uat": [...RUN_UAT_WORKFLOW_TOOL_NAMES, "subagent", ...RUN_UAT_BROWSER_TOOL_NAMES],
47
- "gate-evaluate": ["gsd_save_gate_result"],
48
- "rewrite-docs": ["gsd_summary_save", "gsd_decision_save"],
49
- "workflow-preferences": ["gsd_summary_save"],
50
- "discuss-project": ["gsd_summary_save", "gsd_decision_save", "gsd_requirement_save"],
51
- "discuss-requirements": ["gsd_requirement_save", "gsd_summary_save"],
52
- "research-decision": ["gsd_summary_save"],
53
- "research-project": ["gsd_summary_save", "gsd_decision_save"],
54
- };
2
+ import { AUTO_UNIT_SCOPED_TOOLS, getForbiddenGsdToolReason, } from "./unit-tool-contracts.js";
3
+ export { AUTO_UNIT_SCOPED_TOOLS, RUN_UAT_BROWSER_TOOL_NAMES, } from "./unit-tool-contracts.js";
55
4
  const WORKFLOW_TOOL_ALIASES = {
56
5
  gsd_save_decision: "gsd_decision_save",
57
6
  gsd_update_requirement: "gsd_requirement_update",
@@ -88,6 +37,7 @@ const SCOPED_GSD_LIFECYCLE_TOOLS = new Set([
88
37
  ]
89
38
  .filter((tool) => tool.startsWith("gsd_"))
90
39
  .map(canonicalWorkflowToolName));
40
+ export const GSD_PHASE_SCOPE_DISPLAY_REASON = "This GSD phase only allows its scoped workflow tools.";
91
41
  function stripMcpToolPrefix(toolName) {
92
42
  if (!toolName.startsWith("mcp__"))
93
43
  return toolName;
@@ -103,11 +53,18 @@ export function isWorkflowAliasTool(toolName) {
103
53
  }
104
54
  function hardBlockReason(unitType, what) {
105
55
  return [
106
- `HARD BLOCK: unit "${unitType}" is constrained by auto-unit tool scope — ${what}.`,
56
+ `HARD BLOCK: Tool Contract failure for unit "${unitType}" — ${what}.`,
107
57
  "This is a mechanical phase-boundary gate. You MUST NOT proceed, retry the same call,",
108
58
  "or route around this block; the orchestrator owns phase transitions.",
109
59
  ].join(" ");
110
60
  }
61
+ function hardBlock(unitType, what) {
62
+ return {
63
+ block: true,
64
+ reason: hardBlockReason(unitType, what),
65
+ displayReason: GSD_PHASE_SCOPE_DISPLAY_REASON,
66
+ };
67
+ }
111
68
  function allowedGsdToolsForUnit(unitType) {
112
69
  return [...new Set((AUTO_UNIT_SCOPED_TOOLS[unitType] ?? [])
113
70
  .filter((tool) => tool.startsWith("gsd_"))
@@ -143,20 +100,14 @@ function shouldBlockTaskCompletionScope(unitType, unitId, toolName, input) {
143
100
  actualTask === expected.task) {
144
101
  return { block: false };
145
102
  }
146
- return {
147
- block: true,
148
- reason: hardBlockReason(unitType, `gsd_task_complete may only complete the active task ${expected.milestone}/${expected.slice}/${expected.task}; requested ${actualMilestone}/${actualSlice}/${actualTask}`),
149
- };
103
+ return hardBlock(unitType, `gsd_task_complete may only complete the active task ${expected.milestone}/${expected.slice}/${expected.task}; requested ${actualMilestone}/${actualSlice}/${actualTask}`);
150
104
  }
151
105
  export function shouldBlockAutoUnitToolCall(unitType, toolName, input, unitId) {
152
106
  const scopedTools = AUTO_UNIT_SCOPED_TOOLS[unitType];
153
107
  if (!scopedTools)
154
108
  return { block: false };
155
109
  if (isNativeWorkflowTool(toolName)) {
156
- return {
157
- block: true,
158
- reason: hardBlockReason(unitType, "native Workflow is not permitted inside a dispatched GSD auto-mode unit"),
159
- };
110
+ return hardBlock(unitType, "native Workflow is not permitted inside a dispatched GSD auto-mode unit");
160
111
  }
161
112
  const taskScope = shouldBlockTaskCompletionScope(unitType, unitId, toolName, input);
162
113
  if (taskScope.block)
@@ -167,8 +118,9 @@ export function shouldBlockAutoUnitToolCall(unitType, toolName, input, unitId) {
167
118
  const allowedTools = allowedGsdToolsForUnit(unitType);
168
119
  if (allowedTools.includes(canonicalTool))
169
120
  return { block: false };
170
- return {
171
- block: true,
172
- reason: hardBlockReason(unitType, `GSD lifecycle tool "${canonicalTool}" is not permitted; allowed GSD tools: ${allowedTools.length > 0 ? allowedTools.join(", ") : "(none)"}`),
173
- };
121
+ const forbiddenReason = getForbiddenGsdToolReason(unitType, canonicalTool);
122
+ if (forbiddenReason) {
123
+ return hardBlock(unitType, `GSD lifecycle tool "${canonicalTool}" is not permitted; ${forbiddenReason} Fix unit-tool-contracts.ts or the ${unitType} prompt.`);
124
+ }
125
+ return hardBlock(unitType, `GSD lifecycle tool "${canonicalTool}" is not permitted; allowed GSD tools: ${allowedTools.length > 0 ? allowedTools.join(", ") : "(none)"}`);
174
126
  }