@opengsd/gsd-pi 1.2.0-dev.5457a158 → 1.2.0-dev.84c56d87

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 (135) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +209 -88
  3. package/dist/resources/extensions/browser-tools/engine/selection.js +73 -5
  4. package/dist/resources/extensions/browser-tools/index.js +69 -12
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +3 -2
  6. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +19 -0
  7. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +27 -9
  8. package/dist/resources/extensions/gsd/browser-evidence.js +8 -2
  9. package/dist/resources/extensions/gsd/mcp-filter.js +2 -19
  10. package/dist/resources/extensions/gsd/uat-policy.js +2 -1
  11. package/dist/resources/extensions/gsd/unit-registry.js +7 -20
  12. package/dist/resources/extensions/gsd/web-app-uat.js +45 -8
  13. package/dist/resources/extensions/search-the-web/native-search.js +5 -3
  14. package/dist/resources/extensions/shared/browser-contract.js +59 -0
  15. package/dist/resources/extensions/shared/gsd-browser-cli.js +72 -4
  16. package/dist/resources/skills/create-skill/references/executable-code.md +1 -1
  17. package/dist/resources/skills/create-skill/workflows/add-reference.md +8 -3
  18. package/dist/resources/skills/create-skill/workflows/add-script.md +4 -2
  19. package/dist/resources/skills/create-skill/workflows/add-template.md +3 -1
  20. package/dist/resources/skills/create-skill/workflows/add-workflow.md +8 -3
  21. package/dist/resources/skills/create-skill/workflows/upgrade-to-router.md +10 -5
  22. package/dist/resources/skills/create-skill/workflows/verify-skill.md +9 -4
  23. package/dist/resources/skills/spike-wrap-up/SKILL.md +9 -9
  24. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  25. package/dist/web/standalone/.next/BUILD_ID +1 -1
  26. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  27. package/dist/web/standalone/.next/build-manifest.json +2 -2
  28. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  29. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.html +1 -1
  46. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  53. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  54. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  56. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  57. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  58. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  59. package/dist/web/standalone/node_modules/postcss/lib/container.js +18 -26
  60. package/dist/web/standalone/node_modules/postcss/lib/css-syntax-error.js +14 -47
  61. package/dist/web/standalone/node_modules/postcss/lib/declaration.js +4 -4
  62. package/dist/web/standalone/node_modules/postcss/lib/fromJSON.js +3 -3
  63. package/dist/web/standalone/node_modules/postcss/lib/input.js +29 -54
  64. package/dist/web/standalone/node_modules/postcss/lib/lazy-result.js +37 -47
  65. package/dist/web/standalone/node_modules/postcss/lib/map-generator.js +9 -26
  66. package/dist/web/standalone/node_modules/postcss/lib/no-work-result.js +55 -57
  67. package/dist/web/standalone/node_modules/postcss/lib/node.js +31 -99
  68. package/dist/web/standalone/node_modules/postcss/lib/parse.js +1 -1
  69. package/dist/web/standalone/node_modules/postcss/lib/parser.js +9 -10
  70. package/dist/web/standalone/node_modules/postcss/lib/postcss.js +12 -12
  71. package/dist/web/standalone/node_modules/postcss/lib/previous-map.js +11 -30
  72. package/dist/web/standalone/node_modules/postcss/lib/processor.js +7 -7
  73. package/dist/web/standalone/node_modules/postcss/lib/result.js +5 -5
  74. package/dist/web/standalone/node_modules/postcss/lib/rule.js +6 -6
  75. package/dist/web/standalone/node_modules/postcss/lib/stringifier.js +28 -69
  76. package/dist/web/standalone/node_modules/postcss/lib/tokenize.js +2 -6
  77. package/dist/web/standalone/node_modules/postcss/package.json +48 -48
  78. package/dist/web/standalone/package.json +1 -1
  79. package/package.json +1 -1
  80. package/packages/cloud-mcp-gateway/package.json +2 -2
  81. package/packages/contracts/package.json +1 -1
  82. package/packages/daemon/package.json +4 -4
  83. package/packages/gsd-agent-core/package.json +5 -5
  84. package/packages/gsd-agent-modes/package.json +7 -7
  85. package/packages/mcp-server/package.json +3 -3
  86. package/packages/native/package.json +1 -1
  87. package/packages/pi-agent-core/package.json +1 -1
  88. package/packages/pi-ai/dist/models.generated.d.ts +66 -178
  89. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  90. package/packages/pi-ai/dist/models.generated.js +116 -204
  91. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  92. package/packages/pi-ai/package.json +1 -1
  93. package/packages/pi-coding-agent/package.json +7 -7
  94. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  95. package/packages/pi-tui/dist/tui.js +9 -0
  96. package/packages/pi-tui/dist/tui.js.map +1 -1
  97. package/packages/pi-tui/package.json +2 -2
  98. package/packages/rpc-client/package.json +2 -2
  99. package/pkg/package.json +1 -1
  100. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +265 -98
  101. package/src/resources/extensions/browser-tools/engine/selection.ts +90 -4
  102. package/src/resources/extensions/browser-tools/index.ts +71 -13
  103. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +83 -13
  104. package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +136 -0
  105. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +3 -2
  106. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -0
  107. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +48 -4
  108. package/src/resources/extensions/gsd/browser-evidence.ts +18 -2
  109. package/src/resources/extensions/gsd/mcp-filter.ts +2 -23
  110. package/src/resources/extensions/gsd/tests/browser-automation-contract-fixture.ts +39 -0
  111. package/src/resources/extensions/gsd/tests/browser-contract.test.ts +44 -0
  112. package/src/resources/extensions/gsd/tests/dispatch-run-uat-browser-tools.test.ts +2 -1
  113. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +35 -1
  114. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +7 -11
  115. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +20 -58
  116. package/src/resources/extensions/gsd/tests/integration/gsd-integration-fixture.ts +80 -0
  117. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +35 -0
  118. package/src/resources/extensions/gsd/tests/uat-policy.test.ts +24 -29
  119. package/src/resources/extensions/gsd/tests/web-app-uat.test.ts +44 -1
  120. package/src/resources/extensions/gsd/uat-policy.ts +2 -1
  121. package/src/resources/extensions/gsd/unit-registry.ts +7 -20
  122. package/src/resources/extensions/gsd/web-app-uat.ts +51 -8
  123. package/src/resources/extensions/search-the-web/native-search.ts +5 -3
  124. package/src/resources/extensions/shared/browser-contract.ts +66 -0
  125. package/src/resources/extensions/shared/gsd-browser-cli.ts +88 -4
  126. package/src/resources/skills/create-skill/references/executable-code.md +1 -1
  127. package/src/resources/skills/create-skill/workflows/add-reference.md +8 -3
  128. package/src/resources/skills/create-skill/workflows/add-script.md +4 -2
  129. package/src/resources/skills/create-skill/workflows/add-template.md +3 -1
  130. package/src/resources/skills/create-skill/workflows/add-workflow.md +8 -3
  131. package/src/resources/skills/create-skill/workflows/upgrade-to-router.md +10 -5
  132. package/src/resources/skills/create-skill/workflows/verify-skill.md +9 -4
  133. package/src/resources/skills/spike-wrap-up/SKILL.md +9 -9
  134. /package/dist/web/standalone/.next/static/{2p9Rv9pQflAxCBbGVI2vb → AOpDeK_gJHU8OZjRo31gQ}/_buildManifest.js +0 -0
  135. /package/dist/web/standalone/.next/static/{2p9Rv9pQflAxCBbGVI2vb → AOpDeK_gJHU8OZjRo31gQ}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
1
  /** browser-tools — Pi Browser Automation Contract adapter. */
2
2
  import { importExtensionModule } from "@gsd/pi-coding-agent";
3
3
  import { closeManagedGsdBrowser, registerManagedGsdBrowserTools, warmUpManagedGsdBrowser } from "./engine/managed-gsd-browser.js";
4
- import { resolveBrowserEngineMode } from "./engine/selection.js";
4
+ import { commitBrowserEngineResolution, resolveAmbientBrowserEngineResolution } from "./engine/selection.js";
5
5
  import { setArtifactRootForCwd } from "./state.js";
6
6
  import { detectWebApp } from "./web-app-detect.js";
7
7
  let legacyRegistrationPromise = null;
@@ -130,12 +130,56 @@ function withBrowserArtifactCwdScope(pi) {
130
130
  },
131
131
  };
132
132
  }
133
- async function registerBrowserTools(pi) {
134
- const engine = resolveBrowserEngineMode();
133
+ /** Daemon-connect budget when the probe-resolved managed engine is verified at session start. */
134
+ const PROBE_WARMUP_TIMEOUT_MS = 10_000;
135
+ async function registerBrowserTools(pi, ctx) {
136
+ const projectRoot = ctx.cwd || process.cwd();
137
+ const resolution = resolveAmbientBrowserEngineResolution(projectRoot);
138
+ let engine = resolution.engine;
135
139
  if (engine === "off")
136
140
  return;
141
+ // A probe-resolved managed engine is only a prediction that gsd-browser
142
+ // works — prove it by connecting the daemon before committing the session's
143
+ // tool registrations to it. Connect failure falls back to legacy Playwright
144
+ // (the failure mode that made ADR-024 freeze the old default) and commits
145
+ // the outcome so ambient readers see the engine actually in use. When eager
146
+ // warm-up is disabled the daemon-connect proof cannot run, so the probe
147
+ // default treats the managed engine as unprovable and falls back to legacy
148
+ // rather than registering it unverified. An explicit
149
+ // GSD_BROWSER_ENGINE=gsd-browser override skips the gate and is honored
150
+ // verbatim, matching prior behavior.
151
+ if (engine === "gsd-browser" && resolution.source === "probe" && !registeredEngine) {
152
+ if (isWarmUpDisabled()) {
153
+ engine = commitLegacyFallback(projectRoot, "warm-up disabled; managed engine unverifiable; using legacy Playwright");
154
+ }
155
+ else {
156
+ const warmUp = await warmUpManagedGsdBrowser(ctx, AbortSignal.timeout(PROBE_WARMUP_TIMEOUT_MS));
157
+ if (!warmUp.ok) {
158
+ engine = commitLegacyFallback(projectRoot, `gsd-browser daemon connect failed (${warmUp.error}); using legacy Playwright`);
159
+ if (ctx.hasUI) {
160
+ ctx.ui.notify(`gsd-browser engine unavailable (${warmUp.error}); using Playwright browser tools for this session.`, "warning");
161
+ }
162
+ }
163
+ else if (warmUp.coverageWarning && ctx.hasUI) {
164
+ ctx.ui.notify(warmUp.coverageWarning, "warning");
165
+ }
166
+ }
167
+ }
168
+ // Browser tool registrations are process-global and cannot be swapped once
169
+ // live. When an earlier session in this process already registered an engine
170
+ // and this project resolved a different one (per-project probe resolution can
171
+ // diverge across projects in a multi-session process), adopt the registered
172
+ // engine rather than throwing — a throw surfaces as "browser-tools failed to
173
+ // load" and leaves this session with no browser tools at all. Commit the
174
+ // adoption so ambient readers (UAT guidance, warm-up) describe the engine
175
+ // actually in use.
137
176
  if (registeredEngine && registeredEngine !== engine) {
138
- throw new Error(`Browser tools already registered with GSD_BROWSER_ENGINE=${registeredEngine}. Restart GSD before switching to ${engine}.`);
177
+ engine = registeredEngine;
178
+ commitBrowserEngineResolution(projectRoot, {
179
+ engine,
180
+ source: "probe",
181
+ reason: `browser tools already registered with ${engine} earlier in this process; adopting it`,
182
+ });
139
183
  }
140
184
  let registration;
141
185
  if (engine === "legacy") {
@@ -165,27 +209,40 @@ async function registerBrowserTools(pi) {
165
209
  throw error;
166
210
  }
167
211
  }
212
+ function commitLegacyFallback(projectRoot, reason) {
213
+ commitBrowserEngineResolution(projectRoot, { engine: "legacy", source: "probe", reason });
214
+ return "legacy";
215
+ }
168
216
  function isWarmUpDisabled() {
169
217
  const value = process.env.GSD_BROWSER_WARMUP?.trim().toLowerCase();
170
218
  return value === "0" || value === "false" || value === "off";
171
219
  }
172
220
  /**
173
- * Auto-initialize the managed gsd-browser engine only when explicitly selected
174
- * for a web app. Best-effort and non-blocking: warm-up runs in the background
175
- * and only surfaces a warning if it fails.
221
+ * Auto-initialize the managed gsd-browser engine when it was selected via the
222
+ * explicit GSD_BROWSER_ENGINE override, which registers without the
223
+ * daemon-connect gate. Best-effort and non-blocking: warm-up runs in the
224
+ * background and only surfaces a warning if it fails. Probe-resolved sessions
225
+ * already connected (or fell back) during registration, so they are excluded
226
+ * to avoid re-warming and double-notifying.
176
227
  */
177
228
  function maybeWarmUpManagedEngine(pi, ctx) {
178
229
  if (isWarmUpDisabled())
179
230
  return;
180
- if (resolveBrowserEngineMode() !== "gsd-browser")
181
- return;
182
231
  const projectRoot = ctx.cwd || process.cwd();
232
+ const resolution = resolveAmbientBrowserEngineResolution(projectRoot);
233
+ if (resolution.engine !== "gsd-browser" || resolution.source !== "env")
234
+ return;
183
235
  if (!detectWebApp(projectRoot))
184
236
  return;
185
237
  void warmUpManagedGsdBrowser(ctx).then((result) => {
186
- if (!result.ok && ctx.hasUI) {
238
+ if (!ctx.hasUI)
239
+ return;
240
+ if (!result.ok) {
187
241
  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");
188
242
  }
243
+ else if (result.coverageWarning) {
244
+ ctx.ui.notify(result.coverageWarning, "warning");
245
+ }
189
246
  });
190
247
  }
191
248
  async function closeActiveBrowserEngines() {
@@ -198,14 +255,14 @@ async function closeActiveBrowserEngines() {
198
255
  export default function (pi) {
199
256
  pi.on("session_start", async (_event, ctx) => {
200
257
  if (ctx.hasUI) {
201
- void registerBrowserTools(pi)
258
+ void registerBrowserTools(pi, ctx)
202
259
  .then(() => maybeWarmUpManagedEngine(pi, ctx))
203
260
  .catch((error) => {
204
261
  ctx.ui.notify(`browser-tools failed to load: ${error instanceof Error ? error.message : String(error)}`, "warning");
205
262
  });
206
263
  return;
207
264
  }
208
- await registerBrowserTools(pi);
265
+ await registerBrowserTools(pi, ctx);
209
266
  maybeWarmUpManagedEngine(pi, ctx);
210
267
  });
211
268
  pi.on("session_shutdown", async () => {
@@ -25,6 +25,7 @@ import { markInteractiveElicitationStart, markInteractiveElicitationEnd, } from
25
25
  import { discoverBrowserMcpServerName, discoverMcpServers, discoverMcpServerNames, discoverUserMcpServerNames, discoverWorkflowMcpServerName, computeMcpDisallowedTools, } from "../gsd/mcp-filter.js";
26
26
  import { RUN_UAT_CLAUDE_NATIVE_TOOL_NAMES, RUN_UAT_FORBIDDEN_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES, resolveToolPresentationPlan } from "../gsd/tool-presentation-plan.js";
27
27
  import { getToolSurfaceReadinessError } from "../gsd/tool-surface-readiness.js";
28
+ import { hasBrowserContractPrefix } from "../shared/browser-contract.js";
28
29
  import { showInterviewRound } from "../shared/tui.js";
29
30
  export { buildFinalAssistantContent, extractToolResultsFromSdkUserMessage, handleClaudeCodePartialStreamEvent, mergePendingToolCalls, } from "./turn-assembler.js";
30
31
  export function serverToolUseToToolCallLike(block) {
@@ -1290,7 +1291,7 @@ function browserMcpServerNameFromAllowedTools(allowedTools) {
1290
1291
  const parsed = parseAllowedMcpToolName(toolName);
1291
1292
  if (!parsed)
1292
1293
  continue;
1293
- if (parsed.server === "gsd-browser" || parsed.tool.startsWith("browser_")) {
1294
+ if (parsed.server === "gsd-browser" || hasBrowserContractPrefix(parsed.tool)) {
1294
1295
  return parsed.server;
1295
1296
  }
1296
1297
  }
@@ -1304,7 +1305,7 @@ function workflowMcpServerNameFromAllowedTools(allowedTools) {
1304
1305
  if (typeof toolName !== "string")
1305
1306
  continue;
1306
1307
  const parsed = parseAllowedMcpToolName(toolName);
1307
- if (!parsed || parsed.server === browserServerName || parsed.tool.startsWith("browser_"))
1308
+ if (!parsed || parsed.server === browserServerName || hasBrowserContractPrefix(parsed.tool))
1308
1309
  continue;
1309
1310
  return parsed.server;
1310
1311
  }
@@ -13,6 +13,7 @@ import { registerHooks } from "./register-hooks.js";
13
13
  import { registerShortcuts } from "./register-shortcuts.js";
14
14
  import { writeCrashLog } from "./crash-log.js";
15
15
  import { logWarning } from "../workflow-logger.js";
16
+ import { UNIT_TOOL_CONTRACTS } from "../unit-tool-contracts.js";
16
17
  // Static import so cmux event listeners are registered synchronously during
17
18
  // extension bootstrap. Prior implementation used `void import().then()` which
18
19
  // queued listener registration as a microtask — any CMUX_CHANNELS emit fired
@@ -30,6 +31,9 @@ const EPIPE_STORM_THRESHOLD = 100;
30
31
  const EPIPE_STORM_WINDOW_MS = 10_000;
31
32
  let epipeCount = 0;
32
33
  let epipeWindowStart = 0;
34
+ export const CRITICAL_GSD_WORKFLOW_TOOL_NAMES = [...new Set(Object.values(UNIT_TOOL_CONTRACTS)
35
+ .flatMap((contract) => contract.requiredWorkflowTools)
36
+ .filter((toolName) => toolName.startsWith("gsd_")))].sort();
33
37
  /** Write to stderr without ever re-throwing — stderr can EPIPE too, which would
34
38
  * re-enter this handler and re-loop. */
35
39
  function safeStderr(msg) {
@@ -121,6 +125,20 @@ export function installEpipeGuard() {
121
125
  process.on("unhandledRejection", _gsdRejectionGuard);
122
126
  }
123
127
  }
128
+ function assertCriticalGsdWorkflowToolsRegistered(pi) {
129
+ if (typeof pi.getAllTools !== "function")
130
+ return;
131
+ const registered = new Set(pi.getAllTools().map((tool) => tool.name));
132
+ const missing = CRITICAL_GSD_WORKFLOW_TOOL_NAMES.filter((toolName) => !registered.has(toolName));
133
+ if (missing.length === 0)
134
+ return;
135
+ const message = [
136
+ `Critical GSD workflow tool registration failed; missing required tool(s): ${missing.join(", ")}.`,
137
+ "Check earlier bootstrap warnings for the registration slot that failed.",
138
+ ].join(" ");
139
+ logWarning("bootstrap", message);
140
+ throw new Error(message);
141
+ }
124
142
  export function registerGsdExtension(pi) {
125
143
  // Note: registerGSDCommand is called by index.ts before this function,
126
144
  // so we intentionally skip it here to avoid double-registration.
@@ -186,4 +204,5 @@ export function registerGsdExtension(pi) {
186
204
  logWarning("bootstrap", `Failed to register ${name}: ${err instanceof Error ? err.message : String(err)}`);
187
205
  }
188
206
  }
207
+ assertCriticalGsdWorkflowToolsRegistered(pi);
189
208
  }
@@ -32,12 +32,14 @@ import { applyUnitSkillVisibility, unitHasSkillManifest } from "../skill-scope.j
32
32
  import { getGuidedUnitContext } from "../guided-unit-context.js";
33
33
  import { registerPlanMilestoneSchemaRecovery } from "./plan-milestone-schema-recovery.js";
34
34
  import { AUTO_UNIT_SCOPED_TOOLS, RUN_UAT_BROWSER_TOOL_NAMES, canonicalWorkflowToolName, isWorkflowAliasTool } from "../auto-unit-tool-scope.js";
35
+ import { hasBrowserContractPrefix } from "../../shared/browser-contract.js";
35
36
  import { filterToolsForProvider } from "../model-router.js";
36
37
  import { mcpToolMatchesBaseName } from "../mcp-tool-name.js";
37
38
  import { RUN_UAT_READ_ONLY_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
38
39
  import { supportsSourceObservationsForUnit } from "../source-observations.js";
39
40
  import { clearPendingAutoStart } from "../pending-auto-start.js";
40
41
  import { resolveWorkflowToolBasePath } from "./dynamic-tools.js";
42
+ import { getRequiredWorkflowToolsForUnit } from "../unit-tool-contracts.js";
41
43
  let approvalQuestionAbortInFlight = false;
42
44
  async function loadWelcomeScreenModule() {
43
45
  const candidates = [];
@@ -140,7 +142,7 @@ function withPreservedShimTools(toolNames) {
140
142
  }
141
143
  /** True for the browser automation tools (browser_navigate, browser_click, ...). */
142
144
  function isBrowserTool(toolName) {
143
- return canonicalToolName(toolName).startsWith("browser_");
145
+ return hasBrowserContractPrefix(canonicalToolName(toolName));
144
146
  }
145
147
  /**
146
148
  * True when any message in the request is driven by a GSD workflow command
@@ -202,7 +204,7 @@ export function buildMinimalGsdToolSet(activeToolNames) {
202
204
  const minimal = resolveScopedToolNames(activeToolNames, MINIMAL_GSD_TOOL_NAMES);
203
205
  return withPreservedShimTools([...new Set([...preserved, ...minimal])]);
204
206
  }
205
- export function buildMinimalAutoGsdToolSet(activeToolNames, unitType, registeredToolNames = activeToolNames) {
207
+ export function buildMinimalAutoGsdToolSet(activeToolNames, unitType, registeredToolNames = activeToolNames, warnOnUnresolvedRequiredTools = registeredToolNames !== activeToolNames) {
206
208
  if (unitType === "run-uat") {
207
209
  return buildRunUatGsdToolSet(activeToolNames, registeredToolNames);
208
210
  }
@@ -214,7 +216,20 @@ export function buildMinimalAutoGsdToolSet(activeToolNames, unitType, registered
214
216
  ...availableBaseTools,
215
217
  ])];
216
218
  const scoped = resolveScopedToolNames([...activeToolNames, ...registeredToolNames], [...MINIMAL_GSD_TOOL_NAMES, ...unitTools]);
217
- return withPreservedShimTools([...new Set([...preserved, ...scoped])]);
219
+ const result = withPreservedShimTools([...new Set([...preserved, ...scoped])]);
220
+ warnIfRequiredWorkflowToolsUnresolved(unitType, result, warnOnUnresolvedRequiredTools);
221
+ return result;
222
+ }
223
+ function hasResolvedWorkflowTool(resolvedToolNames, requiredToolName) {
224
+ return resolvedToolNames.some((name) => name === requiredToolName || mcpToolMatchesBaseName(name, requiredToolName));
225
+ }
226
+ function warnIfRequiredWorkflowToolsUnresolved(unitType, scopedToolNames, shouldWarn) {
227
+ if (!unitType || !shouldWarn)
228
+ return;
229
+ const unresolved = getRequiredWorkflowToolsForUnit(unitType).filter((toolName) => !hasResolvedWorkflowTool(scopedToolNames, toolName));
230
+ if (unresolved.length === 0)
231
+ return;
232
+ safetyLogWarning("bootstrap", `buildMinimalAutoGsdToolSet(${unitType}): required workflow tool(s) not in active/registered surface after scoping: ${unresolved.join(", ")}. Tool registration may have partially failed, provider filtering may have removed a required tool, or workflow MCP may be disconnected.`);
218
233
  }
219
234
  export function buildRunUatGsdToolSet(activeToolNames, registeredToolNames = activeToolNames) {
220
235
  const scoped = resolveScopedToolNames([...activeToolNames, ...registeredToolNames], [
@@ -240,7 +255,7 @@ export function buildMinimalGsdWorkflowToolSet(activeToolNames, registeredToolNa
240
255
  const scoped = resolveScopedToolNames([...activeToolNames, ...registeredToolNames], WORKFLOW_GSD_TOOL_NAMES);
241
256
  return withPreservedShimTools([...new Set([...preserved, ...scoped])]);
242
257
  }
243
- export function buildRequestScopedGsdToolSet(activeToolNames, requestCustomMessages, registeredToolNames = activeToolNames, guidedUnitType) {
258
+ export function buildRequestScopedGsdToolSet(activeToolNames, requestCustomMessages, registeredToolNames = activeToolNames, guidedUnitType, warnOnUnresolvedRequiredTools = registeredToolNames !== activeToolNames) {
244
259
  for (let index = (requestCustomMessages?.length ?? 0) - 1; index >= 0; index--) {
245
260
  const currentCustomType = requestCustomMessages?.[index]?.customType;
246
261
  if (currentCustomType === "gsd-run" ||
@@ -248,7 +263,7 @@ export function buildRequestScopedGsdToolSet(activeToolNames, requestCustomMessa
248
263
  currentCustomType === "gsd-doctor-heal" ||
249
264
  currentCustomType === "gsd-triage") {
250
265
  if (guidedUnitType) {
251
- return buildMinimalAutoGsdToolSet(activeToolNames, guidedUnitType, registeredToolNames);
266
+ return buildMinimalAutoGsdToolSet(activeToolNames, guidedUnitType, registeredToolNames, warnOnUnresolvedRequiredTools);
252
267
  }
253
268
  return buildMinimalGsdWorkflowToolSet(activeToolNames, registeredToolNames);
254
269
  }
@@ -282,8 +297,9 @@ function applyMinimalGsdToolSurface(pi) {
282
297
  const dash = getAutoRuntimeSnapshot();
283
298
  if (dash.active && dash.currentUnit) {
284
299
  const currentToolNames = pi.getActiveTools();
300
+ const hasRegisteredSurface = typeof pi.getAllTools === "function";
285
301
  const registeredToolNames = resolveRegisteredToolNames(pi, currentToolNames);
286
- const scopedToolNames = buildMinimalAutoGsdToolSet(currentToolNames, dash.currentUnit.type, registeredToolNames);
302
+ const scopedToolNames = buildMinimalAutoGsdToolSet(currentToolNames, dash.currentUnit.type, registeredToolNames, hasRegisteredSurface);
287
303
  recordAutoToolSurfaceSnapshot({
288
304
  source: "runtime-scope",
289
305
  unitType: dash.currentUnit.type,
@@ -302,9 +318,10 @@ export function scopeGsdWorkflowToolsForDispatch(pi, unitType) {
302
318
  if (isFullGsdToolSurfaceRequested())
303
319
  return null;
304
320
  const current = pi.getActiveTools();
321
+ const hasRegisteredSurface = typeof pi.getAllTools === "function";
305
322
  const registeredToolNames = resolveRegisteredToolNames(pi, current);
306
323
  const scoped = unitType
307
- ? buildMinimalAutoGsdToolSet(current, unitType, registeredToolNames)
324
+ ? buildMinimalAutoGsdToolSet(current, unitType, registeredToolNames, hasRegisteredSurface)
308
325
  : buildMinimalGsdWorkflowToolSet(current, registeredToolNames);
309
326
  recordAutoToolSurfaceSnapshot({
310
327
  source: "dispatch-scope",
@@ -1303,12 +1320,13 @@ export function registerHooks(pi, ecosystemHandlers) {
1303
1320
  return surfaceReduced ? { toolNames: providerCompatible } : undefined;
1304
1321
  }
1305
1322
  const registeredToolNames = resolveRegisteredToolNames(pi, event.activeToolNames);
1323
+ const hasRegisteredSurface = typeof pi.getAllTools === "function";
1306
1324
  const compatibleRegisteredToolNames = filterToolsForProvider(registeredToolNames, event.selectedModelApi, event.selectedModelProvider).compatible.filter((name) => !(dropAliases && isWorkflowAliasTool(name)));
1307
1325
  const guidedUnit = getGuidedUnitContext();
1308
1326
  const requestRegisteredToolNames = guidedUnit?.unitType === "run-uat"
1309
1327
  ? compatibleRegisteredToolNames
1310
1328
  : registeredToolNames;
1311
- const requestScoped = buildRequestScopedGsdToolSet(guidedUnit?.unitType === "run-uat" ? aliasFilteredCompatible : providerCompatible, event.requestCustomMessages, requestRegisteredToolNames, guidedUnit?.unitType);
1329
+ const requestScoped = buildRequestScopedGsdToolSet(guidedUnit?.unitType === "run-uat" ? aliasFilteredCompatible : providerCompatible, event.requestCustomMessages, requestRegisteredToolNames, guidedUnit?.unitType, hasRegisteredSurface);
1312
1330
  if (requestScoped) {
1313
1331
  recordAutoToolSurfaceSnapshot({
1314
1332
  source: "provider-adjustment",
@@ -1324,7 +1342,7 @@ export function registerHooks(pi, ecosystemHandlers) {
1324
1342
  const registeredForUnit = dash.currentUnit.type === "run-uat"
1325
1343
  ? compatibleRegisteredToolNames
1326
1344
  : resolveRegisteredToolNames(pi, event.activeToolNames);
1327
- const scopedToolNames = buildMinimalAutoGsdToolSet(dash.currentUnit.type === "run-uat" ? aliasFilteredCompatible : providerCompatible, dash.currentUnit.type, registeredForUnit);
1345
+ const scopedToolNames = buildMinimalAutoGsdToolSet(dash.currentUnit.type === "run-uat" ? aliasFilteredCompatible : providerCompatible, dash.currentUnit.type, registeredForUnit, hasRegisteredSurface);
1328
1346
  recordAutoToolSurfaceSnapshot({
1329
1347
  source: "provider-adjustment",
1330
1348
  unitType: dash.currentUnit.type,
@@ -1,8 +1,14 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Shared browser-observable UAT requirement and evidence detection.
3
- export const BROWSER_REQUIREMENT_RE = /\b(?:file:\/\/|localhost|playwright|chrome|screenshot|snapshot|browser_(?:assert|batch|find|verify|snapshot_refs))\b|\b(?:open|launch|navigate|load|visit|serve|start)\b.{0,80}\b(?:browser|page|localhost|file:\/\/)\b|\bbrowser\s+(?:check|session|test|uat|tool|automation|interaction|flow)\b/i;
3
+ import { BROWSER_EVIDENCE_SIGNAL_TOOL_NAMES } from "../shared/browser-contract.js";
4
+ // Alternation fragment over the contract's evidence-signal names, e.g.
5
+ // `browser_(?:assert|batch|...)`. The names are `browser_`-prefixed
6
+ // identifiers (pinned by tests/browser-contract.test.ts), so no escaping is
7
+ // needed.
8
+ const BROWSER_TOOL_SIGNAL = `browser_(?:${BROWSER_EVIDENCE_SIGNAL_TOOL_NAMES.map((name) => name.slice("browser_".length)).join("|")})`;
9
+ export const BROWSER_REQUIREMENT_RE = new RegExp(String.raw `\b(?:file://|localhost|playwright|chrome|screenshot|snapshot|${BROWSER_TOOL_SIGNAL})\b|\b(?:open|launch|navigate|load|visit|serve|start)\b.{0,80}\b(?:browser|page|localhost|file://)\b|\bbrowser\s+(?:check|session|test|uat|tool|automation|interaction|flow)\b`, "i");
4
10
  export const NO_BROWSER_EVIDENCE_RE = /\b(?:no|without|not|wasn'?t|isn'?t)\s+(?:automated\s+)?(?:live\s+)?browser(?:\s+(?:session|test|uat))?|\bno\s+automated\s+browser\b|\bnot\s+conducted\b/i;
5
- export const BROWSER_RUNTIME_RE = /\b(?:browser|playwright|chrome|camoufox|browser_(?:assert|batch|find|verify|snapshot_refs)|screenshot|snapshot|file:\/\/|localhost)\b/i;
11
+ export const BROWSER_RUNTIME_RE = new RegExp(String.raw `\b(?:browser|playwright|chrome|camoufox|${BROWSER_TOOL_SIGNAL}|screenshot|snapshot|file://|localhost)\b`, "i");
6
12
  export const BROWSER_ACTION_RE = /\b(?:open(?:ed)?|navigate(?:d)?|click(?:ed)?|type(?:d)?|reload(?:ed)?|capture(?:d)?|screenshot|snapshot)\b/i;
7
13
  export const BROWSER_ASSERTION_RE = /\b(?:assert(?:ed|ion)?|observed|confirmed|verified|expected|visible|text|count|label|strikethrough|localstorage|screenshot|snapshot|passed)\b/i;
8
14
  const NON_REQUIREMENT_BROWSER_HEADING_RE = /^(?:not\s+proven|not\s+covered|out\s+of\s+scope|deferred|follow-?ups?|known\s+limitations|notes\s+for\s+tester)\b/i;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { resolve } from "node:path";
4
+ import { isGsdBrowserMcpServerConfig } from "../shared/gsd-browser-cli.js";
4
5
  import { toMcpWildcardToolName } from "./mcp-tool-name.js";
5
6
  import { resolveModelMcpConfig } from "./preferences-mcp.js";
6
7
  function isRecord(value) {
@@ -63,29 +64,11 @@ function isWorkflowMcpServerConfig(config) {
63
64
  const args = Array.isArray(config.args) ? config.args.filter((arg) => typeof arg === "string") : [];
64
65
  return args.some((arg) => arg.includes("gsd-mcp-server") || arg.includes("packages/mcp-server"));
65
66
  }
66
- function isBrowserMcpServerConfig(config) {
67
- if (!isRecord(config))
68
- return false;
69
- const command = typeof config.command === "string" ? config.command : "";
70
- if (command.includes("gsd-browser") || command.includes("@opengsd/gsd-browser")) {
71
- return true;
72
- }
73
- const env = config.env;
74
- if (isRecord(env)) {
75
- if (typeof env.GSD_BROWSER_CLI_PATH === "string"
76
- || typeof env.GSD_BROWSER_BIN_PATH === "string"
77
- || typeof env.GSD_BROWSER_MCP_COMMAND === "string") {
78
- return true;
79
- }
80
- }
81
- const args = Array.isArray(config.args) ? config.args.filter((arg) => typeof arg === "string") : [];
82
- return args.some((arg) => arg.includes("gsd-browser") || arg.includes("@opengsd/gsd-browser"));
83
- }
84
67
  export function discoverWorkflowMcpServerName(projectDir) {
85
68
  return discoverMcpServers(projectDir).find((server) => isWorkflowMcpServerConfig(server.config))?.name;
86
69
  }
87
70
  export function discoverBrowserMcpServerName(projectDir) {
88
- return discoverMcpServers(projectDir).find((server) => isBrowserMcpServerConfig(server.config))?.name;
71
+ return discoverMcpServers(projectDir).find((server) => isGsdBrowserMcpServerConfig(server.config))?.name;
89
72
  }
90
73
  export function discoverMcpServerNames(projectDir) {
91
74
  return discoverMcpServers(projectDir).map((server) => server.name);
@@ -1,5 +1,6 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Central UAT mode policy for dispatch, tool presentation, and result validation.
3
+ import { hasBrowserContractPrefix } from "../shared/browser-contract.js";
3
4
  import { extractUatType } from "./files.js";
4
5
  import { hasBrowserRequiredText } from "./browser-evidence.js";
5
6
  import { parseMcpToolName } from "./mcp-tool-name.js";
@@ -84,7 +85,7 @@ export function uatTypeIncludesBrowser(uatType) {
84
85
  export function isUatBrowserToolName(toolName) {
85
86
  const parsed = parseMcpToolName(toolName);
86
87
  const canonicalName = parsed?.toolName ?? toolName;
87
- if (canonicalName.startsWith("browser_"))
88
+ if (hasBrowserContractPrefix(canonicalName))
88
89
  return true;
89
90
  return parsed?.toolName === "*" && parsed.serverName.toLowerCase().includes("browser");
90
91
  }
@@ -22,6 +22,7 @@
22
22
  // (`UNIT_MANIFESTS` stays in unit-context-manifest.ts, already type-enforced
23
23
  // against the registry's `UnitType`) and prompt-template association (still
24
24
  // implicit in auto-prompts.ts builders).
25
+ import { BROWSER_CONTRACT_TOOL_NAMES } from "../shared/browser-contract.js";
25
26
  // ─── Shared tool-name constants (used by registry rows) ──────────────────
26
27
  export const RUN_UAT_WORKFLOW_TOOL_NAMES = [
27
28
  "gsd_uat_exec",
@@ -37,26 +38,12 @@ export const RUN_UAT_READ_ONLY_TOOL_NAMES = [
37
38
  "ls",
38
39
  "read",
39
40
  ];
40
- export const RUN_UAT_BROWSER_TOOL_NAMES = [
41
- "browser_navigate",
42
- "browser_click",
43
- "browser_type",
44
- "browser_fill_form",
45
- "browser_click_ref",
46
- "browser_fill_ref",
47
- "browser_wait_for",
48
- "browser_assert",
49
- "browser_verify",
50
- "browser_screenshot",
51
- "browser_snapshot_refs",
52
- "browser_find",
53
- "browser_get_console_logs",
54
- "browser_get_network_logs",
55
- "browser_evaluate",
56
- "browser_reload",
57
- "browser_batch",
58
- "browser_act",
59
- ];
41
+ /**
42
+ * Browser tools presented to run-uat. A derived view of the Browser Automation
43
+ * Contract vocabulary (shared/browser-contract.ts) — the contract module is the
44
+ * only place browser tool names are declared.
45
+ */
46
+ export const RUN_UAT_BROWSER_TOOL_NAMES = BROWSER_CONTRACT_TOOL_NAMES;
60
47
  // ─── The registry ─────────────────────────────────────────────────────────
61
48
  export const UNIT_REGISTRY = {
62
49
  "research-milestone": {
@@ -1,8 +1,10 @@
1
1
  // Project/App: gsd-pi
2
- // File Purpose: Web-app detection and Playwright/UAT guidance for planning and slice closeout.
2
+ // File Purpose: Web-app detection and browser-UAT guidance for planning and slice closeout.
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
+ import { resolveAmbientBrowserEngineResolution } from "../browser-tools/engine/selection.js";
5
6
  import { detectWebApp } from "../browser-tools/web-app-detect.js";
7
+ import { UAT_MODE_POLICIES } from "./uat-policy.js";
6
8
  export { detectWebApp };
7
9
  function readPackageJson(projectRoot) {
8
10
  const packageJsonPath = resolve(projectRoot, "package.json");
@@ -39,24 +41,56 @@ export function findPlaywrightTestScript(projectRoot) {
39
41
  }
40
42
  return null;
41
43
  }
44
+ function describeBrowserToolBacking(engineResolution) {
45
+ switch (engineResolution.engine) {
46
+ case "gsd-browser":
47
+ return "This project looks browser-facing. GSD exposes `browser_*` tools backed by the managed gsd-browser engine for run-uat.";
48
+ case "legacy":
49
+ return "This project looks browser-facing. GSD exposes Playwright-backed `browser_*` tools for run-uat.";
50
+ case "off":
51
+ return "This project looks browser-facing, but Pi browser tools are disabled (GSD_BROWSER_ENGINE=off) — prefer `runtime-executable` UAT with automated browser test commands.";
52
+ }
53
+ }
54
+ // One bullet per recommended UAT mode; `mode` keys into UAT_MODE_POLICIES so
55
+ // modes that require browser tools drop out of the guidance when the resolved
56
+ // engine provides none (mixed/live-runtime share one bullet and one policy bit).
57
+ const UAT_MODE_GUIDANCE = [
58
+ {
59
+ mode: "browser-executable",
60
+ bullet: "- `browser-executable` — navigate to `http://localhost:…`, click, screenshot, assert via `browser_*` tools during run-uat",
61
+ },
62
+ {
63
+ mode: "runtime-executable",
64
+ bullet: "- `runtime-executable` — run an automated browser test command via `gsd_uat_exec` (for example `npx playwright test`)",
65
+ },
66
+ {
67
+ mode: "mixed",
68
+ bullet: "- `mixed` / `live-runtime` — combine runtime startup checks with interactive browser verification",
69
+ },
70
+ ];
42
71
  /**
43
72
  * Markdown block injected into plan/complete-slice prompts when the project
44
- * looks browser-facing. Returns null for CLI/library-only repos.
73
+ * looks browser-facing. Returns null for CLI/library-only repos. Guidance is
74
+ * composed from the resolved Browser Automation Engine so prompts never claim
75
+ * an engine the runtime is not using; `engineResolution` is injectable for
76
+ * tests and defaults to the ambient resolution.
45
77
  */
46
- export function buildWebAppUatGuidanceBlock(projectRoot) {
78
+ export function buildWebAppUatGuidanceBlock(projectRoot, engineResolution) {
47
79
  if (!detectWebApp(projectRoot))
48
80
  return null;
81
+ const resolvedEngine = engineResolution ?? resolveAmbientBrowserEngineResolution(projectRoot);
82
+ const browserToolsAvailable = resolvedEngine.engine !== "off";
49
83
  const playwrightScript = findPlaywrightTestScript(projectRoot);
50
84
  const hasPlaywright = hasPlaywrightTestDependency(projectRoot) || playwrightScript !== null;
51
85
  const lines = [
52
86
  "### Web App UAT (detected)",
53
87
  "",
54
- "This project looks browser-facing. GSD exposes Playwright-backed `browser_*` tools by default for run-uat.",
88
+ describeBrowserToolBacking(resolvedEngine),
55
89
  "",
56
90
  "**UAT modes (pick one per slice — do not use `artifact-driven` for browser steps):**",
57
- "- `browser-executable` — navigate to `http://localhost:…`, click, screenshot, assert via `browser_*` tools during run-uat",
58
- "- `runtime-executable` run an automated browser test command via `gsd_uat_exec` (for example `npx playwright test`)",
59
- "- `mixed` / `live-runtime` — combine runtime startup checks with interactive browser verification",
91
+ ...UAT_MODE_GUIDANCE
92
+ .filter(({ mode }) => browserToolsAvailable || !UAT_MODE_POLICIES[mode].browserTools)
93
+ .map(({ bullet }) => bullet),
60
94
  "",
61
95
  "**Planning / closeout rules:**",
62
96
  "- Preconditions must name the dev-server command and URL (for example `npm run dev` → `http://localhost:3000`)",
@@ -74,7 +108,10 @@ export function buildWebAppUatGuidanceBlock(projectRoot) {
74
108
  lines.push("- Name concrete spec paths in slice Verification (for example `e2e/smoke.spec.ts`)");
75
109
  }
76
110
  else {
77
- lines.push("", "**Playwright scaffolding (first UI slice):** no `playwright` / `@playwright/test` dependency yet.", "- Add a planning task that installs Playwright, adds `playwright.config.ts`, and creates a minimal smoke spec (for example `e2e/smoke.spec.ts`)", "- Task `verify` should run `npx playwright test` (or the focused spec) with a safe, simple command", "- Until specs exist, use `browser-executable` UAT with localhost preconditions and interactive `browser_*` checks at slice closeout");
111
+ lines.push("", "**Playwright scaffolding (first UI slice):** no `playwright` / `@playwright/test` dependency yet.", "- Add a planning task that installs Playwright, adds `playwright.config.ts`, and creates a minimal smoke spec (for example `e2e/smoke.spec.ts`)", "- Task `verify` should run `npx playwright test` (or the focused spec) with a safe, simple command");
112
+ if (browserToolsAvailable) {
113
+ lines.push("- Until specs exist, use `browser-executable` UAT with localhost preconditions and interactive `browser_*` checks at slice closeout");
114
+ }
78
115
  }
79
116
  return lines.join("\n");
80
117
  }
@@ -214,9 +214,11 @@ export function registerNativeSearchHooks(pi) {
214
214
  isAnthropic = isAnthropicProvider && payloadLooksAnthropic !== false;
215
215
  }
216
216
  else {
217
- // Last resort: session-restore paths where the SDK doesn't pass model.
218
- // The model-name prefix is best-effort and assumes direct Anthropic.
219
- isAnthropic = payloadLooksAnthropic === true;
217
+ // No authoritative provider info available (no event.model, no model_select).
218
+ // Do NOT inject native web_search guessing on model name alone causes 400
219
+ // "unsupported_value" errors when the actual provider (copilot, openrouter,
220
+ // proxy, etc.) doesn't expose the server-side search tool (#648).
221
+ isAnthropic = false;
220
222
  }
221
223
  if (!isAnthropic)
222
224
  return;
@@ -0,0 +1,59 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Browser Automation Contract — the single source for the canonical
3
+ // Pi-facing browser tool vocabulary. Engine adapters (legacy Playwright, managed
4
+ // gsd-browser), UAT policy, dispatch preflight, and evidence detection all derive
5
+ // their browser tool knowledge from this module instead of re-listing names.
6
+ /**
7
+ * Canonical `browser_*` tool names of the Browser Automation Contract.
8
+ *
9
+ * These are the product-level names Units see regardless of which Browser
10
+ * Automation Engine serves them (ADR-024). Adding a capability here is the
11
+ * one-line vocabulary change; the engine adapters and presentation surfaces
12
+ * are typed against this list, so missing coverage fails typecheck.
13
+ */
14
+ export const BROWSER_CONTRACT_TOOL_NAMES = [
15
+ "browser_navigate",
16
+ "browser_click",
17
+ "browser_type",
18
+ "browser_fill_form",
19
+ "browser_click_ref",
20
+ "browser_fill_ref",
21
+ "browser_wait_for",
22
+ "browser_assert",
23
+ "browser_verify",
24
+ "browser_screenshot",
25
+ "browser_snapshot_refs",
26
+ "browser_find",
27
+ "browser_get_console_logs",
28
+ "browser_get_network_logs",
29
+ "browser_evaluate",
30
+ "browser_reload",
31
+ "browser_batch",
32
+ "browser_act",
33
+ ];
34
+ const BROWSER_CONTRACT_TOOL_NAME_SET = new Set(BROWSER_CONTRACT_TOOL_NAMES);
35
+ export function isBrowserContractToolName(name) {
36
+ return BROWSER_CONTRACT_TOOL_NAME_SET.has(name);
37
+ }
38
+ /**
39
+ * Whether a canonical (non-MCP-prefixed) tool name belongs to the browser tool
40
+ * family. Broader than the contract list on purpose: an External MCP Client or
41
+ * host integration may supply additional `browser_*` tools that still satisfy
42
+ * browser-backed UAT.
43
+ */
44
+ export function hasBrowserContractPrefix(canonicalToolName) {
45
+ return canonicalToolName.startsWith("browser_");
46
+ }
47
+ /**
48
+ * Contract tool names whose appearance in prose marks browser-backed UAT
49
+ * activity (requirement or evidence language). Consumed by the
50
+ * browser-evidence regexes so textual detection stays derived from the
51
+ * contract vocabulary.
52
+ */
53
+ export const BROWSER_EVIDENCE_SIGNAL_TOOL_NAMES = [
54
+ "browser_assert",
55
+ "browser_batch",
56
+ "browser_find",
57
+ "browser_verify",
58
+ "browser_snapshot_refs",
59
+ ];