@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
@@ -58,12 +58,14 @@ import { applyUnitSkillVisibility, unitHasSkillManifest } from "../skill-scope.j
58
58
  import { getGuidedUnitContext } from "../guided-unit-context.js";
59
59
  import { registerPlanMilestoneSchemaRecovery } from "./plan-milestone-schema-recovery.js";
60
60
  import { AUTO_UNIT_SCOPED_TOOLS, RUN_UAT_BROWSER_TOOL_NAMES, canonicalWorkflowToolName, isWorkflowAliasTool } from "../auto-unit-tool-scope.js";
61
+ import { hasBrowserContractPrefix } from "../../shared/browser-contract.js";
61
62
  import { filterToolsForProvider } from "../model-router.js";
62
63
  import { mcpToolMatchesBaseName } from "../mcp-tool-name.js";
63
64
  import { RUN_UAT_READ_ONLY_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
64
65
  import { supportsSourceObservationsForUnit } from "../source-observations.js";
65
66
  import { clearPendingAutoStart } from "../pending-auto-start.js";
66
67
  import { resolveWorkflowToolBasePath } from "./dynamic-tools.js";
68
+ import { getRequiredWorkflowToolsForUnit } from "../unit-tool-contracts.js";
67
69
 
68
70
  let approvalQuestionAbortInFlight = false;
69
71
 
@@ -180,7 +182,7 @@ function withPreservedShimTools(toolNames: readonly string[]): string[] {
180
182
 
181
183
  /** True for the browser automation tools (browser_navigate, browser_click, ...). */
182
184
  function isBrowserTool(toolName: string): boolean {
183
- return canonicalToolName(toolName).startsWith("browser_");
185
+ return hasBrowserContractPrefix(canonicalToolName(toolName));
184
186
  }
185
187
 
186
188
  /**
@@ -261,6 +263,7 @@ export function buildMinimalAutoGsdToolSet(
261
263
  activeToolNames: readonly string[],
262
264
  unitType: string | undefined,
263
265
  registeredToolNames: readonly string[] = activeToolNames,
266
+ warnOnUnresolvedRequiredTools = registeredToolNames !== activeToolNames,
264
267
  ): string[] {
265
268
  if (unitType === "run-uat") {
266
269
  return buildRunUatGsdToolSet(activeToolNames, registeredToolNames);
@@ -276,7 +279,36 @@ export function buildMinimalAutoGsdToolSet(
276
279
  [...activeToolNames, ...registeredToolNames],
277
280
  [...MINIMAL_GSD_TOOL_NAMES, ...unitTools],
278
281
  );
279
- return withPreservedShimTools([...new Set([...preserved, ...scoped])]);
282
+ const result = withPreservedShimTools([...new Set([...preserved, ...scoped])]);
283
+ warnIfRequiredWorkflowToolsUnresolved(unitType, result, warnOnUnresolvedRequiredTools);
284
+ return result;
285
+ }
286
+
287
+ function hasResolvedWorkflowTool(
288
+ resolvedToolNames: readonly string[],
289
+ requiredToolName: string,
290
+ ): boolean {
291
+ return resolvedToolNames.some(
292
+ (name) => name === requiredToolName || mcpToolMatchesBaseName(name, requiredToolName),
293
+ );
294
+ }
295
+
296
+ function warnIfRequiredWorkflowToolsUnresolved(
297
+ unitType: string | undefined,
298
+ scopedToolNames: readonly string[],
299
+ shouldWarn: boolean,
300
+ ): void {
301
+ if (!unitType || !shouldWarn) return;
302
+
303
+ const unresolved = getRequiredWorkflowToolsForUnit(unitType).filter(
304
+ (toolName) => !hasResolvedWorkflowTool(scopedToolNames, toolName),
305
+ );
306
+ if (unresolved.length === 0) return;
307
+
308
+ safetyLogWarning(
309
+ "bootstrap",
310
+ `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.`,
311
+ );
280
312
  }
281
313
 
282
314
  export function buildRunUatGsdToolSet(
@@ -329,6 +361,7 @@ export function buildRequestScopedGsdToolSet(
329
361
  requestCustomMessages: readonly { customType?: string }[] | undefined,
330
362
  registeredToolNames: readonly string[] = activeToolNames,
331
363
  guidedUnitType?: string,
364
+ warnOnUnresolvedRequiredTools = registeredToolNames !== activeToolNames,
332
365
  ): string[] | undefined {
333
366
  for (let index = (requestCustomMessages?.length ?? 0) - 1; index >= 0; index--) {
334
367
  const currentCustomType = requestCustomMessages?.[index]?.customType;
@@ -339,7 +372,12 @@ export function buildRequestScopedGsdToolSet(
339
372
  currentCustomType === "gsd-triage"
340
373
  ) {
341
374
  if (guidedUnitType) {
342
- return buildMinimalAutoGsdToolSet(activeToolNames, guidedUnitType, registeredToolNames);
375
+ return buildMinimalAutoGsdToolSet(
376
+ activeToolNames,
377
+ guidedUnitType,
378
+ registeredToolNames,
379
+ warnOnUnresolvedRequiredTools,
380
+ );
343
381
  }
344
382
  return buildMinimalGsdWorkflowToolSet(activeToolNames, registeredToolNames);
345
383
  }
@@ -388,11 +426,13 @@ function applyMinimalGsdToolSurface(pi: ExtensionAPI): void {
388
426
  const dash = getAutoRuntimeSnapshot();
389
427
  if (dash.active && dash.currentUnit) {
390
428
  const currentToolNames = pi.getActiveTools();
429
+ const hasRegisteredSurface = typeof pi.getAllTools === "function";
391
430
  const registeredToolNames = resolveRegisteredToolNames(pi, currentToolNames);
392
431
  const scopedToolNames = buildMinimalAutoGsdToolSet(
393
432
  currentToolNames,
394
433
  dash.currentUnit.type,
395
434
  registeredToolNames,
435
+ hasRegisteredSurface,
396
436
  );
397
437
  recordAutoToolSurfaceSnapshot({
398
438
  source: "runtime-scope",
@@ -414,9 +454,10 @@ export function scopeGsdWorkflowToolsForDispatch(
414
454
  ): ScopedGsdWorkflowState | null {
415
455
  if (isFullGsdToolSurfaceRequested()) return null;
416
456
  const current = pi.getActiveTools();
457
+ const hasRegisteredSurface = typeof pi.getAllTools === "function";
417
458
  const registeredToolNames = resolveRegisteredToolNames(pi, current);
418
459
  const scoped = unitType
419
- ? buildMinimalAutoGsdToolSet(current, unitType, registeredToolNames)
460
+ ? buildMinimalAutoGsdToolSet(current, unitType, registeredToolNames, hasRegisteredSurface)
420
461
  : buildMinimalGsdWorkflowToolSet(current, registeredToolNames);
421
462
  recordAutoToolSurfaceSnapshot({
422
463
  source: "dispatch-scope",
@@ -1580,6 +1621,7 @@ export function registerHooks(
1580
1621
  return surfaceReduced ? { toolNames: providerCompatible } : undefined;
1581
1622
  }
1582
1623
  const registeredToolNames = resolveRegisteredToolNames(pi, event.activeToolNames);
1624
+ const hasRegisteredSurface = typeof pi.getAllTools === "function";
1583
1625
  const compatibleRegisteredToolNames = filterToolsForProvider(
1584
1626
  registeredToolNames,
1585
1627
  event.selectedModelApi,
@@ -1594,6 +1636,7 @@ export function registerHooks(
1594
1636
  event.requestCustomMessages,
1595
1637
  requestRegisteredToolNames,
1596
1638
  guidedUnit?.unitType,
1639
+ hasRegisteredSurface,
1597
1640
  );
1598
1641
  if (requestScoped) {
1599
1642
  recordAutoToolSurfaceSnapshot({
@@ -1614,6 +1657,7 @@ export function registerHooks(
1614
1657
  dash.currentUnit.type === "run-uat" ? aliasFilteredCompatible : providerCompatible,
1615
1658
  dash.currentUnit.type,
1616
1659
  registeredForUnit,
1660
+ hasRegisteredSurface,
1617
1661
  );
1618
1662
  recordAutoToolSurfaceSnapshot({
1619
1663
  source: "provider-adjustment",
@@ -1,9 +1,25 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Shared browser-observable UAT requirement and evidence detection.
3
3
 
4
- 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;
4
+ import { BROWSER_EVIDENCE_SIGNAL_TOOL_NAMES } from "../shared/browser-contract.js";
5
+
6
+ // Alternation fragment over the contract's evidence-signal names, e.g.
7
+ // `browser_(?:assert|batch|...)`. The names are `browser_`-prefixed
8
+ // identifiers (pinned by tests/browser-contract.test.ts), so no escaping is
9
+ // needed.
10
+ const BROWSER_TOOL_SIGNAL = `browser_(?:${
11
+ BROWSER_EVIDENCE_SIGNAL_TOOL_NAMES.map((name) => name.slice("browser_".length)).join("|")
12
+ })`;
13
+
14
+ export const BROWSER_REQUIREMENT_RE = new RegExp(
15
+ 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`,
16
+ "i",
17
+ );
5
18
  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;
6
- export const BROWSER_RUNTIME_RE = /\b(?:browser|playwright|chrome|camoufox|browser_(?:assert|batch|find|verify|snapshot_refs)|screenshot|snapshot|file:\/\/|localhost)\b/i;
19
+ export const BROWSER_RUNTIME_RE = new RegExp(
20
+ String.raw`\b(?:browser|playwright|chrome|camoufox|${BROWSER_TOOL_SIGNAL}|screenshot|snapshot|file://|localhost)\b`,
21
+ "i",
22
+ );
7
23
  export const BROWSER_ACTION_RE = /\b(?:open(?:ed)?|navigate(?:d)?|click(?:ed)?|type(?:d)?|reload(?:ed)?|capture(?:d)?|screenshot|snapshot)\b/i;
8
24
  export const BROWSER_ASSERTION_RE = /\b(?:assert(?:ed|ion)?|observed|confirmed|verified|expected|visible|text|count|label|strikethrough|localstorage|screenshot|snapshot|passed)\b/i;
9
25
  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;
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
3
3
  import { resolve } from "node:path";
4
4
 
5
5
  import type { ClaudeCodeMcpConfig } from "./preferences-types.js";
6
+ import { isGsdBrowserMcpServerConfig } from "../shared/gsd-browser-cli.js";
6
7
  import { toMcpWildcardToolName } from "./mcp-tool-name.js";
7
8
  import { resolveModelMcpConfig } from "./preferences-mcp.js";
8
9
 
@@ -83,34 +84,12 @@ function isWorkflowMcpServerConfig(config: unknown): boolean {
83
84
  return args.some((arg) => arg.includes("gsd-mcp-server") || arg.includes("packages/mcp-server"));
84
85
  }
85
86
 
86
- function isBrowserMcpServerConfig(config: unknown): boolean {
87
- if (!isRecord(config)) return false;
88
- const command = typeof config.command === "string" ? config.command : "";
89
- if (command.includes("gsd-browser") || command.includes("@opengsd/gsd-browser")) {
90
- return true;
91
- }
92
-
93
- const env = config.env;
94
- if (isRecord(env)) {
95
- if (
96
- typeof env.GSD_BROWSER_CLI_PATH === "string"
97
- || typeof env.GSD_BROWSER_BIN_PATH === "string"
98
- || typeof env.GSD_BROWSER_MCP_COMMAND === "string"
99
- ) {
100
- return true;
101
- }
102
- }
103
-
104
- const args = Array.isArray(config.args) ? config.args.filter((arg): arg is string => typeof arg === "string") : [];
105
- return args.some((arg) => arg.includes("gsd-browser") || arg.includes("@opengsd/gsd-browser"));
106
- }
107
-
108
87
  export function discoverWorkflowMcpServerName(projectDir: string): string | undefined {
109
88
  return discoverMcpServers(projectDir).find((server) => isWorkflowMcpServerConfig(server.config))?.name;
110
89
  }
111
90
 
112
91
  export function discoverBrowserMcpServerName(projectDir: string): string | undefined {
113
- return discoverMcpServers(projectDir).find((server) => isBrowserMcpServerConfig(server.config))?.name;
92
+ return discoverMcpServers(projectDir).find((server) => isGsdBrowserMcpServerConfig(server.config))?.name;
114
93
  }
115
94
 
116
95
  export function discoverMcpServerNames(projectDir: string): string[] {
@@ -0,0 +1,39 @@
1
+ import assert from "node:assert/strict";
2
+
3
+ import {
4
+ getUatBrowserToolSupportError,
5
+ hasUatBrowserToolSurface,
6
+ type UatType,
7
+ } from "../uat-policy.ts";
8
+
9
+ export const BROWSER_AUTOMATION_CONTRACT_TOOLS = {
10
+ piProvider: ["read", "browser_navigate"],
11
+ externalMcpClient: ["read", "mcp__gsd-browser__browser_navigate"],
12
+ externalMcpWildcard: ["read", "mcp__gsd-browser__*"],
13
+ otherBrowserMcp: ["read", "mcp__browser-uat__*"],
14
+ workflowOnly: ["read", "mcp__gsd-workflow__*"],
15
+ withoutBrowser: ["read", "gsd_uat_exec"],
16
+ } as const;
17
+
18
+ export function assertBrowserAutomationContractAvailable(tools: readonly string[]): void {
19
+ assert.equal(hasUatBrowserToolSurface(tools), true, `${tools.join(", ")} should satisfy the Browser Automation Contract`);
20
+ }
21
+
22
+ export function assertBrowserAutomationContractMissing(tools: readonly string[] | undefined): void {
23
+ assert.equal(hasUatBrowserToolSurface(tools), false, `${tools?.join(", ") ?? "undefined"} should not satisfy the Browser Automation Contract`);
24
+ }
25
+
26
+ export function assertBrowserBackedUatCanDispatch(options: {
27
+ uatType: UatType;
28
+ activeTools: readonly string[] | undefined;
29
+ registeredTools?: readonly string[];
30
+ }): void {
31
+ assert.equal(
32
+ getUatBrowserToolSupportError({
33
+ ...options,
34
+ milestoneId: "M001",
35
+ sliceId: "S01",
36
+ }),
37
+ null,
38
+ );
39
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ BROWSER_CONTRACT_TOOL_NAMES,
6
+ BROWSER_EVIDENCE_SIGNAL_TOOL_NAMES,
7
+ hasBrowserContractPrefix,
8
+ isBrowserContractToolName,
9
+ } from "../../shared/browser-contract.ts";
10
+ import { isUatBrowserToolName } from "../uat-policy.ts";
11
+ import { BROWSER_REQUIREMENT_RE, BROWSER_RUNTIME_RE } from "../browser-evidence.ts";
12
+
13
+ // Note: RUN_UAT_BROWSER_TOOL_NAMES and MANAGED_GSD_BROWSER_TOOL_NAMES are
14
+ // reference-equal aliases of BROWSER_CONTRACT_TOOL_NAMES, and the managed
15
+ // adapter's spec table is Record-keyed by BrowserContractToolName — both
16
+ // derivations are pinned by the type system, not by runtime assertions here.
17
+ describe("Browser Automation Contract parity", () => {
18
+ it("every contract name satisfies the UAT browser-tool predicate, bare and MCP-prefixed", () => {
19
+ for (const name of BROWSER_CONTRACT_TOOL_NAMES) {
20
+ assert.equal(isUatBrowserToolName(name), true, name);
21
+ assert.equal(isUatBrowserToolName(`mcp__gsd-browser__${name}`), true, `mcp__gsd-browser__${name}`);
22
+ }
23
+ });
24
+
25
+ it("contract names are canonical browser_* names with no duplicates", () => {
26
+ assert.equal(new Set(BROWSER_CONTRACT_TOOL_NAMES).size, BROWSER_CONTRACT_TOOL_NAMES.length);
27
+ for (const name of BROWSER_CONTRACT_TOOL_NAMES) {
28
+ assert.equal(hasBrowserContractPrefix(name), true, name);
29
+ assert.equal(isBrowserContractToolName(name), true, name);
30
+ }
31
+ assert.equal(isBrowserContractToolName("browser_not_a_real_tool"), false);
32
+ assert.equal(hasBrowserContractPrefix("gsd_uat_exec"), false);
33
+ });
34
+
35
+ it("evidence-signal names stay a subset of the contract and drive the detection regexes", () => {
36
+ for (const name of BROWSER_EVIDENCE_SIGNAL_TOOL_NAMES) {
37
+ assert.equal(isBrowserContractToolName(name), true, name);
38
+ // Identifier-shaped names keep the regex splice in browser-evidence.ts escape-free.
39
+ assert.match(name, /^browser_[a-z_]+$/);
40
+ assert.match(`Verified via ${name} call`, BROWSER_REQUIREMENT_RE);
41
+ assert.match(`Verified via ${name} call`, BROWSER_RUNTIME_RE);
42
+ }
43
+ });
44
+ });
@@ -9,6 +9,7 @@ import { join } from "node:path";
9
9
 
10
10
  import { DISPATCH_RULES, type DispatchContext } from "../auto-dispatch.ts";
11
11
  import type { GSDState } from "../types.ts";
12
+ import { BROWSER_AUTOMATION_CONTRACT_TOOLS } from "./browser-automation-contract-fixture.ts";
12
13
 
13
14
  type DispatchRuleEntry = (typeof DISPATCH_RULES)[number];
14
15
 
@@ -80,7 +81,7 @@ test("run-uat browser preflight uses registered tools when the active surface is
80
81
  assert.match(blocked?.action === "stop" ? blocked.reason : "", /run-uat tool surface has none/);
81
82
 
82
83
  const dispatched = await runUatRule().match(makeContext(basePath, {
83
- registeredTools: ["browser_navigate"],
84
+ registeredTools: [...BROWSER_AUTOMATION_CONTRACT_TOOLS.piProvider],
84
85
  }));
85
86
  assert.equal(dispatched?.action, "dispatch");
86
87
  assert.equal(dispatched?.action === "dispatch" ? dispatched.unitType : undefined, "run-uat");
@@ -117,7 +117,8 @@ describe("extension bootstrap isolation (#4168, #4172)", () => {
117
117
  // registration is wrapped in its own try/catch so one failure does not
118
118
  // prevent siblings from loading.
119
119
 
120
- import { registerGsdExtension } from "../bootstrap/register-extension.ts";
120
+ import { CRITICAL_GSD_WORKFLOW_TOOL_NAMES, registerGsdExtension } from "../bootstrap/register-extension.ts";
121
+ import { drainLogs } from "../workflow-logger.ts";
121
122
 
122
123
  describe("registerGsdExtension defensive registration", () => {
123
124
  test("a failing shortcut registration does not prevent kill command registration", async () => {
@@ -161,4 +162,37 @@ describe("registerGsdExtension defensive registration", () => {
161
162
  `registerGsdExtension must NOT register 'gsd' (it is registered separately by index.ts), got ${JSON.stringify(registered)}`,
162
163
  );
163
164
  });
165
+
166
+ test("critical workflow tool list includes lifecycle planning and completion tools", () => {
167
+ assert.ok(CRITICAL_GSD_WORKFLOW_TOOL_NAMES.includes("gsd_plan_slice"));
168
+ assert.ok(CRITICAL_GSD_WORKFLOW_TOOL_NAMES.includes("gsd_slice_complete"));
169
+ assert.ok(CRITICAL_GSD_WORKFLOW_TOOL_NAMES.includes("gsd_validate_milestone"));
170
+ assert.ok(CRITICAL_GSD_WORKFLOW_TOOL_NAMES.includes("gsd_complete_milestone"));
171
+ });
172
+
173
+ test("partial db-tools registration fails visibly when critical tools are missing", () => {
174
+ drainLogs();
175
+ const registeredTools: string[] = [];
176
+ const pi = {
177
+ registerCommand: () => {},
178
+ registerTool: (tool: { name: string }) => {
179
+ if (tool.name === "gsd_plan_slice") {
180
+ throw new Error("simulated db-tools partial registration failure");
181
+ }
182
+ registeredTools.push(tool.name);
183
+ },
184
+ registerHook: () => {},
185
+ registerShortcut: () => {},
186
+ events: { on: () => {}, off: () => {}, emit: () => {} },
187
+ getAllTools: () => registeredTools.map((name) => ({ name })),
188
+ };
189
+
190
+ assert.throws(
191
+ () => registerGsdExtension(pi as any),
192
+ /Critical GSD workflow tool registration failed; missing required tool\(s\): .*gsd_plan_slice/,
193
+ );
194
+ assert.ok(registeredTools.includes("gsd_plan_milestone"));
195
+ assert.ok(!registeredTools.includes("gsd_plan_slice"));
196
+ drainLogs();
197
+ });
164
198
  });
@@ -35,6 +35,7 @@ import {
35
35
  insertTask,
36
36
  openDatabase,
37
37
  } from "../../gsd-db.ts";
38
+ import { createGsdIntegrationProject } from "./gsd-integration-fixture.ts";
38
39
 
39
40
  function run(cmd: string, cwd: string): string {
40
41
  // Safe: all inputs are hardcoded test strings, not user input
@@ -42,17 +43,12 @@ function run(cmd: string, cwd: string): string {
42
43
  }
43
44
 
44
45
  function createTempRepo(): string {
45
- const dir = realpathSync(mkdtempSync(join(tmpdir(), "wt-ms-merge-test-")));
46
- run("git init", dir);
47
- run("git config user.email test@test.com", dir);
48
- run("git config user.name Test", dir);
49
- writeFileSync(join(dir, "README.md"), "# test\n");
50
- mkdirSync(join(dir, ".gsd"), { recursive: true });
51
- writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n");
52
- run("git add .", dir);
53
- run("git commit -m init", dir);
54
- run("git branch -M main", dir);
55
- return dir;
46
+ return createGsdIntegrationProject({
47
+ prefix: "wt-ms-merge-test-",
48
+ initialFiles: {
49
+ ".gsd/STATE.md": "# State\n",
50
+ },
51
+ }).root;
56
52
  }
57
53
 
58
54
  function createTempRepoWithExternalGsd(): { repo: string; externalState: string } {
@@ -11,6 +11,11 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync
11
11
  import { join } from "node:path";
12
12
  import { tmpdir } from "node:os";
13
13
  import { execSync } from "node:child_process";
14
+ import {
15
+ commitAll,
16
+ createGsdIntegrationProject,
17
+ writeGsdMilestoneContext,
18
+ } from "./gsd-integration-fixture.ts";
14
19
 
15
20
  import {
16
21
  createAutoWorktree,
@@ -32,17 +37,12 @@ function run(command: string, cwd: string): string {
32
37
  }
33
38
 
34
39
  function createTempRepo(): string {
35
- const dir = realpathSync(mkdtempSync(join(tmpdir(), "auto-wt-test-")));
36
- run("git init", dir);
37
- run("git config user.email test@test.com", dir);
38
- run("git config user.name Test", dir);
39
- // Create initial commit on main
40
- writeFileSync(join(dir, "README.md"), "# test\n");
41
- run("git add .", dir);
42
- run("git commit -m init", dir);
43
- // Ensure branch is called main
44
- run("git branch -M main", dir);
45
- return dir;
40
+ return createGsdIntegrationProject("auto-wt-test-").root;
41
+ }
42
+
43
+ function commitMilestoneContext(repo: string, milestoneId: string): void {
44
+ writeGsdMilestoneContext(repo, milestoneId);
45
+ commitAll(repo, "add milestone");
46
46
  }
47
47
 
48
48
  describe("auto-worktree lifecycle", () => {
@@ -59,13 +59,7 @@ describe("auto-worktree lifecycle", () => {
59
59
 
60
60
  test("create → detect → teardown", () => {
61
61
  tempDir = createTempRepo();
62
-
63
- // Create .gsd/milestones/M003 with a dummy file (simulates planning artifacts)
64
- const msDir = join(tempDir, ".gsd", "milestones", "M003");
65
- mkdirSync(msDir, { recursive: true });
66
- writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
67
- run("git add .", tempDir);
68
- run("git commit -m \"add milestone\"", tempDir);
62
+ commitMilestoneContext(tempDir, "M003");
69
63
 
70
64
  // ─── createAutoWorktree ──────────────────────────────────────────
71
65
  const wtPath = createAutoWorktree(tempDir, "M003");
@@ -112,11 +106,7 @@ describe("auto-worktree lifecycle", () => {
112
106
 
113
107
  test("re-entry: create again, exit without teardown, re-enter", () => {
114
108
  tempDir = createTempRepo();
115
- const msDir = join(tempDir, ".gsd", "milestones", "M003");
116
- mkdirSync(msDir, { recursive: true });
117
- writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
118
- run("git add .", tempDir);
119
- run("git commit -m \"add milestone\"", tempDir);
109
+ commitMilestoneContext(tempDir, "M003");
120
110
 
121
111
  const wtPath2 = createAutoWorktree(tempDir, "M003");
122
112
  assert.ok(existsSync(wtPath2), "worktree re-created");
@@ -247,11 +237,7 @@ describe("auto-worktree lifecycle", () => {
247
237
 
248
238
  test("coexistence with manual worktree", async () => {
249
239
  tempDir = createTempRepo();
250
- const msDir = join(tempDir, ".gsd", "milestones", "M003");
251
- mkdirSync(msDir, { recursive: true });
252
- writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
253
- run("git add .", tempDir);
254
- run("git commit -m \"add milestone\"", tempDir);
240
+ commitMilestoneContext(tempDir, "M003");
255
241
 
256
242
  // Import createWorktree directly for manual worktree
257
243
  const { createWorktree } = await import("../../worktree-manager.ts");
@@ -274,11 +260,7 @@ describe("auto-worktree lifecycle", () => {
274
260
 
275
261
  test("split-brain prevention: originalBase cleared after teardown", () => {
276
262
  tempDir = createTempRepo();
277
- const msDir = join(tempDir, ".gsd", "milestones", "M003");
278
- mkdirSync(msDir, { recursive: true });
279
- writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
280
- run("git add .", tempDir);
281
- run("git commit -m \"add milestone\"", tempDir);
263
+ commitMilestoneContext(tempDir, "M003");
282
264
 
283
265
  createAutoWorktree(tempDir, "M003");
284
266
  teardownAutoWorktree(tempDir, "M003");
@@ -288,11 +270,7 @@ describe("auto-worktree lifecycle", () => {
288
270
 
289
271
  test("#1526: getMainBranch returns milestone/<MID> in auto-worktree", async () => {
290
272
  tempDir = createTempRepo();
291
- const msDir = join(tempDir, ".gsd", "milestones", "M005");
292
- mkdirSync(msDir, { recursive: true });
293
- writeFileSync(join(msDir, "CONTEXT.md"), "# M005 Context\n");
294
- run("git add .", tempDir);
295
- run("git commit -m \"add milestone\"", tempDir);
273
+ commitMilestoneContext(tempDir, "M005");
296
274
 
297
275
  const { GitServiceImpl } = await import("../../git-service.ts");
298
276
 
@@ -312,11 +290,7 @@ describe("auto-worktree lifecycle", () => {
312
290
 
313
291
  test("#1713: stale worktree directory without .git file", async () => {
314
292
  tempDir = createTempRepo();
315
- const msDir = join(tempDir, ".gsd", "milestones", "M010");
316
- mkdirSync(msDir, { recursive: true });
317
- writeFileSync(join(msDir, "CONTEXT.md"), "# M010 Context\n");
318
- run("git add .", tempDir);
319
- run("git commit -m \"add milestone\"", tempDir);
293
+ commitMilestoneContext(tempDir, "M010");
320
294
 
321
295
  // Simulate a crash leaving a stale directory with no .git file.
322
296
  const { worktreePath } = await import("../../worktree-manager.ts");
@@ -337,11 +311,7 @@ describe("auto-worktree lifecycle", () => {
337
311
 
338
312
  test("#778: re-attach does not reconcile plan checkboxes into a worktree-local .gsd projection", async () => {
339
313
  tempDir = createTempRepo();
340
- const msDir = join(tempDir, ".gsd", "milestones", "M003");
341
- mkdirSync(msDir, { recursive: true });
342
- writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
343
- run("git add .", tempDir);
344
- run("git commit -m \"add milestone\"", tempDir);
314
+ commitMilestoneContext(tempDir, "M003");
345
315
 
346
316
  const planRelPath = join(".gsd", "milestones", "M004", "slices", "S01", "S01-PLAN.md");
347
317
  const planDir = join(tempDir, ".gsd", "milestones", "M004", "slices", "S01");
@@ -401,11 +371,7 @@ describe("auto-worktree lifecycle", () => {
401
371
 
402
372
  test("#2791: mcp.json is not copied into worktree on creation after copyPlanningArtifacts removal", () => {
403
373
  tempDir = createTempRepo();
404
- const msDir = join(tempDir, ".gsd", "milestones", "M003");
405
- mkdirSync(msDir, { recursive: true });
406
- writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
407
- run("git add .", tempDir);
408
- run("git commit -m \"add milestone\"", tempDir);
374
+ commitMilestoneContext(tempDir, "M003");
409
375
 
410
376
  // Create mcp.json in .gsd/ AFTER the commit (untracked, like real usage).
411
377
  // Phase C removed copyPlanningArtifacts, so creation should not seed a
@@ -430,11 +396,7 @@ describe("auto-worktree lifecycle", () => {
430
396
 
431
397
  test("#2791: mcp.json synced via syncGsdStateToWorktree (ROOT_STATE_FILES)", () => {
432
398
  tempDir = createTempRepo();
433
- const msDir = join(tempDir, ".gsd", "milestones", "M003");
434
- mkdirSync(msDir, { recursive: true });
435
- writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
436
- run("git add .", tempDir);
437
- run("git commit -m \"add milestone\"", tempDir);
399
+ commitMilestoneContext(tempDir, "M003");
438
400
 
439
401
  // Create worktree first (no mcp.json yet)
440
402
  const wtPath = createAutoWorktree(tempDir, "M003");
@@ -0,0 +1,80 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+
6
+ export type GsdIntegrationProject = {
7
+ root: string;
8
+ };
9
+
10
+ export type CreateGsdIntegrationProjectOptions = {
11
+ prefix?: string;
12
+ initialFiles?: Record<string, string>;
13
+ };
14
+
15
+ export function projectRoot(project: GsdIntegrationProject | string): string {
16
+ return typeof project === "string" ? project : project.root;
17
+ }
18
+
19
+ export function git(project: GsdIntegrationProject | string, ...args: string[]): string {
20
+ return execFileSync("git", args, {
21
+ cwd: projectRoot(project),
22
+ encoding: "utf-8",
23
+ stdio: ["ignore", "pipe", "pipe"],
24
+ }).trim();
25
+ }
26
+
27
+ export function createGsdIntegrationProject(
28
+ options: CreateGsdIntegrationProjectOptions | string = {},
29
+ ): GsdIntegrationProject {
30
+ const resolvedOptions = typeof options === "string" ? { prefix: options } : options;
31
+ const prefix = resolvedOptions.prefix ?? "gsd-integration-";
32
+ const root = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
33
+
34
+ git(root, "init");
35
+ git(root, "config", "user.email", "test@test.com");
36
+ git(root, "config", "user.name", "Test");
37
+ git(root, "config", "core.autocrlf", "false");
38
+
39
+ writeProjectFile(root, "README.md", "# test\n");
40
+ for (const [relativePath, content] of Object.entries(resolvedOptions.initialFiles ?? {})) {
41
+ writeProjectFile(root, relativePath, content);
42
+ }
43
+ git(root, "add", ".");
44
+ git(root, "commit", "-m", "init");
45
+ git(root, "branch", "-M", "main");
46
+
47
+ return { root };
48
+ }
49
+
50
+ export function writeProjectFile(
51
+ project: GsdIntegrationProject | string,
52
+ relativePath: string,
53
+ content: string,
54
+ ): string {
55
+ const filePath = join(projectRoot(project), relativePath);
56
+ mkdirSync(dirname(filePath), { recursive: true });
57
+ writeFileSync(filePath, content, "utf-8");
58
+ return filePath;
59
+ }
60
+
61
+ export function writeGsdMilestoneContext(
62
+ project: GsdIntegrationProject | string,
63
+ milestoneId: string,
64
+ content = `# ${milestoneId} Context\n`,
65
+ ): string {
66
+ return writeProjectFile(
67
+ project,
68
+ join(".gsd", "milestones", milestoneId, "CONTEXT.md"),
69
+ content,
70
+ );
71
+ }
72
+
73
+ export function commitAll(project: GsdIntegrationProject | string, message: string): void {
74
+ git(project, "add", ".");
75
+ git(project, "commit", "-m", message);
76
+ }
77
+
78
+ export function cleanupGsdIntegrationProject(project: GsdIntegrationProject | string): void {
79
+ rmSync(projectRoot(project), { recursive: true, force: true });
80
+ }