@zigrivers/surface-mcp 0.1.1 → 0.2.0

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.
package/dist/index.js CHANGED
@@ -1,24 +1,286 @@
1
1
  // src/index.ts
2
+ import path from "path";
2
3
  import { pathToFileURL } from "url";
3
4
  import {
4
5
  DEFAULT_COMPOSITION_STAGE_IDS,
5
6
  DEFAULT_SURFACE_CONFIG,
7
+ browserQaFlowRunsForGate,
8
+ browserQaFlowSeverityFromWithFlows,
9
+ browserQaGateTargetCli,
6
10
  createBoundedAlternatives,
7
11
  createTrackedFinding,
8
12
  createSurfaceComposition,
9
- createSurfaceError,
13
+ createSurfaceError as createSurfaceError2,
10
14
  diffTrackedFindings,
11
- err,
15
+ err as err2,
16
+ evaluateGateWithQaFlows,
12
17
  instantiateLensExecutionPlan,
13
- isOk,
14
- ok,
18
+ isOk as isOk2,
19
+ ok as ok2,
15
20
  scoreFinding,
16
21
  selectLensExecutionPlan,
17
22
  synthesizeBacklog,
18
- toMcpError,
23
+ toMcpError as toMcpError2,
19
24
  transitionTrackedFinding
20
25
  } from "@zigrivers/surface-core";
26
+ import { z as z2 } from "zod";
27
+
28
+ // src/browser-qa-tools.ts
29
+ import {
30
+ createSurfaceError,
31
+ err,
32
+ isOk,
33
+ ok,
34
+ toMcpError
35
+ } from "@zigrivers/surface-core";
21
36
  import { z } from "zod";
37
+ var BROWSER_QA_MCP_SERVER_TOOL_NAMES = [
38
+ "surface_qa",
39
+ "surface_explore",
40
+ "surface_flow_run",
41
+ "surface_flow_list",
42
+ "surface_flow_promote",
43
+ "surface_evidence",
44
+ "surface_replay",
45
+ "surface_report_qa",
46
+ "surface_artifact_read"
47
+ ];
48
+ var TargetInputSchema = z.object({
49
+ kind: z.enum(["url", "localhost", "route", "screenshot", "component", "dom"]),
50
+ ref: z.string().min(1),
51
+ theme: z.enum(["light", "dark"]).optional(),
52
+ viewport: z.object({
53
+ height: z.number().int().positive(),
54
+ label: z.enum(["mobile", "tablet", "desktop"]),
55
+ width: z.number().int().positive()
56
+ }).strict().optional()
57
+ }).strict();
58
+ var SurfaceQaInputSchema = z.object({
59
+ actionPolicyRef: z.string().min(1).optional(),
60
+ allowedDomains: z.array(z.string().min(1)).optional(),
61
+ ci: z.boolean().optional(),
62
+ evidence: z.enum(["minimal", "failures", "full"]).optional(),
63
+ explore: z.boolean().optional(),
64
+ flows: z.array(z.string().min(1)).optional(),
65
+ maxActions: z.number().int().positive().optional(),
66
+ maxDepth: z.number().int().positive().optional(),
67
+ maxStates: z.number().int().positive().optional(),
68
+ network: z.enum(["summary", "har", "off"]).optional(),
69
+ scope: z.string().min(1).optional(),
70
+ sessionMode: z.enum(["isolated", "shared"]).optional(),
71
+ stateLockTimeoutMs: z.number().int().positive().optional(),
72
+ target: TargetInputSchema,
73
+ task: z.string().min(1).optional(),
74
+ video: z.enum(["off", "failures", "all"]).optional()
75
+ }).strict();
76
+ var SurfaceExploreInputSchema = z.object({
77
+ actionPolicyRef: z.string().min(1).optional(),
78
+ allowedDomains: z.array(z.string().min(1)).optional(),
79
+ evidence: z.enum(["minimal", "failures", "full"]).optional(),
80
+ maxActions: z.number().int().positive().optional(),
81
+ maxDepth: z.number().int().positive().optional(),
82
+ maxStates: z.number().int().positive().optional(),
83
+ network: z.enum(["summary", "har", "off"]).optional(),
84
+ scope: z.string().min(1).optional(),
85
+ sessionMode: z.enum(["isolated", "shared"]).optional(),
86
+ stateLockTimeoutMs: z.number().int().positive().optional(),
87
+ target: TargetInputSchema,
88
+ task: z.string().min(1).optional(),
89
+ video: z.enum(["off", "failures", "all"]).optional()
90
+ }).strict();
91
+ var SurfaceFlowRunInputSchema = z.object({
92
+ actionPolicyRef: z.string().min(1).optional(),
93
+ baseUrl: z.string().min(1).optional(),
94
+ ci: z.boolean().optional(),
95
+ flowPath: z.string().min(1),
96
+ localhost: z.boolean().optional(),
97
+ target: z.string().min(1).optional(),
98
+ url: z.string().min(1).optional()
99
+ }).strict().refine(
100
+ (input) => [input.target, input.url, input.localhost === true ? "localhost" : void 0].filter(
101
+ (value) => value !== void 0
102
+ ).length <= 1,
103
+ "Pass at most one of target, url, or localhost."
104
+ );
105
+ var SurfaceFlowListInputSchema = z.object({ candidates: z.boolean().optional() }).strict();
106
+ var SurfaceFlowPromoteInputSchema = z.object({
107
+ candidateFlowId: z.string().min(1),
108
+ outPath: z.string().min(1)
109
+ }).strict();
110
+ var SurfaceEvidenceInputSchema = z.object({ refId: z.string().min(1) }).strict();
111
+ var SurfaceReplayInputSchema = z.object({
112
+ promoteOnRepro: z.boolean().optional(),
113
+ refId: z.string().min(1)
114
+ }).strict();
115
+ var SurfaceReportQaInputSchema = z.object({
116
+ format: z.enum(["md", "json", "manifest"]).optional(),
117
+ runId: z.string().min(1)
118
+ }).strict();
119
+ var SurfaceArtifactReadInputSchema = z.object({
120
+ artifactId: z.string().min(1),
121
+ maxBytes: z.number().int().positive().max(65536).optional(),
122
+ refId: z.string().min(1)
123
+ }).strict();
124
+ var BROWSER_QA_MCP_SERVER_INPUT_SCHEMAS = {
125
+ surface_qa: SurfaceQaInputSchema,
126
+ surface_explore: SurfaceExploreInputSchema,
127
+ surface_flow_run: SurfaceFlowRunInputSchema,
128
+ surface_flow_list: SurfaceFlowListInputSchema,
129
+ surface_flow_promote: SurfaceFlowPromoteInputSchema,
130
+ surface_evidence: SurfaceEvidenceInputSchema,
131
+ surface_replay: SurfaceReplayInputSchema,
132
+ surface_report_qa: SurfaceReportQaInputSchema,
133
+ surface_artifact_read: SurfaceArtifactReadInputSchema
134
+ };
135
+ var BROWSER_QA_MCP_TOOL_INPUT_SCHEMAS = BROWSER_QA_MCP_SERVER_INPUT_SCHEMAS;
136
+ var BROWSER_QA_MCP_SERVER_TOOL_METADATA = {
137
+ surface_qa: {
138
+ description: "Run agent-led browser QA over a target.",
139
+ title: "Run Browser QA"
140
+ },
141
+ surface_explore: {
142
+ description: "Run bounded browser QA exploration.",
143
+ title: "Explore Browser QA"
144
+ },
145
+ surface_flow_run: {
146
+ description: "Run a reviewed browser QA flow.",
147
+ title: "Run Browser QA Flow"
148
+ },
149
+ surface_flow_list: {
150
+ description: "List browser QA flow runs or candidate flows.",
151
+ title: "List Browser QA Flows"
152
+ },
153
+ surface_flow_promote: {
154
+ description: "Promote a candidate browser QA flow.",
155
+ title: "Promote Browser QA Flow"
156
+ },
157
+ surface_evidence: {
158
+ description: "Read redacted browser QA evidence metadata.",
159
+ title: "Read Browser QA Evidence"
160
+ },
161
+ surface_replay: {
162
+ description: "Replay a browser QA candidate or finding.",
163
+ title: "Replay Browser QA Finding"
164
+ },
165
+ surface_report_qa: {
166
+ description: "Render a browser QA report.",
167
+ title: "Render Browser QA Report"
168
+ },
169
+ surface_artifact_read: {
170
+ description: "Read a bounded MCP-approved browser QA artifact by registered refs.",
171
+ title: "Read Browser QA Artifact"
172
+ }
173
+ };
174
+ var BROWSER_QA_MCP_SERVER_TOOL_NAME_SET = new Set(BROWSER_QA_MCP_SERVER_TOOL_NAMES);
175
+ var REGISTERED_REF_ID_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_-]*$/u;
176
+ function createBrowserQaMcpHandlers(composition) {
177
+ const browserQa = browserQaFromComposition(composition);
178
+ const orchestrator = browserQa?.orchestrator;
179
+ const flowService = browserQa?.flowService;
180
+ const evidenceStore = browserQa?.evidenceStore;
181
+ return {
182
+ artifactRead: (input) => evidenceStore?.readArtifactByRegisteredRef(input) ?? Promise.resolve(qaMcpUnavailable("Browser QA evidence artifact reads are unavailable.")),
183
+ evidence: (input) => orchestrator?.readEvidence(input) ?? Promise.resolve(qaMcpUnavailable("Browser QA evidence reads are unavailable.")),
184
+ explore: (input) => orchestrator?.runExplore({
185
+ ...input,
186
+ maxActions: input.maxActions ?? 25,
187
+ maxDepth: input.maxDepth ?? 2,
188
+ maxStates: input.maxStates ?? 10
189
+ }) ?? Promise.resolve(qaMcpUnavailable("Browser QA exploration is unavailable.")),
190
+ flowList: (input) => flowService?.listFlows(input) ?? Promise.resolve(qaMcpUnavailable("Browser QA flow listing is unavailable.")),
191
+ flowPromote: (input) => flowService?.promoteFlow(input) ?? Promise.resolve(qaMcpUnavailable("Browser QA flow promotion is unavailable.")),
192
+ flowRun: (input) => flowService?.runFlowFile({
193
+ ...input.actionPolicyRef === void 0 ? {} : { actionPolicyRef: input.actionPolicyRef },
194
+ ...input.ci === void 0 ? {} : { ci: input.ci },
195
+ flowPath: input.flowPath,
196
+ targetCli: targetCliForFlowRun(input)
197
+ }) ?? Promise.resolve(qaMcpUnavailable("Browser QA flow running is unavailable.")),
198
+ qa: (input) => orchestrator?.runQa(input) ?? Promise.resolve(qaMcpUnavailable("Browser QA orchestration is unavailable.")),
199
+ replay: (input) => orchestrator?.replay(input) ?? Promise.resolve(qaMcpUnavailable("Browser QA replay is unavailable.")),
200
+ reportQa: (input) => orchestrator?.reportQa(input) ?? Promise.resolve(qaMcpUnavailable("Browser QA reports are unavailable."))
201
+ };
202
+ }
203
+ function isBrowserQaMcpServerToolName(name) {
204
+ return BROWSER_QA_MCP_SERVER_TOOL_NAME_SET.has(name);
205
+ }
206
+ async function callBrowserQaMcpTool(input) {
207
+ const parsed = parseBrowserQaToolInput(input.name, input.input);
208
+ if (!parsed.ok) {
209
+ return parsed;
210
+ }
211
+ switch (input.name) {
212
+ case "surface_qa":
213
+ return await input.handlers.qa(parsed.value);
214
+ case "surface_explore":
215
+ return await input.handlers.explore(parsed.value);
216
+ case "surface_flow_run":
217
+ return await input.handlers.flowRun(parsed.value);
218
+ case "surface_flow_list":
219
+ return await input.handlers.flowList(parsed.value);
220
+ case "surface_flow_promote":
221
+ return await input.handlers.flowPromote(parsed.value);
222
+ case "surface_evidence":
223
+ return await input.handlers.evidence(parsed.value);
224
+ case "surface_replay":
225
+ return await input.handlers.replay(parsed.value);
226
+ case "surface_report_qa":
227
+ return await input.handlers.reportQa(parsed.value);
228
+ case "surface_artifact_read":
229
+ return await callArtifactReadHandler(
230
+ input.handlers,
231
+ parsed.value
232
+ );
233
+ }
234
+ }
235
+ function parseBrowserQaToolInput(name, input) {
236
+ const parsed = BROWSER_QA_MCP_TOOL_INPUT_SCHEMAS[name].safeParse(input);
237
+ if (!parsed.success) {
238
+ return err(
239
+ createSurfaceError("config_invalid", "Browser QA MCP input did not match its schema.", {
240
+ cause: parsed.error,
241
+ details: { tool: name }
242
+ })
243
+ );
244
+ }
245
+ return ok(parsed.data);
246
+ }
247
+ async function callArtifactReadHandler(handlers, input) {
248
+ const refCheck = validateRegisteredRef(input.refId, "refId");
249
+ if (!refCheck.ok) {
250
+ return refCheck;
251
+ }
252
+ const artifactCheck = validateRegisteredRef(input.artifactId, "artifactId");
253
+ if (!artifactCheck.ok) {
254
+ return artifactCheck;
255
+ }
256
+ return await handlers.artifactRead({ ...input, maxBytes: input.maxBytes ?? 8192 });
257
+ }
258
+ function validateRegisteredRef(value, field) {
259
+ if (!REGISTERED_REF_ID_PATTERN.test(value) || value.includes("..")) {
260
+ return err(
261
+ createSurfaceError("config_invalid", "Browser QA artifact reads require registered ids.", {
262
+ details: { field }
263
+ })
264
+ );
265
+ }
266
+ return ok(true);
267
+ }
268
+ function browserQaFromComposition(composition) {
269
+ return composition.browserQa;
270
+ }
271
+ function targetCliForFlowRun(input) {
272
+ return {
273
+ ...input.baseUrl === void 0 ? {} : { baseUrl: input.baseUrl },
274
+ ...input.localhost === true ? { localhost: true } : {},
275
+ ...input.target === void 0 ? {} : { target: input.target },
276
+ ...input.url === void 0 ? {} : { url: input.url }
277
+ };
278
+ }
279
+ function qaMcpUnavailable(message) {
280
+ return err(createSurfaceError("qa_unavailable", message));
281
+ }
282
+
283
+ // src/index.ts
22
284
  var SURFACE_MCP_SERVER_NAME = "surface";
23
285
  var SURFACE_MCP_SERVER_VERSION = "1.0.0";
24
286
  var SURFACE_MCP_TOOL_SCHEMA_VERSION = "1.0.0";
@@ -36,62 +298,77 @@ var TOOL_ORDER = [
36
298
  "surface_trace",
37
299
  "surface_run",
38
300
  "surface_next",
39
- "surface_status"
301
+ "surface_status",
302
+ ...BROWSER_QA_MCP_SERVER_TOOL_NAMES
40
303
  ];
41
- var TargetSchema = z.object({
42
- kind: z.enum(["url", "localhost", "route", "screenshot", "component", "dom"]),
43
- ref: z.string().min(1),
44
- theme: z.enum(["light", "dark"]).optional(),
45
- viewport: z.object({
46
- height: z.number().int().positive(),
47
- label: z.enum(["mobile", "tablet", "desktop"]),
48
- width: z.number().int().positive()
304
+ var TargetSchema = z2.object({
305
+ kind: z2.enum(["url", "localhost", "route", "screenshot", "component", "dom"]),
306
+ ref: z2.string().min(1),
307
+ theme: z2.enum(["light", "dark"]).optional(),
308
+ viewport: z2.object({
309
+ height: z2.number().int().positive(),
310
+ label: z2.enum(["mobile", "tablet", "desktop"]),
311
+ width: z2.number().int().positive()
49
312
  }).strict().optional()
50
313
  }).strict();
51
- var AuthStateRefSchema = z.string().min(1);
52
- var RunRefSchema = z.object({ runId: z.string().min(1) }).strict();
53
- var GatePolicyInputSchema = z.record(z.string(), z.unknown());
314
+ var AuthStateRefSchema = z2.string().min(1);
315
+ var RunRefSchema = z2.object({ runId: z2.string().min(1) }).strict();
316
+ var GatePolicyInputSchema = z2.record(z2.string(), z2.unknown());
54
317
  var TOOL_INPUT_SCHEMAS = {
55
- surface_capture: z.object({
318
+ surface_capture: z2.object({
56
319
  authState: AuthStateRefSchema.optional(),
57
320
  target: TargetSchema
58
321
  }).strict(),
59
- surface_audit: z.object({
322
+ surface_audit: z2.object({
60
323
  authState: AuthStateRefSchema.optional(),
61
- depth: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)]).optional(),
62
- persona: z.string().min(1).optional(),
63
- preset: z.string().min(1).optional(),
324
+ depth: z2.union([z2.literal(1), z2.literal(2), z2.literal(3), z2.literal(4), z2.literal(5)]).optional(),
325
+ persona: z2.string().min(1).optional(),
326
+ preset: z2.string().min(1).optional(),
64
327
  target: TargetSchema,
65
- task: z.string().min(1).optional()
328
+ task: z2.string().min(1).optional()
66
329
  }).strict(),
67
- surface_explain: z.object({ findingId: z.string().min(1) }).strict(),
68
- surface_backlog: z.object({
69
- exportTarget: z.string().min(1).optional(),
70
- runId: z.string().min(1).optional()
330
+ surface_explain: z2.object({ findingId: z2.string().min(1) }).strict(),
331
+ surface_backlog: z2.object({
332
+ exportTarget: z2.string().min(1).optional(),
333
+ runId: z2.string().min(1).optional()
71
334
  }).strict(),
72
- surface_gate: z.object({
335
+ surface_gate: z2.object({
336
+ actionPolicyRef: z2.string().min(1).optional(),
337
+ baseUrl: z2.string().min(1).optional(),
338
+ ci: z2.boolean().optional(),
339
+ localhost: z2.boolean().optional(),
73
340
  policy: GatePolicyInputSchema.optional(),
74
- runId: z.string().min(1).optional()
341
+ runId: z2.string().min(1).optional(),
342
+ target: z2.string().min(1).optional(),
343
+ url: z2.string().min(1).optional(),
344
+ withFlows: z2.union([z2.boolean(), z2.string().min(1)]).optional()
345
+ }).strict().refine(
346
+ (input) => [input.target, input.url, input.localhost === true ? "localhost" : void 0].filter(
347
+ (value) => value !== void 0
348
+ ).length <= 1,
349
+ "Pass at most one of target, url, or localhost."
350
+ ),
351
+ surface_validate: z2.object({ runId: z2.string().min(1) }).strict(),
352
+ surface_baseline: z2.object({ reason: z2.string().min(1).optional() }).strict(),
353
+ surface_verdict: z2.object({
354
+ decision: z2.enum(["accept", "reject", "correct", "defer"]),
355
+ findingId: z2.string().min(1),
356
+ promote: z2.boolean().optional(),
357
+ rationale: z2.string().min(1)
75
358
  }).strict(),
76
- surface_validate: z.object({ runId: z.string().min(1) }).strict(),
77
- surface_baseline: z.object({ reason: z.string().min(1).optional() }).strict(),
78
- surface_verdict: z.object({
79
- decision: z.enum(["accept", "reject", "correct", "defer"]),
80
- findingId: z.string().min(1),
81
- rationale: z.string().min(1)
82
- }).strict(),
83
- surface_diff: z.object({ after: RunRefSchema, before: RunRefSchema }).strict(),
84
- surface_alternatives: z.object({
359
+ surface_diff: z2.object({ after: RunRefSchema, before: RunRefSchema }).strict(),
360
+ surface_alternatives: z2.object({
85
361
  authState: AuthStateRefSchema.optional(),
86
362
  target: TargetSchema
87
363
  }).strict(),
88
- surface_trace: z.object({ findingId: z.string().min(1) }).strict(),
89
- surface_run: z.object({
90
- step: z.string().min(1),
364
+ surface_trace: z2.object({ findingId: z2.string().min(1) }).strict(),
365
+ surface_run: z2.object({
366
+ step: z2.string().min(1),
91
367
  target: TargetSchema.optional()
92
368
  }).strict(),
93
- surface_next: z.object({}).strict(),
94
- surface_status: z.object({}).strict()
369
+ surface_next: z2.object({}).strict(),
370
+ surface_status: z2.object({}).strict(),
371
+ ...BROWSER_QA_MCP_SERVER_INPUT_SCHEMAS
95
372
  };
96
373
  var TOOL_METADATA = {
97
374
  surface_capture: {
@@ -149,7 +426,8 @@ var TOOL_METADATA = {
149
426
  surface_status: {
150
427
  title: "Read Status",
151
428
  description: "Read Surface project status."
152
- }
429
+ },
430
+ ...BROWSER_QA_MCP_SERVER_TOOL_METADATA
153
431
  };
154
432
  var INTERNAL_TOOLS = TOOL_ORDER.map((name) => {
155
433
  const metadata = TOOL_METADATA[name];
@@ -158,7 +436,7 @@ var INTERNAL_TOOLS = TOOL_ORDER.map((name) => {
158
436
  name,
159
437
  ...metadata,
160
438
  inputZodSchema,
161
- inputSchema: z.toJSONSchema(inputZodSchema),
439
+ inputSchema: z2.toJSONSchema(inputZodSchema),
162
440
  schemaVersion: SURFACE_MCP_TOOL_SCHEMA_VERSION
163
441
  };
164
442
  });
@@ -180,8 +458,9 @@ function createSurfaceMcpToolRegistry() {
180
458
  }
181
459
  function createSurfaceMcpServer(options = {}) {
182
460
  const registry = createSurfaceMcpToolRegistry();
461
+ const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
183
462
  const composition = options.composition ?? createSurfaceComposition(options);
184
- const session = createSurfaceMcpSessionState();
463
+ const session = createSurfaceMcpSessionState(projectRoot);
185
464
  return {
186
465
  composition,
187
466
  callTool: async (name, input) => {
@@ -226,14 +505,15 @@ function assertMcpToolSchemaCompatibility(input) {
226
505
  );
227
506
  }
228
507
  }
229
- return ok(true);
508
+ return ok2(true);
230
509
  }
231
- function createSurfaceMcpSessionState() {
510
+ function createSurfaceMcpSessionState(projectRoot) {
232
511
  return {
233
512
  baselines: /* @__PURE__ */ new Map(),
234
513
  baselineOrder: [],
235
514
  runs: /* @__PURE__ */ new Map(),
236
515
  runOrder: [],
516
+ projectRoot,
237
517
  trackedByIdentity: /* @__PURE__ */ new Map(),
238
518
  verdicts: /* @__PURE__ */ new Map(),
239
519
  nextBaselineSequence: 1,
@@ -246,7 +526,7 @@ async function hydrateSurfaceMcpSession(composition, session) {
246
526
  return state;
247
527
  }
248
528
  resetSurfaceMcpSessionFromState(session, state.value);
249
- return ok(void 0);
529
+ return ok2(void 0);
250
530
  }
251
531
  function resetSurfaceMcpSessionFromState(session, state) {
252
532
  session.baselines.clear();
@@ -310,14 +590,14 @@ async function persistSurfaceMcpSession(composition, session) {
310
590
  const updateState = stateSnapshotForMcpSession(session);
311
591
  if (composition.stateStore.updateState !== void 0) {
312
592
  const updated = await composition.stateStore.updateState(updateState);
313
- return updated.ok ? ok(void 0) : updated;
593
+ return updated.ok ? ok2(void 0) : updated;
314
594
  }
315
595
  const current = await composition.stateStore.readState();
316
596
  if (!current.ok) {
317
597
  return current;
318
598
  }
319
599
  const written = await composition.stateStore.writeState(updateState(current.value));
320
- return written.ok ? ok(void 0) : written;
600
+ return written.ok ? ok2(void 0) : written;
321
601
  }
322
602
  function stateSnapshotForMcpSession(session) {
323
603
  return (state) => {
@@ -372,6 +652,13 @@ function nextSequenceFromIds(ids, prefix) {
372
652
  return maxSequence + 1;
373
653
  }
374
654
  async function callSurfaceMcpTool(input) {
655
+ if (isBrowserQaMcpServerToolName(input.name)) {
656
+ return await callBrowserQaMcpTool({
657
+ handlers: createBrowserQaMcpHandlers(input.composition),
658
+ input: input.input,
659
+ name: input.name
660
+ });
661
+ }
375
662
  switch (input.name) {
376
663
  case "surface_capture":
377
664
  return await callSurfaceCapture(input.composition, input.input);
@@ -465,7 +752,7 @@ async function callSurfaceAudit(composition, session, rawInput) {
465
752
  if (!persisted.ok) {
466
753
  return persisted;
467
754
  }
468
- return ok({
755
+ return ok2({
469
756
  backlog: record.backlog,
470
757
  capture: record.capture,
471
758
  findings: record.findings,
@@ -480,20 +767,20 @@ function callSurfaceExplain(session, rawInput) {
480
767
  }
481
768
  const finding = findStoredFinding(session, parsed.value.findingId);
482
769
  if (finding === void 0) {
483
- return err(
484
- createSurfaceError("finding_not_found", "No stored MCP finding matched the requested id.", {
770
+ return err2(
771
+ createSurfaceError2("finding_not_found", "No stored MCP finding matched the requested id.", {
485
772
  details: { findingId: parsed.value.findingId }
486
773
  })
487
774
  );
488
775
  }
489
776
  if (finding.evidence.length === 0) {
490
- return err(
491
- createSurfaceError("evidence_missing", "Stored MCP finding has no evidence to explain.", {
777
+ return err2(
778
+ createSurfaceError2("evidence_missing", "Stored MCP finding has no evidence to explain.", {
492
779
  details: { findingId: finding.id }
493
780
  })
494
781
  );
495
782
  }
496
- return ok({
783
+ return ok2({
497
784
  evidence: finding.evidence,
498
785
  finding,
499
786
  rationale: finding.rationale
@@ -506,21 +793,21 @@ async function callSurfaceBacklog(composition, session, rawInput) {
506
793
  }
507
794
  const record = runRecordFor(session, parsed.value.runId);
508
795
  if (record === void 0) {
509
- return err(
510
- createSurfaceError("run_not_found", "No stored MCP run matched the requested backlog.", {
796
+ return err2(
797
+ createSurfaceError2("run_not_found", "No stored MCP run matched the requested backlog.", {
511
798
  details: { runId: parsed.value.runId ?? null }
512
799
  })
513
800
  );
514
801
  }
515
802
  if (parsed.value.exportTarget === void 0) {
516
- return ok(record.backlog);
803
+ return ok2(record.backlog);
517
804
  }
518
805
  const exporter = composition.issueExporters.find(
519
806
  (candidate) => candidate.target === parsed.value.exportTarget
520
807
  );
521
808
  if (exporter === void 0) {
522
- return err(
523
- createSurfaceError(
809
+ return err2(
810
+ createSurfaceError2(
524
811
  "unknown_export_target",
525
812
  "No issue exporter matched the MCP backlog target.",
526
813
  {
@@ -545,13 +832,13 @@ async function callSurfaceBacklog(composition, session, rawInput) {
545
832
  return exported;
546
833
  }
547
834
  if (exported.value.status === "partial") {
548
- return err(
549
- createSurfaceError("export_partial", "MCP backlog export completed partially.", {
835
+ return err2(
836
+ createSurfaceError2("export_partial", "MCP backlog export completed partially.", {
550
837
  details: { exportId: exported.value.id, target: exported.value.target }
551
838
  })
552
839
  );
553
840
  }
554
- return ok(exported.value);
841
+ return ok2(exported.value);
555
842
  }
556
843
  async function callSurfaceGate(composition, session, rawInput) {
557
844
  const parsed = parseToolInput("surface_gate", rawInput);
@@ -560,8 +847,8 @@ async function callSurfaceGate(composition, session, rawInput) {
560
847
  }
561
848
  const record = runRecordFor(session, parsed.value.runId);
562
849
  if (record === void 0) {
563
- return err(
564
- createSurfaceError("run_not_found", "No stored MCP run matched the requested gate.", {
850
+ return err2(
851
+ createSurfaceError2("run_not_found", "No stored MCP run matched the requested gate.", {
565
852
  details: { runId: parsed.value.runId ?? null }
566
853
  })
567
854
  );
@@ -573,7 +860,32 @@ async function callSurfaceGate(composition, session, rawInput) {
573
860
  return tracked === void 0 || !baselineIdentityKeys.has(tracked.identityKey);
574
861
  });
575
862
  const policy = parsed.value.policy === void 0 ? DEFAULT_SURFACE_CONFIG.reporting.gatePolicy : parsed.value.policy;
576
- return await composition.gateEvaluator.evaluate(findings, policy);
863
+ if (parsed.value.withFlows === void 0) {
864
+ return await composition.gateEvaluator.evaluate(findings, policy);
865
+ }
866
+ const targetCli = browserQaGateTargetCli(parsed.value);
867
+ if (!targetCli.ok) {
868
+ return targetCli;
869
+ }
870
+ const flowRuns = await browserQaFlowRunsForGate(composition, {
871
+ ...parsed.value.actionPolicyRef === void 0 ? {} : { actionPolicyRef: parsed.value.actionPolicyRef },
872
+ ci: parsed.value.ci === true,
873
+ projectRoot: session.projectRoot,
874
+ ...targetCli.value === void 0 ? {} : { targetCli: targetCli.value },
875
+ withFlows: parsed.value.withFlows
876
+ });
877
+ if (!flowRuns.ok) {
878
+ return flowRuns;
879
+ }
880
+ const flowAwareGate = evaluateGateWithQaFlows({
881
+ findings,
882
+ policy: {
883
+ ...policy,
884
+ failOnFlowSeverityAtOrAbove: browserQaFlowSeverityFromWithFlows(parsed.value.withFlows)
885
+ },
886
+ qaFlowRuns: flowRuns.value
887
+ });
888
+ return flowAwareGate;
577
889
  }
578
890
  function callSurfaceValidate(session, rawInput) {
579
891
  const parsed = parseToolInput("surface_validate", rawInput);
@@ -582,13 +894,13 @@ function callSurfaceValidate(session, rawInput) {
582
894
  }
583
895
  const record = runRecordFor(session, parsed.value.runId);
584
896
  if (record === void 0) {
585
- return err(
586
- createSurfaceError("run_not_found", "No stored MCP run matched the requested validation.", {
897
+ return err2(
898
+ createSurfaceError2("run_not_found", "No stored MCP run matched the requested validation.", {
587
899
  details: { runId: parsed.value.runId }
588
900
  })
589
901
  );
590
902
  }
591
- return ok({
903
+ return ok2({
592
904
  checks: record.trackedFindings.map((trackedFinding) => ({
593
905
  id: trackedFinding.identityKey,
594
906
  passed: trackedFinding.status !== "identity-broken",
@@ -604,8 +916,8 @@ async function callSurfaceBaseline(composition, session, rawInput) {
604
916
  }
605
917
  const record = runRecordFor(session, void 0);
606
918
  if (record === void 0 || record.trackedFindings.length === 0) {
607
- return err(
608
- createSurfaceError("no_findings_to_baseline", "No MCP findings are available to baseline.")
919
+ return err2(
920
+ createSurfaceError2("no_findings_to_baseline", "No MCP findings are available to baseline.")
609
921
  );
610
922
  }
611
923
  const baselineId = nextBaselineId(session);
@@ -622,7 +934,7 @@ async function callSurfaceBaseline(composition, session, rawInput) {
622
934
  if (!persisted.ok) {
623
935
  return persisted;
624
936
  }
625
- return ok({
937
+ return ok2({
626
938
  baselineId,
627
939
  count: baseline.identityKeys.size,
628
940
  ...baseline.reason === void 0 ? {} : { reason: baseline.reason }
@@ -633,10 +945,33 @@ async function callSurfaceVerdict(composition, session, rawInput) {
633
945
  if (!parsed.ok) {
634
946
  return parsed;
635
947
  }
948
+ if (parsed.value.findingId.startsWith("qfc_")) {
949
+ if (parsed.value.promote !== true) {
950
+ return err2(
951
+ createSurfaceError2("finding_not_found", "Browser QA candidate verdicts require promote.", {
952
+ details: { findingId: parsed.value.findingId }
953
+ })
954
+ );
955
+ }
956
+ const promotion = await composition.browserQa.orchestrator.promoteCandidateByVerdict({
957
+ refId: parsed.value.findingId,
958
+ reason: parsed.value.rationale,
959
+ verdictId: `verdict_${Date.now().toString(36)}`
960
+ });
961
+ if (!promotion.ok) {
962
+ return promotion;
963
+ }
964
+ return ok2({
965
+ decision: parsed.value.decision,
966
+ findingId: parsed.value.findingId,
967
+ promotion: promotion.value,
968
+ rationale: parsed.value.rationale
969
+ });
970
+ }
636
971
  const finding = findStoredFinding(session, parsed.value.findingId);
637
972
  if (finding === void 0) {
638
- return err(
639
- createSurfaceError("finding_not_found", "No stored MCP finding matched the verdict.", {
973
+ return err2(
974
+ createSurfaceError2("finding_not_found", "No stored MCP finding matched the verdict.", {
640
975
  details: { findingId: parsed.value.findingId }
641
976
  })
642
977
  );
@@ -651,7 +986,7 @@ async function callSurfaceVerdict(composition, session, rawInput) {
651
986
  if (!persisted.ok) {
652
987
  return persisted;
653
988
  }
654
- return ok(verdict);
989
+ return ok2(verdict);
655
990
  }
656
991
  function callSurfaceDiff(session, rawInput) {
657
992
  const parsed = parseToolInput("surface_diff", rawInput);
@@ -661,13 +996,13 @@ function callSurfaceDiff(session, rawInput) {
661
996
  const before = runRecordFor(session, parsed.value.before.runId);
662
997
  const after = runRecordFor(session, parsed.value.after.runId);
663
998
  if (before === void 0 || after === void 0) {
664
- return err(
665
- createSurfaceError("run_not_found", "Both MCP diff runs must exist.", {
999
+ return err2(
1000
+ createSurfaceError2("run_not_found", "Both MCP diff runs must exist.", {
666
1001
  details: { after: parsed.value.after.runId, before: parsed.value.before.runId }
667
1002
  })
668
1003
  );
669
1004
  }
670
- return ok(diffTrackedFindings(before.trackedFindings, after.trackedFindings));
1005
+ return ok2(diffTrackedFindings(before.trackedFindings, after.trackedFindings));
671
1006
  }
672
1007
  async function callSurfaceAlternatives(composition, rawInput) {
673
1008
  const parsed = parseToolInput("surface_alternatives", rawInput);
@@ -682,7 +1017,7 @@ async function callSurfaceAlternatives(composition, rawInput) {
682
1017
  if (!capture.ok) {
683
1018
  return capture;
684
1019
  }
685
- return ok({
1020
+ return ok2({
686
1021
  alternatives: createBoundedAlternatives(target)
687
1022
  });
688
1023
  }
@@ -693,13 +1028,13 @@ function callSurfaceTrace(session, rawInput) {
693
1028
  }
694
1029
  const trackedFinding = findStoredTrackedFinding(session, parsed.value.findingId);
695
1030
  if (trackedFinding === void 0) {
696
- return err(
697
- createSurfaceError("finding_not_found", "No tracked MCP finding matched the requested id.", {
1031
+ return err2(
1032
+ createSurfaceError2("finding_not_found", "No tracked MCP finding matched the requested id.", {
698
1033
  details: { findingId: parsed.value.findingId }
699
1034
  })
700
1035
  );
701
1036
  }
702
- return ok({ trackedFinding });
1037
+ return ok2({ trackedFinding });
703
1038
  }
704
1039
  async function callSurfaceRun(composition, rawInput) {
705
1040
  const parsed = parseToolInput("surface_run", rawInput);
@@ -707,8 +1042,8 @@ async function callSurfaceRun(composition, rawInput) {
707
1042
  return parsed;
708
1043
  }
709
1044
  if (parsed.value.step !== "all" && !isExecutableStageId(parsed.value.step)) {
710
- return err(
711
- createSurfaceError("unknown_step", `Unknown MCP pipeline step "${parsed.value.step}".`, {
1045
+ return err2(
1046
+ createSurfaceError2("unknown_step", `Unknown MCP pipeline step "${parsed.value.step}".`, {
712
1047
  details: { step: parsed.value.step }
713
1048
  })
714
1049
  );
@@ -719,14 +1054,14 @@ async function callSurfaceRun(composition, rawInput) {
719
1054
  runId
720
1055
  });
721
1056
  if (!run.ok) {
722
- return err(
723
- createSurfaceError("step_failed", `MCP pipeline run ${runId} failed.`, {
1057
+ return err2(
1058
+ createSurfaceError2("step_failed", `MCP pipeline run ${runId} failed.`, {
724
1059
  cause: run.error,
725
1060
  details: { runId, stage: parsed.value.step }
726
1061
  })
727
1062
  );
728
1063
  }
729
- return ok({
1064
+ return ok2({
730
1065
  runId: run.value.runId,
731
1066
  stage: parsed.value.step,
732
1067
  status: "completed"
@@ -739,7 +1074,7 @@ async function callSurfaceNext(composition) {
739
1074
  }
740
1075
  const lastCompletedStage = state.value.pipeline?.lastCompletedStage;
741
1076
  const eligible = lastCompletedStage === void 0 ? ["run discovery", "run all"] : eligibleStagesAfter(lastCompletedStage).map((stage) => `run ${stage}`);
742
- return ok({ eligible });
1077
+ return ok2({ eligible });
743
1078
  }
744
1079
  function callSurfaceStatus(session) {
745
1080
  const runHistory = session.runOrder.map((runId) => session.runs.get(runId)).filter((record) => record !== void 0).map((record) => ({
@@ -750,7 +1085,7 @@ function callSurfaceStatus(session) {
750
1085
  }));
751
1086
  const completedRuns = runHistory.filter((entry) => entry.status === "completed").length;
752
1087
  const failedRuns = runHistory.filter((entry) => entry.status === "failed").length;
753
- return ok({
1088
+ return ok2({
754
1089
  currentStage: runHistory.at(-1)?.status === "completed" ? "completed" : "pending",
755
1090
  progress: {
756
1091
  completedRuns,
@@ -771,7 +1106,7 @@ async function groundingEvidenceFor(composition, capture) {
771
1106
  evidence.push(...toolResult.evidence);
772
1107
  }
773
1108
  }
774
- return ok(evidence);
1109
+ return ok2(evidence);
775
1110
  }
776
1111
  async function findingsForPlan(composition, config, capture, evidence, plan) {
777
1112
  const findings = [];
@@ -794,7 +1129,7 @@ async function findingsForPlan(composition, config, capture, evidence, plan) {
794
1129
  findings.push(scored.value);
795
1130
  }
796
1131
  }
797
- return ok(findings);
1132
+ return ok2(findings);
798
1133
  }
799
1134
  function configForAuditInput(input) {
800
1135
  return {
@@ -832,14 +1167,14 @@ function targetForCore(input) {
832
1167
  function parseToolInput(name, input) {
833
1168
  const parsed = TOOL_INPUT_SCHEMAS[name].safeParse(input);
834
1169
  if (!parsed.success) {
835
- return err(
836
- createSurfaceError("config_invalid", "MCP tool input did not match the registered schema.", {
1170
+ return err2(
1171
+ createSurfaceError2("config_invalid", "MCP tool input did not match the registered schema.", {
837
1172
  cause: parsed.error,
838
1173
  details: { tool: name }
839
1174
  })
840
1175
  );
841
1176
  }
842
- return ok(parsed.data);
1177
+ return ok2(parsed.data);
843
1178
  }
844
1179
  function nextRunId(session) {
845
1180
  const runId = `run_mcp_${session.nextRunSequence.toString().padStart(4, "0")}`;
@@ -957,7 +1292,7 @@ async function runSurfaceMcpStdioServer(options = {}) {
957
1292
  await server.connect(new StdioServerTransport());
958
1293
  }
959
1294
  function mcpToolCallResult(result) {
960
- if (isOk(result)) {
1295
+ if (isOk2(result)) {
961
1296
  return {
962
1297
  content: [{ text: JSON.stringify(result.value, null, 2), type: "text" }],
963
1298
  structuredContent: result.value
@@ -966,7 +1301,7 @@ function mcpToolCallResult(result) {
966
1301
  return {
967
1302
  content: [{ text: result.error.message, type: "text" }],
968
1303
  isError: true,
969
- structuredContent: toMcpError(result.error)
1304
+ structuredContent: toMcpError2(result.error)
970
1305
  };
971
1306
  }
972
1307
  function publicToolDefinition(tool) {
@@ -997,8 +1332,8 @@ function majorVersion(version) {
997
1332
  return Number.isInteger(major) ? major : void 0;
998
1333
  }
999
1334
  function mcpSchemaIncompatible(message, input, details = {}) {
1000
- return err(
1001
- createSurfaceError("mcp_schema_incompatible", message, {
1335
+ return err2(
1336
+ createSurfaceError2("mcp_schema_incompatible", message, {
1002
1337
  details: {
1003
1338
  current: {
1004
1339
  name: input.current.name,