@opengsd/gsd-pi 1.1.1-dev.75048e7 → 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 (149) 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 +10 -2
  10. package/dist/resources/extensions/gsd/auto-model-selection.js +26 -0
  11. package/dist/resources/extensions/gsd/auto-timers.js +24 -10
  12. package/dist/resources/extensions/gsd/auto.js +26 -4
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +29 -21
  14. package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
  15. package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -0
  16. package/dist/resources/extensions/gsd/commands-mcp-status.js +1 -1
  17. package/dist/resources/extensions/gsd/config-overlay.js +1 -0
  18. package/dist/resources/extensions/gsd/context-masker.js +129 -5
  19. package/dist/resources/extensions/gsd/guided-flow.js +4 -1
  20. package/dist/resources/extensions/gsd/planner-handoff.js +98 -0
  21. package/dist/resources/extensions/gsd/preferences-models.js +1 -0
  22. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  23. package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -2
  24. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  25. package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
  26. package/dist/resources/extensions/gsd/tool-contract.js +1 -1
  27. package/dist/resources/extensions/gsd/tool-presentation-plan.js +19 -2
  28. package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -1
  29. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +32 -4
  30. package/dist/resources/extensions/gsd/unit-tool-contracts.js +38 -14
  31. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -3
  32. package/dist/resources/extensions/gsd/worktree-manager.js +26 -0
  33. package/dist/resources/extensions/gsd/worktree-reentry.js +96 -0
  34. package/dist/resources/extensions/shared/gsd-browser-cli.js +6 -0
  35. package/dist/web/standalone/.next/BUILD_ID +1 -1
  36. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  37. package/dist/web/standalone/.next/build-manifest.json +2 -2
  38. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  39. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.html +1 -1
  56. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  63. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  64. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  66. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  67. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  68. package/package.json +1 -1
  69. package/packages/cloud-mcp-gateway/package.json +2 -2
  70. package/packages/contracts/package.json +1 -1
  71. package/packages/daemon/package.json +4 -4
  72. package/packages/gsd-agent-core/package.json +5 -5
  73. package/packages/gsd-agent-modes/package.json +7 -7
  74. package/packages/mcp-server/package.json +3 -3
  75. package/packages/native/package.json +1 -1
  76. package/packages/pi-agent-core/package.json +1 -1
  77. package/packages/pi-ai/dist/models.generated.d.ts +158 -2
  78. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  79. package/packages/pi-ai/dist/models.generated.js +149 -9
  80. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  81. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  82. package/packages/pi-ai/dist/providers/transform-messages.js +8 -1
  83. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  84. package/packages/pi-ai/package.json +1 -1
  85. package/packages/pi-coding-agent/package.json +7 -7
  86. package/packages/pi-tui/package.json +1 -1
  87. package/packages/rpc-client/package.json +2 -2
  88. package/pkg/package.json +1 -1
  89. package/scripts/install/handoff.js +16 -3
  90. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +21 -2
  91. package/src/resources/extensions/browser-tools/engine/selection.ts +1 -1
  92. package/src/resources/extensions/browser-tools/extension-manifest.json +1 -1
  93. package/src/resources/extensions/browser-tools/index.ts +36 -5
  94. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +2 -2
  95. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +37 -0
  96. package/src/resources/extensions/browser-tools/tests/web-app-detect.test.mjs +68 -0
  97. package/src/resources/extensions/browser-tools/web-app-detect.ts +63 -0
  98. package/src/resources/extensions/gsd/auto/phases.ts +48 -6
  99. package/src/resources/extensions/gsd/auto/session.ts +2 -0
  100. package/src/resources/extensions/gsd/auto-dispatch.ts +34 -2
  101. package/src/resources/extensions/gsd/auto-model-selection.ts +26 -0
  102. package/src/resources/extensions/gsd/auto-timers.ts +25 -9
  103. package/src/resources/extensions/gsd/auto.ts +28 -4
  104. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +40 -21
  105. package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
  106. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -0
  107. package/src/resources/extensions/gsd/commands-mcp-status.ts +1 -1
  108. package/src/resources/extensions/gsd/config-overlay.ts +1 -0
  109. package/src/resources/extensions/gsd/context-masker.ts +152 -5
  110. package/src/resources/extensions/gsd/guided-flow.ts +4 -1
  111. package/src/resources/extensions/gsd/planner-handoff.ts +149 -0
  112. package/src/resources/extensions/gsd/preferences-models.ts +1 -0
  113. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  114. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  115. package/src/resources/extensions/gsd/prompts/run-uat.md +2 -2
  116. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  117. package/src/resources/extensions/gsd/skill-manifest.ts +12 -0
  118. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +99 -0
  119. package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +66 -4
  120. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +4 -0
  121. package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -0
  122. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +118 -0
  123. package/src/resources/extensions/gsd/tests/context-masker.test.ts +56 -1
  124. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +1 -0
  125. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +24 -0
  126. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +1 -1
  127. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +27 -0
  128. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -0
  129. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +7 -1
  130. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +1 -1
  131. package/src/resources/extensions/gsd/tests/planner-handoff.test.ts +100 -0
  132. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +113 -1
  133. package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
  134. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +20 -0
  135. package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
  136. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
  137. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +131 -2
  138. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
  139. package/src/resources/extensions/gsd/tool-contract.ts +1 -1
  140. package/src/resources/extensions/gsd/tool-presentation-plan.ts +21 -2
  141. package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -1
  142. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +46 -4
  143. package/src/resources/extensions/gsd/unit-tool-contracts.ts +38 -14
  144. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -3
  145. package/src/resources/extensions/gsd/worktree-manager.ts +32 -0
  146. package/src/resources/extensions/gsd/worktree-reentry.ts +103 -0
  147. package/src/resources/extensions/shared/gsd-browser-cli.ts +6 -0
  148. /package/dist/web/standalone/.next/static/{h4TGni4xJzlZjGkxaT6uU → zzYMrKpPGfRQRxSFO32Jr}/_buildManifest.js +0 -0
  149. /package/dist/web/standalone/.next/static/{h4TGni4xJzlZjGkxaT6uU → zzYMrKpPGfRQRxSFO32Jr}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- 5403bc93a34728ed
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";
@@ -467,11 +468,18 @@ export const DISPATCH_RULES = [
467
468
  },
468
469
  {
469
470
  name: "run-uat (post-completion)",
470
- match: async ({ state, mid, basePath, prefs }) => {
471
+ match: async ({ state, mid, basePath, prefs, sessionProvider, sessionAuthMode, activeTools, sessionBaseUrl }) => {
471
472
  const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
472
473
  if (!needsRunUat)
473
474
  return null;
474
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
+ }
475
483
  // Cap run-uat dispatch attempts to prevent infinite replay (#3624).
476
484
  // Check before incrementing so an exhausted counter cannot create a
477
485
  // no-progress skip loop that starves later dispatch rules.
@@ -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
@@ -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).
@@ -30,7 +30,7 @@ import { playNotificationBell, sendDesktopNotification } from "./notifications.j
30
30
  import { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction, resolveCompactionThresholdPercent, shouldRerootStepSessionForContext, } from "./auto-budget.js";
31
31
  import { markToolStart as _markToolStart, markToolEnd as _markToolEnd, getOldestInFlightToolAgeMs as _getOldestInFlightToolAgeMs, clearInFlightTools, isToolInvocationError, isQueuedUserMessageSkip, isDeterministicPolicyError, } from "./auto-tool-tracking.js";
32
32
  import { closeoutUnit } from "./auto-unit-closeout.js";
33
- import { selectAndApplyModel, resolveModelId, clearToolBaseline } from "./auto-model-selection.js";
33
+ import { selectAndApplyModel, resolveModelId, clearToolBaseline, getToolBaselineSnapshot } from "./auto-model-selection.js";
34
34
  import { resetRoutingHistory, recordOutcome } from "./routing-history.js";
35
35
  import { resetHookState, runPreDispatchHooks, restoreHookState, clearPersistedHookState, } from "./post-unit-hooks.js";
36
36
  import { runGSDDoctor, rebuildState } from "./doctor.js";
@@ -287,8 +287,24 @@ export function _synthesizePausedSessionRecoveryForTest(basePath, unitType, unit
287
287
  function handlePausedSessionResumeRecovery(basePath, state, notify) {
288
288
  if (!state.pausedSessionFile)
289
289
  return { skippedReplay: false };
290
- const pausedRecoveryUnitType = state.currentUnit?.type ?? state.pausedUnitType ?? "unknown";
291
- const pausedRecoveryUnitId = state.currentUnit?.id ?? state.pausedUnitId ?? "unknown";
290
+ const pausedRecoveryUnitType = state.currentUnit?.type ?? state.pausedUnitType ?? null;
291
+ const pausedRecoveryUnitId = state.currentUnit?.id ?? state.pausedUnitId ?? null;
292
+ // When the paused-session metadata never captured the unit identity (the
293
+ // pause happened between units, or the worker died before currentUnit was
294
+ // set), we have nothing to verify against and nothing correct to target. A
295
+ // replay synthesized with an "unknown" unit re-injects an unbounded,
296
+ // mis-identified tool-call blob into the fresh resume context — exactly the
297
+ // thrash that turns one stuck unit into several. Disk state has already been
298
+ // rebuilt (rebuildState + doctor) before this runs, so skip the replay and
299
+ // let the normal dispatcher recompute the next unit from disk.
300
+ if (!pausedRecoveryUnitType || !pausedRecoveryUnitId) {
301
+ state.pausedSessionFile = null;
302
+ state.pausedUnitType = null;
303
+ state.pausedUnitId = null;
304
+ state.pendingCrashRecovery = null;
305
+ notify("Paused session had no recorded unit identity. Skipping tool-call replay and resuming from disk state.");
306
+ return { skippedReplay: true };
307
+ }
292
308
  const completedPausedUnit = verifyExpectedArtifact(pausedRecoveryUnitType, pausedRecoveryUnitId, basePath);
293
309
  if (completedPausedUnit) {
294
310
  state.pausedSessionFile = null;
@@ -1637,7 +1653,10 @@ export function createWiredDispatchAdapter(ctx, pi, dispatchBasePath, session) {
1637
1653
  const authMode = sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
1638
1654
  ? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
1639
1655
  : undefined;
1640
- const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
1656
+ // Use baseline snapshot same reason as phases.ts:runDispatch: the live
1657
+ // active set may be narrowed by the prior unit before selectAndApplyModel
1658
+ // restores it, causing false transport-preflight failures (#477 follow-up).
1659
+ const activeTools = getToolBaselineSnapshot(pi);
1641
1660
  // Mirrors runDispatch: deep-planning keeps approval gates in plain chat
1642
1661
  // because structured questions can be cancelled outside the chat turn on
1643
1662
  // some transports.
@@ -1678,6 +1697,9 @@ export function createWiredDispatchAdapter(ctx, pi, dispatchBasePath, session) {
1678
1697
  sessionContextWindow,
1679
1698
  sessionProvider,
1680
1699
  modelRegistry,
1700
+ activeTools,
1701
+ sessionAuthMode: authMode,
1702
+ sessionBaseUrl: ctx.model?.baseUrl,
1681
1703
  });
1682
1704
  if (action.action === "stop") {
1683
1705
  if (session)
@@ -210,7 +210,12 @@ export function buildRunUatGsdToolSet(activeToolNames, registeredToolNames = act
210
210
  "subagent",
211
211
  ...RUN_UAT_BROWSER_TOOL_NAMES,
212
212
  ]);
213
- return [...new Set(scoped)];
213
+ const resolved = [...new Set(scoped)];
214
+ const unresolved = RUN_UAT_WORKFLOW_TOOL_NAMES.filter((tool) => !resolved.some((name) => name === tool || (name.startsWith("mcp__") && name.endsWith(`__${tool}`))));
215
+ if (unresolved.length > 0) {
216
+ safetyLogWarning("bootstrap", `buildRunUatGsdToolSet: required run-uat workflow tool(s) not found in active/registered surface: ${unresolved.join(", ")}. Session may lack gsd-workflow MCP connection.`);
217
+ }
218
+ return resolved;
214
219
  }
215
220
  export function buildMinimalGsdWorkflowToolSet(activeToolNames, registeredToolNames = activeToolNames) {
216
221
  const autoBaseTools = new Set(MINIMAL_AUTO_BASE_TOOL_NAMES);
@@ -463,6 +468,18 @@ export function registerHooks(pi, ecosystemHandlers) {
463
468
  if (isAutoActive() || preserveCloseoutSurface) {
464
469
  ctx.ui.setWidget("gsd-health", undefined);
465
470
  }
471
+ // Cold start after /quit relaunches with cwd at the project root. When
472
+ // auto-mode is neither active nor paused (its own resume path re-enters the
473
+ // worktree with a lease check — auto.ts:3032), proactively chdir back into
474
+ // the active milestone's worktree so subsequent work isn't stranded at the
475
+ // root. Best-effort and a no-op when already inside a worktree.
476
+ if (!isAutoActive() && !isAutoPaused() && !preserveCloseoutSurface) {
477
+ try {
478
+ const { reenterActiveWorktreeIfNeeded } = await import("../worktree-reentry.js");
479
+ await reenterActiveWorktreeIfNeeded(basePath);
480
+ }
481
+ catch { /* non-fatal */ }
482
+ }
466
483
  });
467
484
  pi.on("session_switch", async (_event, ctx) => {
468
485
  const basePath = contextBasePath(ctx);
@@ -1084,16 +1101,19 @@ export function registerHooks(pi, ecosystemHandlers) {
1084
1101
  if (isAutoActive()) {
1085
1102
  try {
1086
1103
  const { loadEffectiveGSDPreferences } = await import("../preferences.js");
1104
+ const { createObservationMask, createResponsesInputObservationMask, truncateContextResultMessages, truncateResponsesInputResultItems, } = await import("../context-masker.js");
1087
1105
  const prefs = loadEffectiveGSDPreferences();
1088
1106
  const cmConfig = prefs?.preferences.context_management;
1089
1107
  // Observation masking: replace old tool results with placeholders
1090
1108
  if (cmConfig?.observation_masking !== false) {
1091
1109
  const keepTurns = cmConfig?.observation_mask_turns ?? 8;
1092
- const { createObservationMask } = await import("../context-masker.js");
1093
- const mask = createObservationMask(keepTurns);
1094
1110
  const messages = payload.messages;
1095
1111
  if (Array.isArray(messages)) {
1096
- payload.messages = mask(messages);
1112
+ payload.messages = createObservationMask(keepTurns)(messages);
1113
+ }
1114
+ const input = payload.input;
1115
+ if (Array.isArray(input)) {
1116
+ payload.input = createResponsesInputObservationMask(keepTurns)(input);
1097
1117
  }
1098
1118
  }
1099
1119
  // Tool result truncation: cap individual tool result content length.
@@ -1102,23 +1122,11 @@ export function registerHooks(pi, ecosystemHandlers) {
1102
1122
  const maxChars = cmConfig?.tool_result_max_chars ?? 800;
1103
1123
  const msgs = payload.messages;
1104
1124
  if (Array.isArray(msgs)) {
1105
- payload.messages = msgs.map((msg) => {
1106
- // Match toolResult messages (role: "toolResult", content is array of content blocks)
1107
- if (msg?.role === "toolResult" && Array.isArray(msg.content)) {
1108
- const blocks = msg.content;
1109
- const totalLen = blocks.reduce((sum, b) => sum + (typeof b.text === "string" ? b.text.length : 0), 0);
1110
- if (totalLen > maxChars) {
1111
- const truncated = blocks.map(b => {
1112
- if (typeof b.text === "string" && b.text.length > maxChars) {
1113
- return { ...b, text: b.text.slice(0, maxChars) + "\n…[truncated]" };
1114
- }
1115
- return b;
1116
- });
1117
- return { ...msg, content: truncated };
1118
- }
1119
- }
1120
- return msg;
1121
- });
1125
+ payload.messages = truncateContextResultMessages(msgs, maxChars);
1126
+ }
1127
+ const input = payload.input;
1128
+ if (Array.isArray(input)) {
1129
+ payload.input = truncateResponsesInputResultItems(input, maxChars);
1122
1130
  }
1123
1131
  }
1124
1132
  catch { /* non-fatal */ }
@@ -56,7 +56,7 @@ export const BUNDLED_SKILL_TRIGGERS = [
56
56
  { trigger: "Core Web Vitals — fix LCP, CLS, INP; layout shifts; page experience optimization", skill: "core-web-vitals" },
57
57
  { trigger: "GitHub Actions CI/CD — write, run, and debug workflow files; live syntax and run monitoring", skill: "github-workflows" },
58
58
  { trigger: "Comprehensive web quality audit — performance, accessibility, SEO, and best-practices (Lighthouse-style)", skill: "web-quality-audit" },
59
- { trigger: "gsd-browser UAT default browser MCP/CLI for real UI verification, screenshots, assertions, console/network diagnostics", skill: "gsd-browser" },
59
+ { trigger: "gsd-browser opt-in and External MCP UAT screenshots, assertions, console/network diagnostics", skill: "gsd-browser" },
60
60
  { trigger: "Browser automation — open sites, fill forms, click, screenshot, scrape, or test web apps programmatically", skill: "agent-browser" },
61
61
  { trigger: "Review UI code for Web Interface Guidelines compliance — UX, design, and accessibility patterns", skill: "web-design-guidelines" },
62
62
  { trigger: "UI/UX patterns reference — animations, CSS, typography, prefetching, icons (file:line findings)", skill: "userinterface-wiki" },
@@ -198,6 +198,16 @@ export async function handleAutoCommand(trimmed, ctx, pi) {
198
198
  if (!(await guardRemoteSession(ctx, pi)))
199
199
  return true;
200
200
  const basePath = projectRoot();
201
+ // Cold start after /quit lands at the project root, not the worktree. If the
202
+ // active milestone has a live worktree, chdir back into it now so the agent
203
+ // doesn't have to search for it. Best-effort; resolves to a no-op otherwise.
204
+ try {
205
+ const { reenterActiveWorktreeIfNeeded } = await import("../../worktree-reentry.js");
206
+ await reenterActiveWorktreeIfNeeded(basePath, {
207
+ notify: (message) => ctx.ui.notify(message, "info"),
208
+ });
209
+ }
210
+ catch { /* non-fatal */ }
201
211
  const { hasGsdBootstrapArtifacts } = await import("../../detection.js");
202
212
  const { gsdRoot } = await import("../../paths.js");
203
213
  if (!hasGsdBootstrapArtifacts(gsdRoot(basePath))) {
@@ -34,7 +34,7 @@ export function formatMcpInitResult(status, configPath, targetPath) {
34
34
  `Config: ${configPath}`,
35
35
  "",
36
36
  "MCP-capable clients can now load the GSD workflow and gsd-browser MCP servers from this folder.",
37
- "Pi Providers use the managed gsd-browser engine directly; this project config is for External MCP Clients.",
37
+ "Pi Providers use built-in browser tools directly; this project config is for External MCP Clients.",
38
38
  "Restart or reconnect any client that already has this project open.",
39
39
  ].join("\n");
40
40
  }
@@ -122,6 +122,7 @@ function collectConfigSections() {
122
122
  supRows.push({ label: "Model", value: sup.model });
123
123
  supRows.push({ label: "Soft timeout", value: `${sup.soft_timeout_minutes}m` });
124
124
  supRows.push({ label: "Idle timeout", value: `${sup.idle_timeout_minutes}m` });
125
+ supRows.push({ label: "Stalled tool timeout", value: `${sup.stalled_tool_timeout_minutes}m` });
125
126
  supRows.push({ label: "Hard timeout", value: `${sup.hard_timeout_minutes}m` });
126
127
  sections.push({ title: "Auto Supervisor", rows: supRows });
127
128
  }