@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
@@ -10,6 +10,7 @@ import { DISCUSS_TOOLS_ALLOWLIST } from "../constants.ts";
10
10
  import { buildMinimalAutoGsdToolSet, buildMinimalGsdToolSet, buildMinimalGsdWorkflowToolSet, buildRequestScopedGsdToolSet, MINIMAL_AUTO_BASE_TOOL_NAMES, MINIMAL_GSD_TOOL_NAMES, requestHasGsdCustomType, restoreGsdWorkflowTools, scopeGsdWorkflowToolsForDispatch } from "../bootstrap/register-hooks.ts";
11
11
  import { filterToolsForProvider } from "../model-router.ts";
12
12
  import { applyUnitSkillVisibility } from "../skill-scope.ts";
13
+ import { drainLogs } from "../workflow-logger.ts";
13
14
 
14
15
  test("buildMinimalGsdToolSet preserves non-GSD tools and replaces broad GSD surface", () => {
15
16
  const result = buildMinimalGsdToolSet([
@@ -103,6 +104,40 @@ test("buildMinimalAutoGsdToolSet keeps unit-specific completion tools without al
103
104
  assert.ok(!result.includes("gsd_complete_slice"));
104
105
  });
105
106
 
107
+ test("buildMinimalAutoGsdToolSet warns when plan-milestone required tools are unresolved", () => {
108
+ drainLogs();
109
+ const result = buildMinimalAutoGsdToolSet(
110
+ [
111
+ "ask_user_questions",
112
+ "bash",
113
+ "read",
114
+ "gsd_milestone_status",
115
+ "gsd_plan_milestone",
116
+ ],
117
+ "plan-milestone",
118
+ [
119
+ "ask_user_questions",
120
+ "bash",
121
+ "read",
122
+ "gsd_milestone_status",
123
+ "gsd_plan_milestone",
124
+ ],
125
+ );
126
+
127
+ assert.ok(result.includes("gsd_plan_milestone"));
128
+ assert.ok(!result.includes("gsd_plan_slice"));
129
+
130
+ const logs = drainLogs();
131
+ assert.ok(
132
+ logs.some((entry) =>
133
+ entry.component === "bootstrap" &&
134
+ entry.message.includes("buildMinimalAutoGsdToolSet(plan-milestone)") &&
135
+ entry.message.includes("gsd_plan_slice")
136
+ ),
137
+ `expected missing gsd_plan_slice bootstrap warning, got ${JSON.stringify(logs)}`,
138
+ );
139
+ });
140
+
106
141
  test("buildMinimalAutoGsdToolSet scopes run-uat to UAT-specific and read-only tools", () => {
107
142
  const active = ["ask_user_questions", "bash", "read", "edit", "write", "gsd_summary_save"];
108
143
  const registered = [
@@ -5,7 +5,6 @@ import {
5
5
  classifyUatContent,
6
6
  getDeclaredUatType,
7
7
  getUatBrowserToolSupportError,
8
- hasUatBrowserToolSurface,
9
8
  isPartialEligibleUatType,
10
9
  resolveEffectiveUatType,
11
10
  shouldDispatchUatForContent,
@@ -13,6 +12,12 @@ import {
13
12
  uatTypeIncludesBrowser,
14
13
  validateUatModePolicy,
15
14
  } from "../uat-policy.ts";
15
+ import {
16
+ assertBrowserAutomationContractAvailable,
17
+ assertBrowserAutomationContractMissing,
18
+ assertBrowserBackedUatCanDispatch,
19
+ BROWSER_AUTOMATION_CONTRACT_TOOLS,
20
+ } from "./browser-automation-contract-fixture.ts";
16
21
 
17
22
  describe("uat-policy", () => {
18
23
  it("defaults missing UAT mode to artifact-driven", () => {
@@ -63,14 +68,14 @@ describe("uat-policy", () => {
63
68
  }
64
69
  });
65
70
 
66
- it("detects direct and MCP-shaped browser tool surfaces", () => {
67
- assert.equal(hasUatBrowserToolSurface(["read", "browser_navigate"]), true);
68
- assert.equal(hasUatBrowserToolSurface(["read", "mcp__gsd-browser__browser_navigate"]), true);
69
- assert.equal(hasUatBrowserToolSurface(["read", "mcp__gsd-browser__*"]), true);
70
- assert.equal(hasUatBrowserToolSurface(["read", "mcp__browser-uat__*"]), true);
71
- assert.equal(hasUatBrowserToolSurface(["read", "mcp__gsd-workflow__*"]), false);
72
- assert.equal(hasUatBrowserToolSurface(["read", "gsd_uat_exec"]), false);
73
- assert.equal(hasUatBrowserToolSurface(undefined), false);
71
+ it("detects Browser Automation Contract capability across adapters", () => {
72
+ assertBrowserAutomationContractAvailable(BROWSER_AUTOMATION_CONTRACT_TOOLS.piProvider);
73
+ assertBrowserAutomationContractAvailable(BROWSER_AUTOMATION_CONTRACT_TOOLS.externalMcpClient);
74
+ assertBrowserAutomationContractAvailable(BROWSER_AUTOMATION_CONTRACT_TOOLS.externalMcpWildcard);
75
+ assertBrowserAutomationContractAvailable(BROWSER_AUTOMATION_CONTRACT_TOOLS.otherBrowserMcp);
76
+ assertBrowserAutomationContractMissing(BROWSER_AUTOMATION_CONTRACT_TOOLS.workflowOnly);
77
+ assertBrowserAutomationContractMissing(BROWSER_AUTOMATION_CONTRACT_TOOLS.withoutBrowser);
78
+ assertBrowserAutomationContractMissing(undefined);
74
79
  });
75
80
 
76
81
  it("reports missing browser tools only for browser-backed UAT with a known tool snapshot", () => {
@@ -92,26 +97,16 @@ describe("uat-policy", () => {
92
97
  }),
93
98
  null,
94
99
  );
95
- assert.equal(
96
- getUatBrowserToolSupportError({
97
- uatType: "human-experience",
98
- activeTools: ["read", "gsd_uat_exec"],
99
- registeredTools: ["browser_navigate"],
100
- milestoneId: "M001",
101
- sliceId: "S01",
102
- }),
103
- null,
104
- );
105
- assert.equal(
106
- getUatBrowserToolSupportError({
107
- uatType: "human-experience",
108
- activeTools: ["read", "gsd_uat_exec"],
109
- registeredTools: ["mcp__gsd-browser__*"],
110
- milestoneId: "M001",
111
- sliceId: "S01",
112
- }),
113
- null,
114
- );
100
+ assertBrowserBackedUatCanDispatch({
101
+ uatType: "human-experience",
102
+ activeTools: BROWSER_AUTOMATION_CONTRACT_TOOLS.withoutBrowser,
103
+ registeredTools: BROWSER_AUTOMATION_CONTRACT_TOOLS.piProvider,
104
+ });
105
+ assertBrowserBackedUatCanDispatch({
106
+ uatType: "human-experience",
107
+ activeTools: BROWSER_AUTOMATION_CONTRACT_TOOLS.withoutBrowser,
108
+ registeredTools: BROWSER_AUTOMATION_CONTRACT_TOOLS.externalMcpWildcard,
109
+ });
115
110
 
116
111
  const error = getUatBrowserToolSupportError({
117
112
  uatType: "browser-executable",
@@ -15,6 +15,10 @@ function scaffoldProject(root: string, pkg: Record<string, unknown>): void {
15
15
  writeFileSync(join(root, "package.json"), JSON.stringify(pkg, null, 2));
16
16
  }
17
17
 
18
+ const LEGACY_ENGINE = { engine: "legacy", source: "probe", reason: "test" } as const;
19
+ const MANAGED_ENGINE = { engine: "gsd-browser", source: "probe", reason: "test" } as const;
20
+ const OFF_ENGINE = { engine: "off", source: "env", reason: "test" } as const;
21
+
18
22
  describe("web-app-uat guidance", () => {
19
23
  test("returns null for non-web projects", () => {
20
24
  const root = mkdtempSync(join(tmpdir(), "gsd-web-uat-"));
@@ -36,15 +40,54 @@ describe("web-app-uat guidance", () => {
36
40
  scripts: { dev: "vite" },
37
41
  });
38
42
  assert.equal(detectWebApp(root), true);
39
- const block = buildWebAppUatGuidanceBlock(root);
43
+ const block = buildWebAppUatGuidanceBlock(root, LEGACY_ENGINE);
40
44
  assert.ok(block);
41
45
  assert.match(block!, /browser-executable/);
46
+ assert.match(block!, /Playwright-backed `browser_\*` tools/);
42
47
  assert.match(block!, /Playwright scaffolding/);
43
48
  } finally {
44
49
  rmSync(root, { recursive: true, force: true });
45
50
  }
46
51
  });
47
52
 
53
+ test("describes the managed gsd-browser engine when it is the resolved backing", () => {
54
+ const root = mkdtempSync(join(tmpdir(), "gsd-web-uat-"));
55
+ try {
56
+ scaffoldProject(root, {
57
+ dependencies: { react: "19.0.0" },
58
+ scripts: { dev: "vite" },
59
+ });
60
+ const block = buildWebAppUatGuidanceBlock(root, MANAGED_ENGINE);
61
+ assert.ok(block);
62
+ assert.match(block!, /managed gsd-browser engine/);
63
+ assert.match(block!, /browser-executable/);
64
+ assert.doesNotMatch(block!, /Playwright-backed/);
65
+ } finally {
66
+ rmSync(root, { recursive: true, force: true });
67
+ }
68
+ });
69
+
70
+ test("steers to runtime-executable UAT when browser tools are off", () => {
71
+ const root = mkdtempSync(join(tmpdir(), "gsd-web-uat-"));
72
+ try {
73
+ scaffoldProject(root, {
74
+ dependencies: { react: "19.0.0" },
75
+ scripts: { dev: "vite" },
76
+ });
77
+ const block = buildWebAppUatGuidanceBlock(root, OFF_ENGINE);
78
+ assert.ok(block);
79
+ assert.match(block!, /browser tools are disabled/);
80
+ assert.doesNotMatch(block!, /- `browser-executable`/);
81
+ // mixed/live-runtime require browser tools per UAT_MODE_POLICIES, so the
82
+ // bullet must drop out too — recommending them would dead-end at dispatch.
83
+ assert.doesNotMatch(block!, /- `mixed`/);
84
+ assert.doesNotMatch(block!, /interactive `browser_\*` checks/);
85
+ assert.match(block!, /runtime-executable/);
86
+ } finally {
87
+ rmSync(root, { recursive: true, force: true });
88
+ }
89
+ });
90
+
48
91
  test("detects existing Playwright and npm script", () => {
49
92
  const root = mkdtempSync(join(tmpdir(), "gsd-web-uat-"));
50
93
  try {
@@ -1,6 +1,7 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Central UAT mode policy for dispatch, tool presentation, and result validation.
3
3
 
4
+ import { hasBrowserContractPrefix } from "../shared/browser-contract.js";
4
5
  import { extractUatType } from "./files.js";
5
6
  import type { UatType } from "./files.js";
6
7
  import { hasBrowserRequiredText } from "./browser-evidence.js";
@@ -126,7 +127,7 @@ export function uatTypeIncludesBrowser(uatType: string | undefined): boolean {
126
127
  export function isUatBrowserToolName(toolName: string): boolean {
127
128
  const parsed = parseMcpToolName(toolName);
128
129
  const canonicalName = parsed?.toolName ?? toolName;
129
- if (canonicalName.startsWith("browser_")) return true;
130
+ if (hasBrowserContractPrefix(canonicalName)) return true;
130
131
  return parsed?.toolName === "*" && parsed.serverName.toLowerCase().includes("browser");
131
132
  }
132
133
 
@@ -24,6 +24,7 @@
24
24
  // implicit in auto-prompts.ts builders).
25
25
 
26
26
  import type { CanonicalWorkflowToolName } from "@opengsd/contracts";
27
+ import { BROWSER_CONTRACT_TOOL_NAMES } from "../shared/browser-contract.js";
27
28
  import type { GSDModelPhaseKey } from "./preferences-types.js";
28
29
  import type { WorkflowMcpAdapterToolName } from "./workflow-tool-surface.js";
29
30
 
@@ -82,26 +83,12 @@ export const RUN_UAT_READ_ONLY_TOOL_NAMES = [
82
83
  "read",
83
84
  ] as const;
84
85
 
85
- export const RUN_UAT_BROWSER_TOOL_NAMES = [
86
- "browser_navigate",
87
- "browser_click",
88
- "browser_type",
89
- "browser_fill_form",
90
- "browser_click_ref",
91
- "browser_fill_ref",
92
- "browser_wait_for",
93
- "browser_assert",
94
- "browser_verify",
95
- "browser_screenshot",
96
- "browser_snapshot_refs",
97
- "browser_find",
98
- "browser_get_console_logs",
99
- "browser_get_network_logs",
100
- "browser_evaluate",
101
- "browser_reload",
102
- "browser_batch",
103
- "browser_act",
104
- ] as const;
86
+ /**
87
+ * Browser tools presented to run-uat. A derived view of the Browser Automation
88
+ * Contract vocabulary (shared/browser-contract.ts) — the contract module is the
89
+ * only place browser tool names are declared.
90
+ */
91
+ export const RUN_UAT_BROWSER_TOOL_NAMES = BROWSER_CONTRACT_TOOL_NAMES;
105
92
 
106
93
  // ─── The registry ─────────────────────────────────────────────────────────
107
94
 
@@ -1,10 +1,12 @@
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
 
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { resolve } from "node:path";
6
6
 
7
+ import { resolveAmbientBrowserEngineResolution, type BrowserEngineResolution } from "../browser-tools/engine/selection.js";
7
8
  import { detectWebApp } from "../browser-tools/web-app-detect.js";
9
+ import { UAT_MODE_POLICIES, type UatType } from "./uat-policy.js";
8
10
 
9
11
  export { detectWebApp };
10
12
 
@@ -47,24 +49,61 @@ export function findPlaywrightTestScript(projectRoot: string): string | null {
47
49
  return null;
48
50
  }
49
51
 
52
+ function describeBrowserToolBacking(engineResolution: BrowserEngineResolution): string {
53
+ switch (engineResolution.engine) {
54
+ case "gsd-browser":
55
+ return "This project looks browser-facing. GSD exposes `browser_*` tools backed by the managed gsd-browser engine for run-uat.";
56
+ case "legacy":
57
+ return "This project looks browser-facing. GSD exposes Playwright-backed `browser_*` tools for run-uat.";
58
+ case "off":
59
+ 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.";
60
+ }
61
+ }
62
+
63
+ // One bullet per recommended UAT mode; `mode` keys into UAT_MODE_POLICIES so
64
+ // modes that require browser tools drop out of the guidance when the resolved
65
+ // engine provides none (mixed/live-runtime share one bullet and one policy bit).
66
+ const UAT_MODE_GUIDANCE: ReadonlyArray<{ mode: UatType; bullet: string }> = [
67
+ {
68
+ mode: "browser-executable",
69
+ bullet: "- `browser-executable` — navigate to `http://localhost:…`, click, screenshot, assert via `browser_*` tools during run-uat",
70
+ },
71
+ {
72
+ mode: "runtime-executable",
73
+ bullet: "- `runtime-executable` — run an automated browser test command via `gsd_uat_exec` (for example `npx playwright test`)",
74
+ },
75
+ {
76
+ mode: "mixed",
77
+ bullet: "- `mixed` / `live-runtime` — combine runtime startup checks with interactive browser verification",
78
+ },
79
+ ];
80
+
50
81
  /**
51
82
  * Markdown block injected into plan/complete-slice prompts when the project
52
- * looks browser-facing. Returns null for CLI/library-only repos.
83
+ * looks browser-facing. Returns null for CLI/library-only repos. Guidance is
84
+ * composed from the resolved Browser Automation Engine so prompts never claim
85
+ * an engine the runtime is not using; `engineResolution` is injectable for
86
+ * tests and defaults to the ambient resolution.
53
87
  */
54
- export function buildWebAppUatGuidanceBlock(projectRoot: string): string | null {
88
+ export function buildWebAppUatGuidanceBlock(
89
+ projectRoot: string,
90
+ engineResolution?: BrowserEngineResolution,
91
+ ): string | null {
55
92
  if (!detectWebApp(projectRoot)) return null;
56
93
 
94
+ const resolvedEngine = engineResolution ?? resolveAmbientBrowserEngineResolution(projectRoot);
95
+ const browserToolsAvailable = resolvedEngine.engine !== "off";
57
96
  const playwrightScript = findPlaywrightTestScript(projectRoot);
58
97
  const hasPlaywright = hasPlaywrightTestDependency(projectRoot) || playwrightScript !== null;
59
98
  const lines = [
60
99
  "### Web App UAT (detected)",
61
100
  "",
62
- "This project looks browser-facing. GSD exposes Playwright-backed `browser_*` tools by default for run-uat.",
101
+ describeBrowserToolBacking(resolvedEngine),
63
102
  "",
64
103
  "**UAT modes (pick one per slice — do not use `artifact-driven` for browser steps):**",
65
- "- `browser-executable` — navigate to `http://localhost:…`, click, screenshot, assert via `browser_*` tools during run-uat",
66
- "- `runtime-executable` run an automated browser test command via `gsd_uat_exec` (for example `npx playwright test`)",
67
- "- `mixed` / `live-runtime` — combine runtime startup checks with interactive browser verification",
104
+ ...UAT_MODE_GUIDANCE
105
+ .filter(({ mode }) => browserToolsAvailable || !UAT_MODE_POLICIES[mode].browserTools)
106
+ .map(({ bullet }) => bullet),
68
107
  "",
69
108
  "**Planning / closeout rules:**",
70
109
  "- Preconditions must name the dev-server command and URL (for example `npm run dev` → `http://localhost:3000`)",
@@ -93,8 +132,12 @@ export function buildWebAppUatGuidanceBlock(projectRoot: string): string | null
93
132
  "**Playwright scaffolding (first UI slice):** no `playwright` / `@playwright/test` dependency yet.",
94
133
  "- Add a planning task that installs Playwright, adds `playwright.config.ts`, and creates a minimal smoke spec (for example `e2e/smoke.spec.ts`)",
95
134
  "- Task `verify` should run `npx playwright test` (or the focused spec) with a safe, simple command",
96
- "- Until specs exist, use `browser-executable` UAT with localhost preconditions and interactive `browser_*` checks at slice closeout",
97
135
  );
136
+ if (browserToolsAvailable) {
137
+ lines.push(
138
+ "- Until specs exist, use `browser-executable` UAT with localhost preconditions and interactive `browser_*` checks at slice closeout",
139
+ );
140
+ }
98
141
  }
99
142
 
100
143
  return lines.join("\n");
@@ -243,9 +243,11 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): { getIsAnthropic:
243
243
  // Anthropic-only tool never leaks into OpenAI Responses requests.
244
244
  isAnthropic = isAnthropicProvider && payloadLooksAnthropic !== false;
245
245
  } else {
246
- // Last resort: session-restore paths where the SDK doesn't pass model.
247
- // The model-name prefix is best-effort and assumes direct Anthropic.
248
- isAnthropic = payloadLooksAnthropic === true;
246
+ // No authoritative provider info available (no event.model, no model_select).
247
+ // Do NOT inject native web_search guessing on model name alone causes 400
248
+ // "unsupported_value" errors when the actual provider (copilot, openrouter,
249
+ // proxy, etc.) doesn't expose the server-side search tool (#648).
250
+ isAnthropic = false;
249
251
  }
250
252
  if (!isAnthropic) return;
251
253
 
@@ -0,0 +1,66 @@
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
+ /**
8
+ * Canonical `browser_*` tool names of the Browser Automation Contract.
9
+ *
10
+ * These are the product-level names Units see regardless of which Browser
11
+ * Automation Engine serves them (ADR-024). Adding a capability here is the
12
+ * one-line vocabulary change; the engine adapters and presentation surfaces
13
+ * are typed against this list, so missing coverage fails typecheck.
14
+ */
15
+ export const BROWSER_CONTRACT_TOOL_NAMES = [
16
+ "browser_navigate",
17
+ "browser_click",
18
+ "browser_type",
19
+ "browser_fill_form",
20
+ "browser_click_ref",
21
+ "browser_fill_ref",
22
+ "browser_wait_for",
23
+ "browser_assert",
24
+ "browser_verify",
25
+ "browser_screenshot",
26
+ "browser_snapshot_refs",
27
+ "browser_find",
28
+ "browser_get_console_logs",
29
+ "browser_get_network_logs",
30
+ "browser_evaluate",
31
+ "browser_reload",
32
+ "browser_batch",
33
+ "browser_act",
34
+ ] as const;
35
+
36
+ export type BrowserContractToolName = (typeof BROWSER_CONTRACT_TOOL_NAMES)[number];
37
+
38
+ const BROWSER_CONTRACT_TOOL_NAME_SET: ReadonlySet<string> = new Set(BROWSER_CONTRACT_TOOL_NAMES);
39
+
40
+ export function isBrowserContractToolName(name: string): name is BrowserContractToolName {
41
+ return BROWSER_CONTRACT_TOOL_NAME_SET.has(name);
42
+ }
43
+
44
+ /**
45
+ * Whether a canonical (non-MCP-prefixed) tool name belongs to the browser tool
46
+ * family. Broader than the contract list on purpose: an External MCP Client or
47
+ * host integration may supply additional `browser_*` tools that still satisfy
48
+ * browser-backed UAT.
49
+ */
50
+ export function hasBrowserContractPrefix(canonicalToolName: string): boolean {
51
+ return canonicalToolName.startsWith("browser_");
52
+ }
53
+
54
+ /**
55
+ * Contract tool names whose appearance in prose marks browser-backed UAT
56
+ * activity (requirement or evidence language). Consumed by the
57
+ * browser-evidence regexes so textual detection stays derived from the
58
+ * contract vocabulary.
59
+ */
60
+ export const BROWSER_EVIDENCE_SIGNAL_TOOL_NAMES = [
61
+ "browser_assert",
62
+ "browser_batch",
63
+ "browser_find",
64
+ "browser_verify",
65
+ "browser_snapshot_refs",
66
+ ] as const satisfies readonly BrowserContractToolName[];
@@ -55,6 +55,14 @@ function parseGsdBrowserVersion(output: string): string | null {
55
55
  return output.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] ?? null;
56
56
  }
57
57
 
58
+ function isRecord(value: unknown): value is Record<string, unknown> {
59
+ return !!value && typeof value === "object" && !Array.isArray(value);
60
+ }
61
+
62
+ function resolveExplicitGsdBrowserCliPath(env: NodeJS.ProcessEnv): string | undefined {
63
+ return env.GSD_BROWSER_CLI_PATH?.trim() || env.GSD_BROWSER_BIN_PATH?.trim() || undefined;
64
+ }
65
+
58
66
  function resolveBundledGsdBrowserPackageVersion(): string | null {
59
67
  try {
60
68
  const requireFromHere = createRequire(import.meta.url);
@@ -66,20 +74,28 @@ function resolveBundledGsdBrowserPackageVersion(): string | null {
66
74
  }
67
75
  }
68
76
 
77
+ // The `gsd-browser --version` subprocess result cannot change mid-session (the
78
+ // engine-switch guard forbids restarting with a different engine), and both the
79
+ // availability probe and the launch-config resolution ask for it at session
80
+ // start — memoize so the up-to-2s spawn happens once per process.
81
+ let cachedPathProbeVersion: string | null | undefined;
82
+
69
83
  function resolvePathGsdBrowserVersion(env: NodeJS.ProcessEnv): string | null {
70
84
  const explicit = env.GSD_BROWSER_PATH_VERSION?.trim();
71
85
  if (explicit) return parseGsdBrowserVersion(explicit);
86
+ if (cachedPathProbeVersion !== undefined) return cachedPathProbeVersion;
72
87
 
73
88
  try {
74
- return parseGsdBrowserVersion(execFileSync("gsd-browser", ["--version"], {
89
+ cachedPathProbeVersion = parseGsdBrowserVersion(execFileSync("gsd-browser", ["--version"], {
75
90
  encoding: "utf-8",
76
91
  env,
77
92
  stdio: ["ignore", "pipe", "ignore"],
78
93
  timeout: 2000,
79
94
  }));
80
95
  } catch {
81
- return null;
96
+ cachedPathProbeVersion = null;
82
97
  }
98
+ return cachedPathProbeVersion;
83
99
  }
84
100
 
85
101
  function shouldPreferPathGsdBrowser(env: NodeJS.ProcessEnv): boolean {
@@ -91,7 +107,7 @@ function shouldPreferPathGsdBrowser(env: NodeJS.ProcessEnv): boolean {
91
107
  }
92
108
 
93
109
  export function resolveBundledGsdBrowserCliPath(env: NodeJS.ProcessEnv = process.env): string | null {
94
- const explicit = env.GSD_BROWSER_CLI_PATH?.trim() || env.GSD_BROWSER_BIN_PATH?.trim();
110
+ const explicit = resolveExplicitGsdBrowserCliPath(env);
95
111
  if (explicit) return explicit;
96
112
 
97
113
  try {
@@ -115,6 +131,45 @@ export function resolveBundledGsdBrowserCliPath(env: NodeJS.ProcessEnv = process
115
131
  return null;
116
132
  }
117
133
 
134
+ export type GsdBrowserCliAvailability =
135
+ | { available: true; via: "explicit-env" | "bundled" | "path"; detail: string }
136
+ | { available: false; detail: string };
137
+
138
+ /**
139
+ * Cheap availability probe for the gsd-browser CLI: explicit env overrides,
140
+ * then the bundled @opengsd/gsd-browser binary (filesystem checks only), then
141
+ * a PATH lookup (one short subprocess, memoized). Used by Browser Automation
142
+ * Engine resolution to decide whether the managed engine is provable before
143
+ * preferring it over legacy Playwright. `via` names the provable source, not
144
+ * necessarily the launch source — resolveGsdBrowserMcpLaunchConfig may still
145
+ * prefer a newer PATH CLI over the bundled one.
146
+ */
147
+ export function resolveGsdBrowserCliAvailability(env: NodeJS.ProcessEnv = process.env): GsdBrowserCliAvailability {
148
+ const explicitCommand = env.GSD_BROWSER_MCP_COMMAND?.trim();
149
+ if (explicitCommand) {
150
+ return { available: true, via: "explicit-env", detail: `GSD_BROWSER_MCP_COMMAND=${explicitCommand}` };
151
+ }
152
+
153
+ const explicitCliPath = resolveExplicitGsdBrowserCliPath(env);
154
+ if (explicitCliPath) {
155
+ return existsSync(explicitCliPath)
156
+ ? { available: true, via: "explicit-env", detail: `CLI at ${explicitCliPath}` }
157
+ : { available: false, detail: `configured gsd-browser CLI path does not exist: ${explicitCliPath}` };
158
+ }
159
+
160
+ const bundledCliPath = resolveBundledGsdBrowserCliPath(env);
161
+ if (bundledCliPath) {
162
+ return { available: true, via: "bundled", detail: `bundled CLI at ${bundledCliPath}` };
163
+ }
164
+
165
+ const pathVersion = resolvePathGsdBrowserVersion(env);
166
+ if (pathVersion) {
167
+ return { available: true, via: "path", detail: `gsd-browser ${pathVersion} on PATH` };
168
+ }
169
+
170
+ return { available: false, detail: "no bundled or PATH gsd-browser CLI found" };
171
+ }
172
+
118
173
  export function buildGsdBrowserSessionName(projectRoot: string, suffix?: string): string {
119
174
  const resolvedProjectRoot = resolve(projectRoot);
120
175
  const base = sanitizeSessionSegment(basename(resolvedProjectRoot)) || "project";
@@ -123,6 +178,35 @@ export function buildGsdBrowserSessionName(projectRoot: string, suffix?: string)
123
178
  return cleanSuffix ? `gsd-${base}-${hash}-${cleanSuffix}` : `gsd-${base}-${hash}`;
124
179
  }
125
180
 
181
+ /**
182
+ * Recognize an MCP server config (from .mcp.json / Claude settings) as a
183
+ * gsd-browser server. Paired with resolveGsdBrowserMcpLaunchConfig: this module
184
+ * writes the config shape, so it also owns recognizing it. New launch shapes
185
+ * are taught here, in one place.
186
+ */
187
+ export function isGsdBrowserMcpServerConfig(config: unknown): boolean {
188
+ if (!isRecord(config)) return false;
189
+
190
+ const command = typeof config.command === "string" ? config.command : "";
191
+ if (command.includes("gsd-browser") || command.includes("@opengsd/gsd-browser")) {
192
+ return true;
193
+ }
194
+
195
+ if (isRecord(config.env)) {
196
+ const env = config.env;
197
+ if (
198
+ typeof env.GSD_BROWSER_CLI_PATH === "string"
199
+ || typeof env.GSD_BROWSER_BIN_PATH === "string"
200
+ || typeof env.GSD_BROWSER_MCP_COMMAND === "string"
201
+ ) {
202
+ return true;
203
+ }
204
+ }
205
+
206
+ const args = Array.isArray(config.args) ? config.args.filter((arg): arg is string => typeof arg === "string") : [];
207
+ return args.some((arg) => arg.includes("gsd-browser") || arg.includes("@opengsd/gsd-browser"));
208
+ }
209
+
126
210
  export function resolveGsdBrowserMcpLaunchConfig(
127
211
  projectRoot: string,
128
212
  env: NodeJS.ProcessEnv = process.env,
@@ -133,7 +217,7 @@ export function resolveGsdBrowserMcpLaunchConfig(
133
217
  const explicitArgs = parseJsonEnv<unknown>(env, "GSD_BROWSER_MCP_ARGS");
134
218
  const explicitEnv = parseJsonEnv<Record<string, string>>(env, "GSD_BROWSER_MCP_ENV");
135
219
  const explicitCommand = env.GSD_BROWSER_MCP_COMMAND?.trim();
136
- const explicitCliPath = env.GSD_BROWSER_CLI_PATH?.trim() || env.GSD_BROWSER_BIN_PATH?.trim();
220
+ const explicitCliPath = resolveExplicitGsdBrowserCliPath(env);
137
221
  const preferPathCli = !explicitCommand && !explicitCliPath && shouldPreferPathGsdBrowser(env);
138
222
  const bundledCliPath = !explicitCommand && !explicitCliPath && !preferPathCli
139
223
  ? resolveBundledGsdBrowserCliPath(env)
@@ -45,7 +45,7 @@ skill-name/
45
45
  **Reference pattern**: In SKILL.md, reference scripts using the `scripts/` path:
46
46
 
47
47
  ```bash
48
- python ~/.claude/skills/skill-name/scripts/analyze.py input.har
48
+ python ~/.agents/skills/skill-name/scripts/analyze.py input.har
49
49
  ```
50
50
  </scripts_directory>
51
51
  </file_organization>
@@ -10,16 +10,21 @@
10
10
  ## Step 1: Select the Skill
11
11
 
12
12
  ```bash
13
- ls ~/.claude/skills/
13
+ # User-global skills
14
+ ls ~/.agents/skills/ 2>/dev/null
15
+ # Project-local skills
16
+ ls .agents/skills/ 2>/dev/null
14
17
  ```
15
18
 
16
19
  Present numbered list, ask: "Which skill needs a new reference?"
17
20
 
21
+ Determine `{skill-path}`: use `.agents/skills/{skill-name}` (project-local) if found there, otherwise `~/.agents/skills/{skill-name}` (user-global). Project-local takes precedence because the skill catalog loads it first on name collision.
22
+
18
23
  ## Step 2: Analyze Current Structure
19
24
 
20
25
  ```bash
21
- cat ~/.claude/skills/{skill-name}/SKILL.md
22
- ls ~/.claude/skills/{skill-name}/references/ 2>/dev/null
26
+ cat {skill-path}/SKILL.md
27
+ ls {skill-path}/references/ 2>/dev/null
23
28
  ```
24
29
 
25
30
  Determine:
@@ -12,6 +12,8 @@ Ask (if not already provided):
12
12
  - Which skill needs a script?
13
13
  - What operation should the script perform?
14
14
 
15
+ Determine `{skill-path}`: use `.agents/skills/{skill-name}` (project-local) if found there, otherwise `~/.agents/skills/{skill-name}` (user-global). Project-local takes precedence because the skill catalog loads it first on name collision.
16
+
15
17
  ## Step 2: Analyze Script Need
16
18
 
17
19
  Confirm this is a good script candidate:
@@ -24,7 +26,7 @@ If not a good fit, suggest alternatives (inline code in workflow, reference exam
24
26
  ## Step 3: Create Scripts Directory
25
27
 
26
28
  ```bash
27
- mkdir -p ~/.claude/skills/{skill-name}/scripts
29
+ mkdir -p {skill-path}/scripts
28
30
  ```
29
31
 
30
32
  ## Step 4: Design Script
@@ -58,7 +60,7 @@ set -euo pipefail
58
60
  ## Step 6: Make Executable (if bash)
59
61
 
60
62
  ```bash
61
- chmod +x ~/.claude/skills/{skill-name}/scripts/{script-name}.sh
63
+ chmod +x {skill-path}/scripts/{script-name}.sh
62
64
  ```
63
65
 
64
66
  ## Step 7: Update Workflow to Use Script
@@ -12,6 +12,8 @@ Ask (if not already provided):
12
12
  - Which skill needs a template?
13
13
  - What output does this template structure?
14
14
 
15
+ Determine `{skill-path}`: use `.agents/skills/{skill-name}` (project-local) if found there, otherwise `~/.agents/skills/{skill-name}` (user-global). Project-local takes precedence because the skill catalog loads it first on name collision.
16
+
15
17
  ## Step 2: Analyze Template Need
16
18
 
17
19
  Confirm this is a good template candidate:
@@ -24,7 +26,7 @@ If not a good fit, suggest alternatives (workflow guidance, reference examples).
24
26
  ## Step 3: Create Templates Directory
25
27
 
26
28
  ```bash
27
- mkdir -p ~/.claude/skills/{skill-name}/templates
29
+ mkdir -p {skill-path}/templates
28
30
  ```
29
31
 
30
32
  ## Step 4: Design Template Structure