@vostride/agent-qa-core 0.2.2 → 0.2.4

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
@@ -122,7 +122,9 @@ var NamedLLMConfigSchema = ModelConfigBaseSchema.extend({
122
122
  var TimeoutConfigSchema = z2.object({
123
123
  step: DurationString,
124
124
  test: DurationString,
125
- navigation: DurationString
125
+ navigation: DurationString.describe(
126
+ "How long a web page load or URL wait can take. Default 2m. Android and iOS runs do not use it."
127
+ )
126
128
  }).strict();
127
129
  var BrowserConfigSchema = z2.object({
128
130
  name: z2.enum(["chromium", "firefox", "webkit"]),
@@ -132,7 +134,6 @@ var BrowserConfigSchema = z2.object({
132
134
  height: z2.number()
133
135
  }).strict().optional()
134
136
  }).strict();
135
- var TransportSchema = z2.enum(["local", "browserstack"]);
136
137
  var PlannerConfigSchema = z2.object({
137
138
  maxActions: z2.number().int().positive().optional(),
138
139
  maxPlanningTurns: z2.number().int().positive().optional(),
@@ -156,69 +157,28 @@ var DashboardConfigSchema = z3.object({
156
157
  port: z3.number().optional(),
157
158
  dbPath: z3.string().optional(),
158
159
  artifactsDir: z3.string().optional(),
159
- // Phase 387, D-10: where an entity card's `url` points. BOTH optional, so the v52 default
160
- // is a card with no `url` at all rather than a link to a route nothing serves. The card
161
- // producer reads these through the masked config reader and renders
162
- // `ENTITY_URL_ROUTE` (@vostride/agent-qa-schema) against them.
163
- //
164
- // They live on the EXISTING `services.dashboard` block rather than on a new top-level
165
- // `dashboard` key: this project already has one dashboard configuration home, and a second
166
- // one would be two places an operator could reasonably write the same fact.
167
- //
168
- // 2026-09-01 (Phase 408 plan 10, WR-04 — T-408-16): REFINED TO AN ABSOLUTE http(s) URL, because
169
- // this value stops being a display string in M5. It composes the tokenless live link
170
- // (`live-link.ts`'s `liveRunLinkOf`, which only strips trailing slashes), and that link is
171
- // assigned to an `href` INSIDE the served App document — which runs with `allow-scripts` at an
172
- // opaque origin — as well as appended verbatim to every carrier tool result's model-readable
173
- // text. A `javascript:` base therefore composed a script-executing anchor. The trust boundary is
174
- // the operator, so this is not remote-triggerable; the finding is that a URL-shaped value reached
175
- // an `href` with no scheme check at any hop, and the guard costs one refinement.
176
- //
177
- // AT THE PARSE BOUNDARY, so there is exactly one place that decides. The defensive drop at the
178
- // anchor itself is the SECOND half of the same finding and lives in `app-wrapper.ts`.
179
- //
180
- // STILL `.optional()`: an absent base is a real and honest posture — a standalone `agent-qa mcp`
181
- // with no dashboard — and the carrier result names that absence rather than fabricating a URL.
182
- // An EMPTY STRING is now REFUSED, which is stricter than before: `""` is not an absence an
183
- // operator wrote by leaving the key out, it is a value that composes a relative fragment reading
184
- // as a link and resolving against nothing.
160
+ // Only an absolute http(s) URL: the base composes an `href` inside the served App document.
185
161
  baseUrl: z3.string().refine(
186
162
  isAbsoluteHttpUrl,
187
163
  { message: "services.dashboard.baseUrl must be an absolute http(s) URL" }
188
164
  ).optional(),
189
165
  workspace: z3.string().optional(),
190
- // 2026-10-01 (432-22, D-12/D-22): `runServiceTokens` is deleted. The run service is tokenless
191
- // since 432-21, and a file that still declares the key gets the normal strict unrecognized-key
192
- // error, with no migration and no special message. The local doors never needed a credential.
193
- // Phase 402 D-07: the interface this server binds. Consumed by 402-05's pre-listen bind guard,
194
- // which REFUSES any non-loopback value — unconditionally since the phase-402 verification round
195
- // (a declared run-service credential authenticates the three run-service paths and nothing else,
196
- // so it was never evidence that the rest of this server's surface was authed).
197
- //
198
- // DELIBERATELY NOT `LoopbackHostSchema` even so. That schema refuses at PARSE time, which would
199
- // make the runtime refusal unreachable and would report the problem as a config-shape error
200
- // rather than as the security refusal it is — and it would also refuse a loopback SPELLING this
201
- // field exists to allow (`localhost`, `::1`, `127.0.0.2` is refused on purpose).
166
+ // The run service and the other local doors are tokenless, so this block declares no token key.
167
+ // The interface the dashboard binds. A plain string, because the pre-listen bind guard refuses
168
+ // any non-loopback value at runtime, as the security refusal it is rather than a config-shape
169
+ // error, and a loopback spelling other than the default (`localhost`, `::1`) stays configurable.
202
170
  host: z3.string().trim().min(1).optional()
203
171
  }).strict();
204
172
  var LOOPBACK_HOSTS = Object.freeze(["localhost", "127.0.0.1", "::1"]);
205
173
  function isLoopbackHost(host) {
206
174
  return LOOPBACK_HOSTS.includes(host.trim());
207
175
  }
208
- var LoopbackHostSchema = z3.string().trim().min(1).refine(
209
- isLoopbackHost,
210
- { message: "MCP host must be localhost, 127.0.0.1, or ::1 in local-only mode" }
211
- );
212
176
  var McpConfigSchema = z3.object({
213
177
  enabled: z3.boolean().optional(),
214
- transport: z3.enum(["http", "stdio"]).optional(),
215
- host: LoopbackHostSchema.optional(),
216
- port: z3.number().int().min(1).max(65535).optional(),
217
- path: z3.string().trim().min(1).regex(/^\//, "MCP path must start with /").optional()
178
+ port: z3.number().int().min(1).max(65535).optional()
218
179
  }).strict();
219
180
  var CacheConfigSchema = z3.object({
220
- dir: z3.string(),
221
- ttl: DurationString
181
+ dir: z3.string()
222
182
  }).strict();
223
183
  var AuthStateConfigSchema = z3.object({
224
184
  dir: z3.string().trim().min(1)
@@ -233,7 +193,7 @@ var RecordingConfigSchema = z3.object({
233
193
  var AccessibilityConfigSchema = z3.object({
234
194
  enabled: z3.boolean(),
235
195
  standard: z3.enum(["wcag2a", "wcag2aa", "wcag2aaa"]).optional(),
236
- runAfter: z3.enum(["every-step", "navigation", "test-end"]).optional(),
196
+ runAfter: z3.enum(["every-step", "test-end"]).optional(),
237
197
  failOnViolation: z3.boolean().optional(),
238
198
  disableRules: z3.array(z3.string()).optional(),
239
199
  exclude: z3.array(z3.string()).optional()
@@ -260,14 +220,7 @@ var isRepoRelative = (dir) => !/^[/\\]|^[A-Za-z]:[\\/]/.test(dir) && !/^\.\.(?:[
260
220
  var KnowledgeConfigSchema = z4.object({
261
221
  dir: z4.string().trim().min(1).refine(isRepoRelative, {
262
222
  message: "knowledge.dir must be repo-relative \u2014 the committed knowledge root must live inside the repo for PR review and the committed .gitignore split"
263
- }).default("agent-qa-knowledge"),
264
- // D-08: closed enum — deliberately no 'full' mode; full-res artifacts ride the
265
- // artifact store/git-LFS side channel per knowledge-store.md §5.
266
- commitArtifacts: z4.enum(["none", "thumbnails"]).default("none"),
267
- // Absent = no cap: the engine's derived budget serves everything it ranks useful; when set
268
- // it is a hard ceiling min(cap, derived), clamped above min_brief — efficacy over token
269
- // saving (D-09, binds Phase 380 brief budgeting).
270
- contextBudgetTokens: z4.number().int().positive().optional()
223
+ }).default("agent-qa-knowledge")
271
224
  }).strict();
272
225
 
273
226
  // src/schema/retired-keys.ts
@@ -288,6 +241,39 @@ var RETIRED_MEMORY_KEY_MESSAGES = {
288
241
  circuitBreakerBaselineSize: "services.memory.circuitBreakerBaselineSize was removed \u2014 no circuit breaker exists",
289
242
  circuitBreakerThreshold: "services.memory.circuitBreakerThreshold was removed \u2014 no circuit breaker exists"
290
243
  };
244
+ var RETIRED_EVAL_KEY_MESSAGES = {
245
+ gate1: "eval.gate1 was removed: memory is no longer screened by replay before promotion. Delete this block.",
246
+ gate2: "eval.gate2 was removed: completed runs no longer replay served memory. Delete this block.",
247
+ adversarial: "eval.adversarial was removed: runs no longer stage adversarial probes. Delete this block.",
248
+ seeding: "eval.seeding was removed: runs no longer seed an eval corpus. Delete this block.",
249
+ judge: "eval.judge was removed: pass --judge <name> to agent-qa eval replay. Delete this key.",
250
+ judges: "eval.judges was removed: repeat --judge <name> on agent-qa eval replay. Delete this key.",
251
+ judgeAllowSameFamily: "eval.judgeAllowSameFamily was removed: pass --judge-allow-same-family to agent-qa eval replay. Delete this key.",
252
+ k: "eval.k was removed: pass --k <n> to agent-qa eval replay. Delete this key.",
253
+ seed: "eval.seed was removed: pass --seed <n> to agent-qa eval replay. Delete this key."
254
+ };
255
+ var RETIRED_STEP_KEY_MESSAGES = {
256
+ retries: "steps[].retries was removed: a test retries as a whole; set meta.retries. Delete this line.",
257
+ screenshot: "steps[].screenshot was removed: screenshots are set per run; pass --screenshot-mode every-step to agent-qa run. Delete this line."
258
+ };
259
+ var RETIRED_EVAL_BLOCK_MESSAGE = "eval was removed: agent-qa eval replay takes its settings as flags. Delete this block.";
260
+ var RETIRED_ALLOW_PRODUCTION_MESSAGE = "allowProduction was removed: production targets run like any other target. Delete this line.";
261
+ var RETIRED_REGISTRY_PROVIDERS_MESSAGE = "registry.providers was removed: BrowserStack is no longer supported; runs use local Playwright and Appium. Delete this block.";
262
+ var RETIRED_DEVICE_TRANSPORT_MESSAGE = "registry.devices.<name>.transport was removed: devices run on local Appium. Delete this line.";
263
+ var RETIRED_APP_BROWSERSTACK_MESSAGE = "registry.targets.<name>.app.browserstack was removed: BrowserStack is no longer supported; app.path is the app file. Delete this line.";
264
+ var RETIRED_CACHE_TTL_MESSAGE = "services.cache.ttl was removed: cached plans do not expire by age. Delete this line.";
265
+ var RETIRED_MCP_KEY_MESSAGES = {
266
+ host: "services.mcp.host was removed: agent-qa mcp always listens on 127.0.0.1. Delete this line.",
267
+ path: "services.mcp.path was removed: the MCP server answers on any path. Delete this line.",
268
+ transport: "services.mcp.transport was removed: agent-qa mcp uses stdio, or HTTP with --http. Delete this line."
269
+ };
270
+ var RETIRED_RUN_AFTER_NAVIGATION_MESSAGE = "services.accessibility.runAfter: navigation was removed: it never ran a check. Use test-end, or delete the line for every-step.";
271
+ var RETIRED_KNOWLEDGE_KEY_MESSAGES = {
272
+ contextBudgetTokens: "knowledge.contextBudgetTokens was removed: agent-qa sizes each run's knowledge itself. Delete this line.",
273
+ commitArtifacts: "knowledge.commitArtifacts was removed: knowledge bundles never commit images. Delete this line."
274
+ };
275
+ var RETIRED_LOCAL_PROVIDERS_MESSAGE = "providers was removed: BrowserStack is no longer supported. Delete this block.";
276
+ var RETIRED_LOCAL_APP_BROWSERSTACK_MESSAGE = "apps.<name>.browserstack was removed: BrowserStack is no longer supported; apps.<name>.path is the app file. Delete this line.";
291
277
  var RETIRED_MAX_SUB_ACTIONS_MESSAGE = "use.planner.maxSubActions was removed in v2 \u2014 it was a refilling iteration count; set use.planner.maxActions (default 30) and use.planner.maxPlanningTurns (default 30)";
292
278
  var RETIRED_PLANNER_KEY = "maxSubActions";
293
279
  function isPlainObject(value) {
@@ -301,7 +287,7 @@ var retiredKeyScan = z5.any().superRefine((value, ctx) => {
301
287
  if (isPlainObject(value.services) && value.services.memory !== void 0) {
302
288
  ctx.addIssue({
303
289
  code: z5.ZodIssueCode.custom,
304
- params: { retired: true },
290
+ params: { retired: true, hardCut: true },
305
291
  path: ["services", "memory"],
306
292
  message: "services.memory was removed in v2 \u2014 the memory config surface collapsed into the top-level knowledge block"
307
293
  });
@@ -312,7 +298,7 @@ var retiredKeyScan = z5.any().superRefine((value, ctx) => {
312
298
  if (message) {
313
299
  ctx.addIssue({
314
300
  code: z5.ZodIssueCode.custom,
315
- params: { retired: true },
301
+ params: { retired: true, hardCut: true },
316
302
  path: ["services", "memory", key],
317
303
  message
318
304
  });
@@ -323,30 +309,164 @@ var retiredKeyScan = z5.any().superRefine((value, ctx) => {
323
309
  if (isPlainObject(value.workspace) && value.workspace.agentRules !== void 0) {
324
310
  ctx.addIssue({
325
311
  code: z5.ZodIssueCode.custom,
326
- params: { retired: true },
312
+ params: { retired: true, hardCut: true },
327
313
  path: ["workspace", "agentRules"],
328
314
  message: "workspace.agentRules was removed in v2 \u2014 rules dissolve into agent-qa-knowledge/policies/<scope>.md files"
329
315
  });
330
316
  }
331
- if (hasRetiredPlannerKey(value.use)) {
317
+ if (Object.hasOwn(value, "eval")) {
318
+ let named = 0;
319
+ if (isPlainObject(value.eval)) {
320
+ for (const key of Object.keys(value.eval)) {
321
+ if (Object.hasOwn(RETIRED_EVAL_KEY_MESSAGES, key)) {
322
+ named++;
323
+ ctx.addIssue({
324
+ code: z5.ZodIssueCode.custom,
325
+ params: { retired: true },
326
+ path: ["eval", key],
327
+ message: RETIRED_EVAL_KEY_MESSAGES[key]
328
+ });
329
+ }
330
+ }
331
+ }
332
+ if (named === 0) {
333
+ ctx.addIssue({
334
+ code: z5.ZodIssueCode.custom,
335
+ params: { retired: true },
336
+ path: ["eval"],
337
+ message: RETIRED_EVAL_BLOCK_MESSAGE
338
+ });
339
+ }
340
+ }
341
+ if (Object.hasOwn(value, "allowProduction")) {
332
342
  ctx.addIssue({
333
343
  code: z5.ZodIssueCode.custom,
334
344
  params: { retired: true },
345
+ path: ["allowProduction"],
346
+ message: RETIRED_ALLOW_PRODUCTION_MESSAGE
347
+ });
348
+ }
349
+ if (isPlainObject(value.registry) && Object.hasOwn(value.registry, "providers")) {
350
+ ctx.addIssue({
351
+ code: z5.ZodIssueCode.custom,
352
+ params: { retired: true },
353
+ path: ["registry", "providers"],
354
+ message: RETIRED_REGISTRY_PROVIDERS_MESSAGE
355
+ });
356
+ }
357
+ if (isPlainObject(value.registry) && isPlainObject(value.registry.devices)) {
358
+ for (const [name, device] of Object.entries(value.registry.devices)) {
359
+ if (isPlainObject(device) && Object.hasOwn(device, "transport")) {
360
+ ctx.addIssue({
361
+ code: z5.ZodIssueCode.custom,
362
+ params: { retired: true },
363
+ path: ["registry", "devices", name, "transport"],
364
+ message: RETIRED_DEVICE_TRANSPORT_MESSAGE
365
+ });
366
+ }
367
+ }
368
+ }
369
+ if (isPlainObject(value.registry) && isPlainObject(value.registry.targets)) {
370
+ for (const [name, target] of Object.entries(value.registry.targets)) {
371
+ if (isPlainObject(target) && isPlainObject(target.app) && Object.hasOwn(target.app, "browserstack")) {
372
+ ctx.addIssue({
373
+ code: z5.ZodIssueCode.custom,
374
+ params: { retired: true },
375
+ path: ["registry", "targets", name, "app", "browserstack"],
376
+ message: RETIRED_APP_BROWSERSTACK_MESSAGE
377
+ });
378
+ }
379
+ }
380
+ }
381
+ if (isPlainObject(value.services) && isPlainObject(value.services.cache) && Object.hasOwn(value.services.cache, "ttl")) {
382
+ ctx.addIssue({
383
+ code: z5.ZodIssueCode.custom,
384
+ params: { retired: true },
385
+ path: ["services", "cache", "ttl"],
386
+ message: RETIRED_CACHE_TTL_MESSAGE
387
+ });
388
+ }
389
+ if (isPlainObject(value.services) && isPlainObject(value.services.mcp)) {
390
+ for (const key of Object.keys(value.services.mcp)) {
391
+ if (Object.hasOwn(RETIRED_MCP_KEY_MESSAGES, key)) {
392
+ ctx.addIssue({
393
+ code: z5.ZodIssueCode.custom,
394
+ params: { retired: true },
395
+ path: ["services", "mcp", key],
396
+ message: RETIRED_MCP_KEY_MESSAGES[key]
397
+ });
398
+ }
399
+ }
400
+ }
401
+ if (isPlainObject(value.services) && isPlainObject(value.services.accessibility) && value.services.accessibility.runAfter === "navigation") {
402
+ ctx.addIssue({
403
+ code: z5.ZodIssueCode.custom,
404
+ params: { retired: true },
405
+ path: ["services", "accessibility", "runAfter"],
406
+ message: RETIRED_RUN_AFTER_NAVIGATION_MESSAGE
407
+ });
408
+ }
409
+ if (isPlainObject(value.knowledge)) {
410
+ for (const key of Object.keys(value.knowledge)) {
411
+ if (Object.hasOwn(RETIRED_KNOWLEDGE_KEY_MESSAGES, key)) {
412
+ ctx.addIssue({
413
+ code: z5.ZodIssueCode.custom,
414
+ params: { retired: true },
415
+ path: ["knowledge", key],
416
+ message: RETIRED_KNOWLEDGE_KEY_MESSAGES[key]
417
+ });
418
+ }
419
+ }
420
+ }
421
+ if (hasRetiredPlannerKey(value.use)) {
422
+ ctx.addIssue({
423
+ code: z5.ZodIssueCode.custom,
424
+ params: { retired: true, hardCut: true },
335
425
  path: ["use", "planner", RETIRED_PLANNER_KEY],
336
426
  message: RETIRED_MAX_SUB_ACTIONS_MESSAGE
337
427
  });
338
428
  }
339
429
  });
430
+ function refuseRetiredLocalBindings(raw, filePath) {
431
+ if (!isPlainObject(raw)) return;
432
+ const issues = [];
433
+ if (Object.hasOwn(raw, "providers")) {
434
+ issues.push({ path: "providers", message: RETIRED_LOCAL_PROVIDERS_MESSAGE });
435
+ }
436
+ if (isPlainObject(raw.apps)) {
437
+ for (const [name, app] of Object.entries(raw.apps)) {
438
+ if (isPlainObject(app) && Object.hasOwn(app, "browserstack")) {
439
+ issues.push({ path: `apps.${name}.browserstack`, message: RETIRED_LOCAL_APP_BROWSERSTACK_MESSAGE });
440
+ }
441
+ }
442
+ }
443
+ if (issues.length === 0) return;
444
+ throw new Error(
445
+ [`${filePath}:`, ...issues.map((issue) => ` ${issue.path}: ${issue.message}`)].join("\n")
446
+ );
447
+ }
340
448
  var retiredUseKeyScan = z5.any().superRefine((value, ctx) => {
341
449
  if (hasRetiredPlannerKey(value)) {
342
450
  ctx.addIssue({
343
451
  code: z5.ZodIssueCode.custom,
344
- params: { retired: true },
452
+ params: { retired: true, hardCut: true },
345
453
  path: ["planner", RETIRED_PLANNER_KEY],
346
454
  message: RETIRED_MAX_SUB_ACTIONS_MESSAGE
347
455
  });
348
456
  }
349
457
  });
458
+ var retiredStepKeyScan = z5.any().superRefine((value, ctx) => {
459
+ if (!isPlainObject(value)) return;
460
+ for (const [key, message] of Object.entries(RETIRED_STEP_KEY_MESSAGES)) {
461
+ if (!Object.hasOwn(value, key)) continue;
462
+ ctx.addIssue({
463
+ code: z5.ZodIssueCode.custom,
464
+ params: { retired: true },
465
+ path: [key],
466
+ message
467
+ });
468
+ }
469
+ });
350
470
 
351
471
  // src/schema/registry-schema.ts
352
472
  import { z as z7 } from "zod";
@@ -376,8 +496,7 @@ function isAbsoluteAppPath(value) {
376
496
  return value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
377
497
  }
378
498
  var AppInstallSchema = z7.object({
379
- path: z7.string().refine((v) => !isAbsoluteAppPath(v), { message: "app.path must be relative" }).optional(),
380
- browserstack: z7.string().optional()
499
+ path: z7.string().refine((v) => !isAbsoluteAppPath(v), { message: "app.path must be relative" }).optional()
381
500
  }).strict();
382
501
  var TargetSchema = z7.object({
383
502
  product: z7.string().refine(
@@ -390,15 +509,8 @@ var TargetSchema = z7.object({
390
509
  appActivity: z7.string().optional(),
391
510
  app: AppInstallSchema.optional(),
392
511
  url: z7.string().optional(),
393
- // D-06 (phase 401): which environment a run against this target actually touches. Deliberately
394
- // `.optional()` with NO default — an undeclared target must stay visibly undeclared; since 432
395
- // (D-18) undeclared means non-production, decided where the production refusal reads it (the
396
- // run gate that decided "shared" is deleted). A default here would make silence and an honest
397
- // declaration indistinguishable at every later reader, and
398
- // `.strict()` above is what turns a typoed key into a refusal rather than a silent undeclared.
399
- // 432 D-18: `production` is the third declared class; only top-level `allowProduction: true`
400
- // opts a run into it.
401
- environment: z7.enum(["local", "shared", "production"]).optional()
512
+ // A label only: agent-qa runs every target the same way, whatever it declares.
513
+ environment: z7.enum(["local", "shared", "production"]).optional().describe("A label for this target's environment. agent-qa runs every target the same way.")
402
514
  }).strict().superRefine((val, ctx) => {
403
515
  if (val.platform === "web" && !val.url) {
404
516
  ctx.addIssue({ code: z7.ZodIssueCode.custom, message: "Web targets must have a url" });
@@ -406,30 +518,25 @@ var TargetSchema = z7.object({
406
518
  });
407
519
  var DeviceProfileSchema = z7.object({
408
520
  platform: z7.enum(["android", "ios"]),
409
- transport: TransportSchema,
410
521
  match: z7.record(z7.string(), z7.unknown()).optional().default({})
411
522
  }).strict().superRefine((val, ctx) => {
412
- if (val.transport === "local") {
413
- const androidOnly = ["avd", "serial", "appPackage", "appActivity", "automationName", "browserName", "platformVersion"];
414
- const iosOnly = ["udid", "bundleId", "automationName", "platformVersion"];
415
- const allowed = val.platform === "android" ? androidOnly : iosOnly;
416
- for (const key of Object.keys(val.match)) {
417
- if (!allowed.includes(key)) {
418
- ctx.addIssue({
419
- code: z7.ZodIssueCode.custom,
420
- path: ["match", key],
421
- message: `Match field "${key}" is not valid for ${val.platform} local transport. Allowed: ${allowed.join(", ")}`
422
- });
423
- }
523
+ const androidOnly = ["avd", "serial", "appPackage", "appActivity", "automationName", "browserName", "platformVersion"];
524
+ const iosOnly = ["udid", "bundleId", "automationName", "platformVersion"];
525
+ const allowed = val.platform === "android" ? androidOnly : iosOnly;
526
+ for (const key of Object.keys(val.match)) {
527
+ if (!allowed.includes(key)) {
528
+ ctx.addIssue({
529
+ code: z7.ZodIssueCode.custom,
530
+ path: ["match", key],
531
+ message: `Match field "${key}" is not valid for an ${val.platform} device. Allowed: ${allowed.join(", ")}`
532
+ });
424
533
  }
425
534
  }
426
535
  });
427
- var ProviderConfigSchema = z7.record(z7.string(), z7.unknown());
428
536
  var RegistrySchema = z7.object({
429
537
  llms: z7.array(NamedLLMConfigSchema).optional().default([]),
430
538
  targets: z7.record(TargetNameSchema, TargetSchema).optional(),
431
- devices: z7.record(z7.string(), DeviceProfileSchema).optional(),
432
- providers: z7.record(z7.string(), ProviderConfigSchema).optional()
539
+ devices: z7.record(z7.string(), DeviceProfileSchema).optional()
433
540
  }).strict();
434
541
 
435
542
  // src/schema/use-schema.ts
@@ -473,28 +580,11 @@ var UseSchema = z8.object({
473
580
  logCapture: LogCaptureConfigSchema.optional(),
474
581
  cache: z8.boolean().optional(),
475
582
  llm: z8.string().optional(),
476
- /**
477
- * Names the app-under-test build for the DCAY-01 buildTag-to-ordinal mapping.
478
- * Precedence: CLI --build-tag > AGENT_QA_BUILD_TAG > this config key.
479
- */
583
+ // Precedence: CLI `--build-tag`, then `AGENT_QA_BUILD_TAG`, then this key.
480
584
  buildTag: z8.string().trim().min(1).optional(),
481
- /**
482
- * 402-03 (D-12): run the members of one run CONCURRENTLY. Absent or false is one member at a
483
- * time — today's behaviour, byte for byte.
484
- */
585
+ // Runs one run's members concurrently; absent or false runs one member at a time.
485
586
  parallel: z8.boolean().optional(),
486
- /**
487
- * 402-03 (D-12): the ceiling on concurrent members when `parallel` is true. Absent resolves to
488
- * `DEFAULT_PARALLEL_WORKERS` (3). Effective concurrency is `min(memberCount, workers)`, decided
489
- * in exactly one place — `resolveEffectiveWorkers` in `@vostride/agent-qa-schema`.
490
- *
491
- * A POSITIVE INTEGER, refused otherwise: an unbounded or nonsensical count here is the only
492
- * reachable input to a concurrency denial-of-service (T-402-12), and it is a config typo rather
493
- * than a caller's field — `RunsStartInput` names neither of these two.
494
- *
495
- * NOT `.default(...)`: a defaulted key changes the PARSED config type and every hand-built
496
- * config fixture would then need it. The default is the resolver's, not the schema's.
497
- */
587
+ // Not defaulted, so the parsed type is unchanged; `resolveEffectiveWorkers` owns the default.
498
588
  workers: z8.number().int().positive().optional()
499
589
  }).strict();
500
590
  var UseOverrideSchema = z8.object({
@@ -509,7 +599,9 @@ var UseOverrideSchema = z8.object({
509
599
  timeout: z8.object({
510
600
  step: DurationString.optional(),
511
601
  test: DurationString.optional(),
512
- navigation: DurationString.optional()
602
+ navigation: DurationString.optional().describe(
603
+ "How long a web page load or URL wait can take. Default 2m. Android and iOS runs do not use it."
604
+ )
513
605
  }).strict().optional(),
514
606
  planner: z8.object({
515
607
  maxActions: z8.number().int().positive().optional(),
@@ -525,7 +617,7 @@ var UseOverrideSchema = z8.object({
525
617
  mobile: MobileUseOverrideSchema.optional(),
526
618
  llm: z8.string().optional(),
527
619
  parallel: z8.boolean().optional(),
528
- /** 402-03 (D-12): the override half of `use.workers`. Same shape as `UseSchema`'s. */
620
+ // The override half of `use.workers`, shaped like `UseSchema`'s.
529
621
  workers: z8.number().int().positive().optional(),
530
622
  device: z8.string().optional()
531
623
  }).strict();
@@ -569,82 +661,15 @@ var PluginsSchema = z10.object({
569
661
  var AgentQaConfigSchemaInner = z10.object({
570
662
  workspace: WorkspaceSchema,
571
663
  services: ServicesSchema.prefault({}).optional(),
572
- // D-08: slim replacement for the retired v1 memory config surface; prefaulted so parsed
573
- // configs always materialize knowledge defaults (zod 4 .default() short-circuits without
574
- // parsing — .prefault() runs the inner defaults) — matches the providers `.default([])`
575
- // precedent in spirit.
664
+ // `.prefault` runs the inner defaults; zod 4's `.default()` short-circuits without parsing.
576
665
  knowledge: KnowledgeConfigSchema.prefault({}),
577
666
  registry: RegistrySchema.optional(),
578
667
  use: UseSchema.optional(),
579
668
  plugins: PluginsSchema.optional(),
580
- analytics: AnalyticsSchema.optional(),
581
- // 432 D-18: the one production opt-in. Optional with no default: its consumer reads absence as
582
- // false, so only an explicit `true` in the validated config authorizes a production target.
583
- allowProduction: z10.boolean().optional(),
584
- // 2026-10-01 (432-22, D-15/D-18/D-22): the `safety` section (`safety.trustMode`) is deleted with
585
- // trust mode. `allowProduction` above is the only production opt-in, and a file that still
586
- // declares `safety` gets the normal strict unrecognized-key error.
587
- // D-03 (Phase 372): eval substrate params. `judge` references a NAMED registry.llms entry;
588
- // strict so unknown eval subkeys are rejected.
589
- eval: z10.object({
590
- judge: z10.string().min(1).refine((name) => name.trim().length > 0).optional(),
591
- judges: z10.array(z10.string().min(1).refine((name) => name.trim().length > 0)).max(3).optional(),
592
- // Owner override of D-10 (2026-07-12): explicit default-off opt-in that lets a
593
- // KNOWN-same-family judge run; the eligibility law discloses the override on every
594
- // scored entry (familyOverride) and the report projection renders one honesty line.
595
- // Unknown families stay forbidden regardless.
596
- judgeAllowSameFamily: z10.boolean().optional(),
597
- k: z10.number().int().positive().optional(),
598
- seed: z10.number().int().optional(),
599
- // 2026-09-07, 417 D-19: Gate-1 screen cap; recorded default lives in the CLI.
600
- // 2026-09-11, 422 D-14: `maxPerJob` bounds how many Gate-1 CALLS one post-run proposal job
601
- // may make; recorded default lives beside the handler (PROPOSE_DEFAULT_MAX_GATE1_PER_JOB).
602
- // A SECOND axis beside `maxScreens`, not a refinement of it — that one bounds how much a
603
- // single screening looks at, this one bounds how many screenings a run's tail buys — and an
604
- // operator can legitimately want a generous first and a tight second. Positive-integer only,
605
- // for the sharper version of the usual reason: a bound that silently fell back to the
606
- // recorded default because the operator typed `0` would buy replay work they refused. There
607
- // is deliberately NO CLI flag: the bound is charged durably per job and read back by a
608
- // resumed attempt, so a per-invocation flag would be a bound that could change between the
609
- // attempt that spent it and the attempt that has to honour what was spent.
610
- gate1: z10.object({
611
- maxScreens: z10.number().int().positive().optional(),
612
- maxPerJob: z10.number().int().positive().optional()
613
- }).strict().optional(),
614
- // 2026-09-07, 418 D-04/D-13/D-14: Gate-2 ablation cap, canary horizon (build ordinals)
615
- // and per-scope lower-trust cap; recorded defaults live in the CLI (GATE2_DEFAULT_*).
616
- gate2: z10.object({
617
- maxAblations: z10.number().int().positive().optional(),
618
- canaryHorizonBuilds: z10.number().int().positive().optional(),
619
- maxLowerTrustPerScope: z10.number().int().positive().optional()
620
- }).strict().optional(),
621
- // 2026-09-11, 422 D-04: how many NOVEL seeds one adversarial refresh may derive from
622
- // before the rest are counted and deferred; recorded default lives in eval core
623
- // (ADVERSARIAL_DEFAULT_MAX_SEEDS_PER_REFRESH). Strict, and positive-integer only: a cap
624
- // that silently fell back to the default because the operator typed `0` is not a cap.
625
- // 2026-09-11, 422 D-04/D-10: `maxProbeCalls` bounds PROVIDER CALLS one refresh may make to
626
- // author probes; recorded default lives in eval-local
627
- // (ADVERSARIAL_DEFAULT_MAX_PROBE_CALLS). It is a SECOND budget beside the seed cap on
628
- // purpose — deterministic seeds cost CPU and authored probes cost money no later run can
629
- // recover — and it is held to the same positive-integer strictness for the sharper version
630
- // of the same reason: a cap that silently fell back to the default because the operator
631
- // typed `0` would buy calls they refused.
632
- adversarial: z10.object({
633
- maxSeedsPerRefresh: z10.number().int().positive().optional(),
634
- maxProbeCalls: z10.number().int().positive().optional()
635
- }).strict().optional(),
636
- seeding: z10.object({
637
- silverPerTestCap: z10.number().int().positive().optional()
638
- }).strict().optional()
639
- }).strict().optional()
669
+ analytics: AnalyticsSchema.optional()
670
+ // There is no `safety` section: a file that still declares one gets the normal strict
671
+ // unrecognized-key error.
640
672
  }).strict().superRefine((val, ctx) => {
641
- if (val.eval?.judge !== void 0 && val.eval.judges !== void 0) {
642
- ctx.addIssue({
643
- code: z10.ZodIssueCode.custom,
644
- path: ["eval"],
645
- message: "Set either eval.judge or eval.judges, never both; remove one declaration."
646
- });
647
- }
648
673
  const llms = val.registry?.llms ?? [];
649
674
  const names = llms.map((c) => c.name);
650
675
  const dupes = names.filter((n, i) => names.indexOf(n) !== i);
@@ -669,13 +694,6 @@ var AgentQaConfigSchemaInner = z10.object({
669
694
  message: `use.llm "${val.use.llm}" does not match any name in registry.llms`
670
695
  });
671
696
  }
672
- if (!MobileAppStateSchema.safeParse(val.use?.mobile?.appState).success) {
673
- ctx.addIssue({
674
- code: z10.ZodIssueCode.custom,
675
- path: ["use", "mobile", "appState"],
676
- message: "use.mobile.appState is required and must be one of: preserve | reset"
677
- });
678
- }
679
697
  });
680
698
  var AgentQaConfigSchema = retiredKeyScan.pipe(AgentQaConfigSchemaInner);
681
699
 
@@ -698,8 +716,6 @@ var TestStepSchema = z11.union([
698
716
  z11.object({
699
717
  step: z11.string(),
700
718
  timeout: OptionalDuration,
701
- retries: z11.number().optional(),
702
- screenshot: z11.boolean().optional(),
703
719
  capture: CaptureConfigSchema.optional()
704
720
  }).strict()
705
721
  ]);
@@ -722,19 +738,9 @@ var TestDefinitionSchema = z11.object({
722
738
  meta: TestMetaSchema.optional(),
723
739
  setup: z11.array(HookIdSchema).optional(),
724
740
  teardown: z11.array(HookIdSchema).optional(),
725
- steps: z11.array(TestStepSchema).min(1)
741
+ steps: z11.array(retiredStepKeyScan.pipe(TestStepSchema)).min(1)
726
742
  }).strict();
727
743
 
728
- // src/schema/action-schema.ts
729
- import { z as z12 } from "zod";
730
- var ActionPlanSchema = z12.object({
731
- reasoning: z12.string().describe("Brief explanation of why this action was chosen"),
732
- action: z12.object({ type: z12.string() }).passthrough(),
733
- confidence: z12.number().describe("Confidence level from 0 to 1"),
734
- stepComplete: z12.boolean().default(false).describe("Whether this action completes the step goal. Set true when the step objective is fully accomplished, false when more actions are needed."),
735
- stepFailed: z12.boolean().optional().default(false).describe("Set true when the step goal cannot be achieved from the current screen state. When true, no action is executed and the step fails with the reasoning as the error message.")
736
- });
737
-
738
744
  // src/schema/resolve-knowledge-root.ts
739
745
  import path from "path";
740
746
  function resolveKnowledgeRoot(knowledge, configDir) {
@@ -742,66 +748,66 @@ function resolveKnowledgeRoot(knowledge, configDir) {
742
748
  }
743
749
 
744
750
  // src/schema/suite-schema.ts
745
- import { z as z13 } from "zod";
751
+ import { z as z12 } from "zod";
746
752
  import { isCanonicalHookId as isCanonicalHookId2, isCanonicalSuiteId, isCanonicalTestId as isCanonicalTestId2 } from "@vostride/agent-qa-ids";
747
- var SuiteTestEntrySchema = z13.object({
748
- test: z13.string(),
749
- id: z13.string().refine((value) => isCanonicalTestId2(value), {
753
+ var SuiteTestEntrySchema = z12.object({
754
+ test: z12.string(),
755
+ id: z12.string().refine((value) => isCanonicalTestId2(value), {
750
756
  message: "Test ID must be t_ followed by 10 id-agent words"
751
757
  })
752
758
  }).strict();
753
- var HookIdSchema2 = z13.string().refine((value) => isCanonicalHookId2(value), {
759
+ var HookIdSchema2 = z12.string().refine((value) => isCanonicalHookId2(value), {
754
760
  message: "Hook ID must be h_ followed by 10 id-agent words"
755
761
  });
756
- var SuiteDefinitionSchema = z13.object({
757
- "suite-id": z13.string().refine((value) => isCanonicalSuiteId(value), {
762
+ var SuiteDefinitionSchema = z12.object({
763
+ "suite-id": z12.string().refine((value) => isCanonicalSuiteId(value), {
758
764
  message: "Suite ID must be s_ followed by 10 id-agent words"
759
765
  }).optional(),
760
- name: z13.string(),
761
- target: z13.string(),
762
- context: z13.string().optional(),
763
- setup: z13.array(HookIdSchema2).optional(),
764
- teardown: z13.array(HookIdSchema2).optional(),
765
- tests: z13.array(SuiteTestEntrySchema).min(1),
766
+ name: z12.string(),
767
+ target: z12.string(),
768
+ context: z12.string().optional(),
769
+ setup: z12.array(HookIdSchema2).optional(),
770
+ teardown: z12.array(HookIdSchema2).optional(),
771
+ tests: z12.array(SuiteTestEntrySchema).min(1),
766
772
  use: retiredUseKeyScan.pipe(UseOverrideSchema).optional()
767
773
  }).strict();
768
774
 
769
775
  // src/hooks/schema.ts
770
- import { z as z14 } from "zod";
776
+ import { z as z13 } from "zod";
771
777
  import ms3 from "ms";
772
778
  import { isCanonicalHookId as isCanonicalHookId3 } from "@vostride/agent-qa-ids";
773
- var HookDurationString = z14.string().refine((v) => ms3(v) !== void 0, {
779
+ var HookDurationString = z13.string().refine((v) => ms3(v) !== void 0, {
774
780
  message: 'Invalid duration format (e.g., "30s", "2m", "1h")'
775
781
  }).transform((v) => ms3(v));
776
782
  var HOOK_ID_MESSAGE = "Hook ID must be h_ followed by 10 id-agent words";
777
- var HookDefinitionSchema = z14.object({
778
- id: z14.string().refine((value) => isCanonicalHookId3(value), {
783
+ var HookDefinitionSchema = z13.object({
784
+ id: z13.string().refine((value) => isCanonicalHookId3(value), {
779
785
  message: HOOK_ID_MESSAGE
780
786
  }),
781
- name: z14.string().min(1),
782
- runtime: z14.enum(["node", "bun", "python", "bash"]),
783
- file: z14.string().min(1),
784
- deps: z14.array(z14.string()).optional().default([]),
785
- packageFile: z14.string().optional(),
787
+ name: z13.string().min(1),
788
+ runtime: z13.enum(["node", "bun", "python", "bash"]),
789
+ file: z13.string().min(1),
790
+ deps: z13.array(z13.string()).optional().default([]),
791
+ packageFile: z13.string().optional(),
786
792
  timeout: HookDurationString,
787
- network: z14.boolean().optional().default(true)
793
+ network: z13.boolean().optional().default(true)
788
794
  });
789
- var HooksFileSchema = z14.object({
790
- hooks: z14.array(HookDefinitionSchema)
795
+ var HooksFileSchema = z13.object({
796
+ hooks: z13.array(HookDefinitionSchema)
791
797
  }).superRefine((val, ctx) => {
792
798
  const ids = /* @__PURE__ */ new Set();
793
799
  const names = /* @__PURE__ */ new Set();
794
800
  for (let i = 0; i < val.hooks.length; i++) {
795
801
  if (ids.has(val.hooks[i].id)) {
796
802
  ctx.addIssue({
797
- code: z14.ZodIssueCode.custom,
803
+ code: z13.ZodIssueCode.custom,
798
804
  path: ["hooks", i, "id"],
799
805
  message: `Duplicate hook id: "${val.hooks[i].id}"`
800
806
  });
801
807
  }
802
808
  if (names.has(val.hooks[i].name)) {
803
809
  ctx.addIssue({
804
- code: z14.ZodIssueCode.custom,
810
+ code: z13.ZodIssueCode.custom,
805
811
  path: ["hooks", i, "name"],
806
812
  message: `Duplicate hook name: "${val.hooks[i].name}"`
807
813
  });
@@ -1167,7 +1173,7 @@ var PatchOpUnsupportedTargetError = class extends Error {
1167
1173
  var PatchedDocumentInvalidError = class extends Error {
1168
1174
  constructor(phase, detail) {
1169
1175
  super(
1170
- `cst-patch: the ${phase} document is not a valid test: ${detail}; a patched document is schema-validated before any caller may write it (WTST-07)`
1176
+ `cst-patch: the ${phase} document is not a valid test: ${detail}; a patched document is schema-validated before any caller may write it`
1171
1177
  );
1172
1178
  this.phase = phase;
1173
1179
  this.detail = detail;
@@ -1263,7 +1269,7 @@ function resolveSeqSlot(doc, pointer, segments, op) {
1263
1269
  if (segments.length < 2) {
1264
1270
  throw new PatchOpUnsupportedTargetError(
1265
1271
  pointer,
1266
- `${op} operates on a sequence item, and that path names a top-level key; adding or removing a key is a document rewrite (D-02), not a patch`
1272
+ `${op} operates on a sequence item, and that path names a top-level key; adding or removing a key is a document rewrite, not a patch`
1267
1273
  );
1268
1274
  }
1269
1275
  const parent = resolveNode(doc, segments.slice(0, -1));
@@ -1932,9 +1938,6 @@ var MobileSetupError = class extends Error {
1932
1938
  function isMobilePlatform(value) {
1933
1939
  return value === "android" || value === "ios";
1934
1940
  }
1935
- function isMobileTransport(value) {
1936
- return value === "local" || value === "browserstack";
1937
- }
1938
1941
  function asOptionalString(value) {
1939
1942
  return typeof value === "string" && value.length > 0 ? value : void 0;
1940
1943
  }
@@ -2006,11 +2009,6 @@ function resolveAppInstall(target, input, platform, deviceName, sourceTrace) {
2006
2009
  const pathValue = localPath ?? targetPath;
2007
2010
  const pathSource = localPath ? `agent-qa.local.yaml apps.${input.targetName}.path` : targetPath ? `agent-qa.config.yaml registry.targets.${input.targetName}.app.path` : void 0;
2008
2011
  const pathBaseDir = localPath ? baseDirFor(input.localConfigFilePath ?? input.localBindings?.filePath) : targetPath ? baseDirFor(input.configFilePath) : void 0;
2009
- const localBrowserStack = asOptionalString(localApp?.browserstack);
2010
- const targetBrowserStack = asOptionalString(target.app?.browserstack);
2011
- const browserstackValue = localBrowserStack ?? targetBrowserStack;
2012
- const browserstackSource = localBrowserStack ? `agent-qa.local.yaml apps.${input.targetName}.browserstack` : targetBrowserStack ? `agent-qa.config.yaml registry.targets.${input.targetName}.app.browserstack` : void 0;
2013
- const browserstackBaseDir = localBrowserStack ? baseDirFor(input.localConfigFilePath ?? input.localBindings?.filePath) : targetBrowserStack ? baseDirFor(input.configFilePath) : void 0;
2014
2012
  const install = { sourceTrace: installSourceTrace };
2015
2013
  if (pathValue) {
2016
2014
  if (isAbsoluteAppInstallValue(pathValue)) {
@@ -2036,23 +2034,7 @@ function resolveAppInstall(target, input, platform, deviceName, sourceTrace) {
2036
2034
  sourceTrace.push(`app.path=${pathSource}`);
2037
2035
  }
2038
2036
  }
2039
- if (browserstackValue) {
2040
- if (isAbsoluteAppInstallValue(browserstackValue)) {
2041
- throw appInstallError(`Configured BrowserStack app value must be relative or a BrowserStack reference: ${browserstackValue}`, input, {
2042
- platform,
2043
- deviceName,
2044
- appId: browserstackValue,
2045
- sourceTrace: [...sourceTrace, `app.browserstack=${browserstackSource}`]
2046
- });
2047
- }
2048
- install.browserstack = browserstackValue;
2049
- install.browserstackBaseDir = browserstackBaseDir;
2050
- if (browserstackSource) {
2051
- installSourceTrace["app.browserstack"] = browserstackSource;
2052
- sourceTrace.push(`app.browserstack=${browserstackSource}`);
2053
- }
2054
- }
2055
- return install.path || install.browserstack ? install : void 0;
2037
+ return install.path ? install : void 0;
2056
2038
  }
2057
2039
  function resolveMobileRunConfig(input) {
2058
2040
  const targets = input.config.registry?.targets;
@@ -2106,7 +2088,6 @@ function resolveMobileRunConfig(input) {
2106
2088
  { platform, deviceName }
2107
2089
  );
2108
2090
  }
2109
- const transport = isMobileTransport(deviceProfile.transport) ? deviceProfile.transport : "local";
2110
2091
  const localMatch = input.localBindings?.devices?.[deviceName] ?? null;
2111
2092
  const mergedMatch = { ...deviceProfile.match ?? {}, ...localMatch ?? {} };
2112
2093
  const appSourceTrace = {};
@@ -2148,11 +2129,9 @@ function resolveMobileRunConfig(input) {
2148
2129
  platform,
2149
2130
  targetName: input.targetName,
2150
2131
  deviceName,
2151
- transport,
2152
2132
  device: {
2153
2133
  name: deviceName,
2154
2134
  platform,
2155
- transport,
2156
2135
  match: mergedMatch
2157
2136
  },
2158
2137
  app,
@@ -3120,7 +3099,7 @@ function clearLoadedProviderPluginModules() {
3120
3099
  }
3121
3100
 
3122
3101
  // src/logging/types.ts
3123
- import { z as z15 } from "zod";
3102
+ import { z as z14 } from "zod";
3124
3103
  var LOG_LEVEL_PRIORITY = {
3125
3104
  silent: 0,
3126
3105
  error: 1,
@@ -3128,46 +3107,45 @@ var LOG_LEVEL_PRIORITY = {
3128
3107
  info: 3,
3129
3108
  debug: 4
3130
3109
  };
3131
- var AgentLogDataSchema = z15.object({
3132
- extractDom: z15.boolean().optional(),
3133
- hasScreenshot: z15.boolean().optional(),
3134
- duration: z15.number().optional(),
3135
- elementCount: z15.number().optional()
3110
+ var AgentLogDataSchema = z14.object({
3111
+ hasScreenshot: z14.boolean().optional(),
3112
+ duration: z14.number().optional(),
3113
+ elementCount: z14.number().optional()
3136
3114
  });
3137
- var AdapterLogDataSchema = z15.object({
3138
- actionType: z15.string().optional(),
3139
- selector: z15.string().optional(),
3140
- coordinates: z15.object({ x: z15.number(), y: z15.number() }).optional(),
3141
- elementProperties: z15.record(z15.string(), z15.unknown()).optional(),
3142
- duration: z15.number().optional()
3115
+ var AdapterLogDataSchema = z14.object({
3116
+ actionType: z14.string().optional(),
3117
+ selector: z14.string().optional(),
3118
+ coordinates: z14.object({ x: z14.number(), y: z14.number() }).optional(),
3119
+ elementProperties: z14.record(z14.string(), z14.unknown()).optional(),
3120
+ duration: z14.number().optional()
3143
3121
  });
3144
- var CacheLogDataSchema = z15.object({
3145
- operation: z15.enum(["get", "set", "invalidate", "bypass", "batch-purge"]),
3146
- stepHash: z15.string().optional(),
3147
- screenHash: z15.string().optional(),
3148
- hit: z15.boolean().optional(),
3149
- age: z15.number().optional(),
3150
- entriesDeleted: z15.number().optional()
3122
+ var CacheLogDataSchema = z14.object({
3123
+ operation: z14.enum(["get", "set", "invalidate", "bypass", "batch-purge"]),
3124
+ stepHash: z14.string().optional(),
3125
+ screenHash: z14.string().optional(),
3126
+ hit: z14.boolean().optional(),
3127
+ age: z14.number().optional(),
3128
+ entriesDeleted: z14.number().optional()
3151
3129
  });
3152
- var PlannerLogDataSchema = z15.object({
3153
- model: z15.string().optional(),
3154
- promptTokens: z15.number().optional(),
3155
- completionTokens: z15.number().optional(),
3156
- latencyMs: z15.number().optional(),
3157
- confidence: z15.number().optional(),
3158
- actionType: z15.string().optional()
3130
+ var PlannerLogDataSchema = z14.object({
3131
+ model: z14.string().optional(),
3132
+ promptTokens: z14.number().optional(),
3133
+ completionTokens: z14.number().optional(),
3134
+ latencyMs: z14.number().optional(),
3135
+ confidence: z14.number().optional(),
3136
+ actionType: z14.string().optional()
3159
3137
  });
3160
- var HookLogDataSchema = z15.object({
3161
- hookName: z15.string().optional(),
3162
- phase: z15.enum(["setup", "teardown"]).optional(),
3163
- duration: z15.number().optional(),
3164
- success: z15.boolean().optional()
3138
+ var HookLogDataSchema = z14.object({
3139
+ hookName: z14.string().optional(),
3140
+ phase: z14.enum(["setup", "teardown"]).optional(),
3141
+ duration: z14.number().optional(),
3142
+ success: z14.boolean().optional()
3165
3143
  });
3166
- var RunnerLogDataSchema = z15.object({
3167
- stepIndex: z15.number().optional(),
3168
- totalSteps: z15.number().optional(),
3169
- testName: z15.string().optional(),
3170
- variablesCaptured: z15.number().optional()
3144
+ var RunnerLogDataSchema = z14.object({
3145
+ stepIndex: z14.number().optional(),
3146
+ totalSteps: z14.number().optional(),
3147
+ testName: z14.string().optional(),
3148
+ variablesCaptured: z14.number().optional()
3171
3149
  });
3172
3150
  var LogSourceDataSchemas = {
3173
3151
  agent: AgentLogDataSchema,
@@ -3198,12 +3176,60 @@ function formatLogEntry(entry) {
3198
3176
  return `${time} ${level} ${source} ${entry.message}${dataSuffix}`;
3199
3177
  }
3200
3178
 
3179
+ // src/logging/run-text-output.ts
3180
+ import { randomBytes as randomBytes2 } from "crypto";
3181
+ var STDOUT_RESERVATIONS = /* @__PURE__ */ Symbol.for("agent-qa.run-text-output.stdout-reservations");
3182
+ function stdoutReservations() {
3183
+ const holder = globalThis;
3184
+ holder[STDOUT_RESERVATIONS] ??= { count: 0 };
3185
+ return holder[STDOUT_RESERVATIONS];
3186
+ }
3187
+ function isGitHubActions(env = process.env) {
3188
+ const value = env.GITHUB_ACTIONS;
3189
+ return value !== void 0 && value !== "" && value.toLowerCase() !== "false" && value !== "0";
3190
+ }
3191
+ function guardRunText(text, env = process.env) {
3192
+ if (!isGitHubActions(env)) return text;
3193
+ const token = randomBytes2(16).toString("hex");
3194
+ const block = text.endsWith("\n") ? text : `${text}
3195
+ `;
3196
+ return `::stop-commands::${token}
3197
+ ${block}::${token}::
3198
+ `;
3199
+ }
3200
+ function guardJsonText(serialized, env = process.env) {
3201
+ return isGitHubActions(env) ? serialized.replaceAll("#", "\\u0023") : serialized;
3202
+ }
3203
+ function reserveStdoutForData() {
3204
+ const reservations = stdoutReservations();
3205
+ reservations.count += 1;
3206
+ let released = false;
3207
+ return () => {
3208
+ if (released) return;
3209
+ released = true;
3210
+ reservations.count -= 1;
3211
+ };
3212
+ }
3213
+ function printRunText(text, print = (line) => console.log(line), options = {}) {
3214
+ const env = options.env ?? process.env;
3215
+ if (!isGitHubActions(env)) {
3216
+ print(text);
3217
+ return;
3218
+ }
3219
+ const block = guardRunText(text, env);
3220
+ if (options.stdoutIsData ?? stdoutReservations().count > 0) {
3221
+ process.stderr.write(block);
3222
+ return;
3223
+ }
3224
+ process.stdout.write(block);
3225
+ }
3226
+
3201
3227
  // src/logging/log-manager.ts
3202
3228
  import { redactSecretValue } from "@vostride/agent-qa-runner";
3203
3229
  var MemberLogScopeUnavailableError = class extends Error {
3204
3230
  constructor() {
3205
3231
  super(
3206
- "LogManager.runInMemberScope requires a memberScope store. Construct the manager with `memberScope: new AsyncLocalStorage()` \u2014 without it, concurrent members would silently share one run id (WINDOWS #233)."
3232
+ "LogManager.runInMemberScope requires a memberScope store. Construct the manager with `memberScope: new AsyncLocalStorage()` \u2014 without it, concurrent members would silently share one run id."
3207
3233
  );
3208
3234
  this.name = "MemberLogScopeUnavailableError";
3209
3235
  }
@@ -3218,6 +3244,7 @@ var LogManager = class {
3218
3244
  ndjson;
3219
3245
  redactor;
3220
3246
  memberScope;
3247
+ env;
3221
3248
  constructor(opts) {
3222
3249
  this.runId = opts.runId ?? "";
3223
3250
  this.runIdSet = !!opts.runId;
@@ -3226,21 +3253,12 @@ var LogManager = class {
3226
3253
  this.ndjson = opts.ndjson ?? false;
3227
3254
  this.redactor = opts.redactor;
3228
3255
  this.memberScope = opts.memberScope;
3256
+ this.env = opts.env;
3229
3257
  }
3230
3258
  setRedactor(redactor) {
3231
3259
  this.redactor = redactor;
3232
3260
  }
3233
- /**
3234
- * Run `fn` with its own run-id binding, so concurrent members cannot overwrite each other.
3235
- *
3236
- * ONE `LogManager` SERVES EVERY MEMBER of a run — `commands/run.ts` builds it once, outside the
3237
- * per-member closure — and since 402-02/402-03 the direct path runs members concurrently. Without
3238
- * a scope, `setRunId` is a single mutable field plus a whole-buffer backfill, so the LAST member
3239
- * to mint an id claims every line in the buffer, its siblings' included (measured, not inferred).
3240
- *
3241
- * Outside a scope nothing changes: `setRunId` keeps its original whole-buffer behaviour, which is
3242
- * what the sequential path and the run-level `onRunCreated` stamp rely on.
3243
- */
3261
+ // One manager serves every concurrent member; outside a scope `setRunId` backfills the buffer.
3244
3262
  runInMemberScope(fn) {
3245
3263
  if (!this.memberScope) throw new MemberLogScopeUnavailableError();
3246
3264
  return this.memberScope.run({ runId: this.runId, pending: [] }, fn);
@@ -3260,7 +3278,14 @@ var LogManager = class {
3260
3278
  this.buffer.push(entry);
3261
3279
  if (scope && !entry.runId) scope.pending.push(entry);
3262
3280
  if (this.shouldDisplay(level)) {
3263
- process.stderr.write(formatLogEntry(entry) + "\n");
3281
+ printRunText(
3282
+ formatLogEntry(entry),
3283
+ (line) => {
3284
+ process.stderr.write(`${line}
3285
+ `);
3286
+ },
3287
+ { env: this.env }
3288
+ );
3264
3289
  }
3265
3290
  if (this.ndjson) {
3266
3291
  this.emitNdjson(entry);
@@ -3281,7 +3306,7 @@ var LogManager = class {
3281
3306
  */
3282
3307
  emitNdjson(entry) {
3283
3308
  process.stdout.write(
3284
- `AGENT_QA_EVENT:${JSON.stringify({ type: "log", ...entry })}
3309
+ `AGENT_QA_EVENT:${guardJsonText(JSON.stringify({ type: "log", ...entry }), this.env)}
3285
3310
  `
3286
3311
  );
3287
3312
  }
@@ -3329,263 +3354,6 @@ var LogManager = class {
3329
3354
  }
3330
3355
  };
3331
3356
 
3332
- // src/farm/credentials.ts
3333
- var ENV_MAP = {
3334
- browserstack: {
3335
- username: "BROWSERSTACK_USERNAME",
3336
- accessKey: "BROWSERSTACK_ACCESS_KEY"
3337
- }
3338
- };
3339
- var DISPLAY_NAMES = {
3340
- browserstack: "BrowserStack"
3341
- };
3342
- function resolveFarmCredentials(provider, providerConfig) {
3343
- const envKeys = ENV_MAP[provider];
3344
- if (!envKeys) {
3345
- throw new Error(`Unknown farm provider: ${provider}`);
3346
- }
3347
- const envUsername = process.env[envKeys.username];
3348
- const envAccessKey = process.env[envKeys.accessKey];
3349
- if (envUsername && envAccessKey) {
3350
- return { username: envUsername, accessKey: envAccessKey };
3351
- }
3352
- if (providerConfig?.username && providerConfig?.accessKey) {
3353
- return { username: providerConfig.username, accessKey: providerConfig.accessKey };
3354
- }
3355
- const displayName = DISPLAY_NAMES[provider] ?? provider;
3356
- throw new Error(
3357
- `${displayName} credentials not found. Set ${envKeys.username} and ${envKeys.accessKey} env vars, or add provider credentials to agent-qa.local.yaml.`
3358
- );
3359
- }
3360
-
3361
- // src/farm/capability-mapper.ts
3362
- import { createRequire as createRequire2 } from "module";
3363
- var BROWSER_MAP = {
3364
- chromium: "playwright-chromium",
3365
- firefox: "playwright-firefox",
3366
- webkit: "playwright-webkit"
3367
- };
3368
- function detectPlaywrightVersion() {
3369
- try {
3370
- const require4 = createRequire2(typeof __filename === "string" ? __filename : import.meta.url);
3371
- const pkg = require4("playwright-core/package.json");
3372
- return pkg.version;
3373
- } catch {
3374
- return "1.59.1";
3375
- }
3376
- }
3377
- function mapWebCapabilities(browser, credentials, opts) {
3378
- if (!browser.viewport) {
3379
- throw new Error("Farm mode requires browser.viewport to be set (e.g. { width: 1280, height: 720 })");
3380
- }
3381
- const bstackBrowser = BROWSER_MAP[browser.name];
3382
- if (!bstackBrowser) {
3383
- throw new Error(`Unsupported browser for BrowserStack: "${browser.name}". Supported: ${Object.keys(BROWSER_MAP).join(", ")}`);
3384
- }
3385
- return {
3386
- browser: bstackBrowser,
3387
- os: "os x",
3388
- os_version: "sonoma",
3389
- resolution: "1920x1080",
3390
- "browserstack.username": credentials.username,
3391
- "browserstack.accessKey": credentials.accessKey,
3392
- "client.playwrightVersion": opts?.playwrightVersion ?? detectPlaywrightVersion(),
3393
- project: "agent-qa",
3394
- build: "agent-qa-" + Date.now(),
3395
- name: opts?.testName
3396
- };
3397
- }
3398
- function mapMobileCapabilities(match, platform, credentials, opts) {
3399
- if (!opts?.testTimeout) {
3400
- throw new Error("Farm mode requires timeout.test to be set in config");
3401
- }
3402
- return {
3403
- "bstack:options": {
3404
- userName: credentials.username,
3405
- accessKey: credentials.accessKey,
3406
- deviceName: match.deviceName,
3407
- osVersion: match.platformVersion,
3408
- projectName: "agent-qa",
3409
- buildName: "agent-qa-" + Date.now(),
3410
- sessionName: opts.testName,
3411
- idleTimeout: Math.ceil(opts.testTimeout / 1e3)
3412
- }
3413
- };
3414
- }
3415
-
3416
- // src/farm/browserstack.ts
3417
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
3418
- import { basename as basename2, resolve as resolvePath2 } from "path";
3419
- function isAbsoluteAppValue(value) {
3420
- return value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
3421
- }
3422
- function isExplicitRelativePath(value) {
3423
- return value.startsWith("./") || value.startsWith("../");
3424
- }
3425
- async function resolveBrowserStackApp(config) {
3426
- if (!config.app) return void 0;
3427
- if (isAbsoluteAppValue(config.app)) {
3428
- throw new MobileSetupError({
3429
- category: "app-install",
3430
- message: `BrowserStack app value must be a BrowserStack reference or relative upload path: ${config.app}`,
3431
- platform: config.platform,
3432
- appId: config.app,
3433
- sourceTrace: config.appSourceTrace
3434
- });
3435
- }
3436
- const appBaseDir = config.appBaseDir ?? process.cwd();
3437
- const resolvedPath = resolvePath2(appBaseDir, config.app);
3438
- const mustExist = isExplicitRelativePath(config.app);
3439
- if (!existsSync5(resolvedPath)) {
3440
- if (mustExist) {
3441
- throw new MobileSetupError({
3442
- category: "app-install",
3443
- message: `Configured BrowserStack app upload path not found: ${resolvedPath}`,
3444
- platform: config.platform,
3445
- appId: config.app,
3446
- sourceTrace: config.appSourceTrace
3447
- });
3448
- }
3449
- return config.app;
3450
- }
3451
- const auth = Buffer.from(`${config.credentials.username}:${config.credentials.accessKey}`).toString("base64");
3452
- const form = new FormData();
3453
- form.append("file", new Blob([readFileSync2(resolvedPath)]), basename2(resolvedPath));
3454
- let res;
3455
- try {
3456
- res = await fetch("https://api-cloud.browserstack.com/app-automate/upload", {
3457
- method: "POST",
3458
- headers: { Authorization: `Basic ${auth}` },
3459
- body: form
3460
- });
3461
- } catch (err) {
3462
- throw new MobileSetupError({
3463
- category: "app-install",
3464
- message: `Failed to upload BrowserStack app: ${err instanceof Error ? err.message : String(err)}`,
3465
- platform: config.platform,
3466
- appId: config.app,
3467
- sourceTrace: config.appSourceTrace,
3468
- cause: err
3469
- });
3470
- }
3471
- if (!res.ok) {
3472
- throw new MobileSetupError({
3473
- category: "app-install",
3474
- message: `Failed to upload BrowserStack app: HTTP ${res.status}`,
3475
- platform: config.platform,
3476
- appId: config.app,
3477
- sourceTrace: config.appSourceTrace
3478
- });
3479
- }
3480
- const body = await res.json();
3481
- if (!body.app_url) {
3482
- throw new MobileSetupError({
3483
- category: "app-install",
3484
- message: "Failed to upload BrowserStack app: response missing app_url",
3485
- platform: config.platform,
3486
- appId: config.app,
3487
- sourceTrace: config.appSourceTrace
3488
- });
3489
- }
3490
- return body.app_url;
3491
- }
3492
- var browserstackProvider = {
3493
- name: "BrowserStack",
3494
- slug: "browserstack",
3495
- async resolveWebEndpoint(config) {
3496
- const caps = mapWebCapabilities(config.browser, config.credentials, {
3497
- testName: config.testName,
3498
- playwrightVersion: config.playwrightVersion
3499
- });
3500
- const wsEndpoint = `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify(caps))}`;
3501
- return { wsEndpoint, capabilities: caps };
3502
- },
3503
- async resolveMobileCapabilities(config) {
3504
- const bstackCaps = mapMobileCapabilities(config.match, config.platform, config.credentials, {
3505
- testName: config.testName,
3506
- testTimeout: config.testTimeout
3507
- });
3508
- const automationName = config.match.automationName ?? (config.platform === "android" ? "UiAutomator2" : "XCUITest");
3509
- const platformName = config.platform === "android" ? "Android" : "iOS";
3510
- const capabilities = {
3511
- platformName,
3512
- "appium:automationName": automationName
3513
- };
3514
- for (const [key, value] of Object.entries(config.match)) {
3515
- if (key !== "automationName" && key !== "noReset") {
3516
- capabilities[`appium:${key}`] = value;
3517
- }
3518
- }
3519
- Object.assign(capabilities, bstackCaps);
3520
- const browserName = typeof config.match.browserName === "string" ? config.match.browserName.trim() : "";
3521
- const appStateNoReset = config.appState === "preserve" ? true : config.appState === "reset" ? false : void 0;
3522
- if (!browserName && appStateNoReset !== void 0) {
3523
- capabilities["appium:noReset"] = appStateNoReset;
3524
- }
3525
- const resolvedAppValue = await resolveBrowserStackApp(config);
3526
- if (resolvedAppValue) {
3527
- capabilities["appium:app"] = resolvedAppValue;
3528
- }
3529
- return {
3530
- hostname: "hub.browserstack.com",
3531
- port: 443,
3532
- path: "/wd/hub",
3533
- capabilities
3534
- };
3535
- },
3536
- resolveCredentials(providerConfig) {
3537
- return resolveFarmCredentials("browserstack", providerConfig);
3538
- },
3539
- async validateDevice(deviceName, platform) {
3540
- let credentials;
3541
- try {
3542
- credentials = resolveFarmCredentials("browserstack");
3543
- } catch {
3544
- return { valid: false, suggestions: ["Could not validate \u2014 credentials not configured"] };
3545
- }
3546
- const auth = Buffer.from(`${credentials.username}:${credentials.accessKey}`).toString("base64");
3547
- try {
3548
- const res = await fetch("https://api-cloud.browserstack.com/app-automate/devices.json", {
3549
- headers: { Authorization: `Basic ${auth}` }
3550
- });
3551
- if (!res.ok) {
3552
- return { valid: false, suggestions: ["Could not validate \u2014 API returned " + res.status] };
3553
- }
3554
- const devices = await res.json();
3555
- const targetOs = platform === "android" ? "android" : "ios";
3556
- const match = devices.find(
3557
- (d) => d.device.toLowerCase() === deviceName.toLowerCase() && d.os.toLowerCase() === targetOs
3558
- );
3559
- if (match) return { valid: true };
3560
- const similar = devices.filter((d) => d.os.toLowerCase() === targetOs).filter((d) => d.device.toLowerCase().includes(deviceName.toLowerCase().split(" ")[0])).map((d) => d.device).slice(0, 5);
3561
- return { valid: false, suggestions: similar.length > 0 ? similar : void 0 };
3562
- } catch {
3563
- return { valid: false, suggestions: ["Could not validate \u2014 network error"] };
3564
- }
3565
- },
3566
- getSessionUrl(sessionId, type) {
3567
- if (type === "web") {
3568
- return `https://automate.browserstack.com/sessions/${sessionId}`;
3569
- }
3570
- return `https://app-automate.browserstack.com/sessions/${sessionId}`;
3571
- }
3572
- };
3573
-
3574
- // src/farm/registry.ts
3575
- var providers = /* @__PURE__ */ new Map();
3576
- function registerProvider(provider) {
3577
- providers.set(provider.slug, provider);
3578
- }
3579
- function getProvider(slug) {
3580
- return providers.get(slug);
3581
- }
3582
- function listProviders() {
3583
- return Array.from(providers.values());
3584
- }
3585
- function registerAllProviders() {
3586
- registerProvider(browserstackProvider);
3587
- }
3588
-
3589
3357
  // src/suite/member.ts
3590
3358
  function isPlainObject2(val) {
3591
3359
  return typeof val === "object" && val !== null && !Array.isArray(val);
@@ -3621,7 +3389,7 @@ function patchTestForSuite(test, suite, _index) {
3621
3389
 
3622
3390
  // src/validation/validate.ts
3623
3391
  import { readFile as readFile8 } from "fs/promises";
3624
- import { existsSync as existsSync7 } from "fs";
3392
+ import { existsSync as existsSync6 } from "fs";
3625
3393
  import path6 from "path";
3626
3394
  import { parse as parseYaml3, LineCounter as LineCounter2, parseDocument } from "yaml";
3627
3395
  import { glob as glob2 } from "glob";
@@ -3670,7 +3438,7 @@ function isPathInsideDir(childPath, parentDir) {
3670
3438
  }
3671
3439
 
3672
3440
  // src/workspace/workspace-paths.ts
3673
- import { existsSync as existsSync6 } from "fs";
3441
+ import { existsSync as existsSync5 } from "fs";
3674
3442
  import path5 from "path";
3675
3443
  import { glob } from "glob";
3676
3444
  function toForwardSlash(value) {
@@ -3785,7 +3553,7 @@ function resolveWorkspacePaths(input) {
3785
3553
  ["workspace.envFile", resolved.envFile],
3786
3554
  ["workspace.secretsFile", resolved.secretsFile]
3787
3555
  ]) {
3788
- if (!existsSync6(file.absolutePath)) {
3556
+ if (!existsSync5(file.absolutePath)) {
3789
3557
  throw new Error(`Configured workspace file not found: ${key} -> ${file.absolutePath}`);
3790
3558
  }
3791
3559
  }
@@ -3816,7 +3584,7 @@ async function resolveWorkspaceFileTarget(input) {
3816
3584
  })) {
3817
3585
  throw new Error(`Workspace ${input.kind} file is not matched by configured workspace patterns: ${workspaceRelativePath}`);
3818
3586
  }
3819
- if (input.requireExisting && !existsSync6(absolutePath)) {
3587
+ if (input.requireExisting && !existsSync5(absolutePath)) {
3820
3588
  throw new Error(`Workspace ${input.kind} file not found: ${workspaceRelativePath}`);
3821
3589
  }
3822
3590
  return {
@@ -3938,7 +3706,7 @@ function validateSuiteFile(filePath, content, basedir) {
3938
3706
  continue;
3939
3707
  }
3940
3708
  const resolved = path6.resolve(basedir, testPath);
3941
- if (!existsSync7(resolved)) {
3709
+ if (!existsSync6(resolved)) {
3942
3710
  let line = 1;
3943
3711
  let column = 1;
3944
3712
  const seqNode = testsNode;
@@ -3959,13 +3727,13 @@ function validateSuiteFile(filePath, content, basedir) {
3959
3727
  return diagnostics;
3960
3728
  }
3961
3729
  function validateFilename(filePath) {
3962
- const basename3 = path6.basename(filePath);
3963
- if (!VALID_FILENAME_RE.test(basename3)) {
3730
+ const basename2 = path6.basename(filePath);
3731
+ if (!VALID_FILENAME_RE.test(basename2)) {
3964
3732
  return [{
3965
3733
  file: filePath,
3966
3734
  line: 1,
3967
3735
  column: 1,
3968
- message: `Invalid filename "${basename3}" \u2014 expected pattern: [a-zA-Z0-9._-]+.(suite.)?(yaml|yml)`,
3736
+ message: `Invalid filename "${basename2}" \u2014 expected pattern: [a-zA-Z0-9._-]+.(suite.)?(yaml|yml)`,
3969
3737
  severity: "warning"
3970
3738
  }];
3971
3739
  }
@@ -3973,7 +3741,7 @@ function validateFilename(filePath) {
3973
3741
  }
3974
3742
  async function validateHooksFile(hooksFilePath, options = {}) {
3975
3743
  const diagnostics = [];
3976
- if (!existsSync7(hooksFilePath)) {
3744
+ if (!existsSync6(hooksFilePath)) {
3977
3745
  if (options.required) {
3978
3746
  return [buildDiagnostic(hooksFilePath, `Configured hooks file not found: ${hooksFilePath}`)];
3979
3747
  }
@@ -3993,7 +3761,7 @@ async function validateHooksFile(hooksFilePath, options = {}) {
3993
3761
  return diagnostics;
3994
3762
  }
3995
3763
  for (const hook of hooks) {
3996
- if (!existsSync7(hook.file)) {
3764
+ if (!existsSync6(hook.file)) {
3997
3765
  diagnostics.push({
3998
3766
  file: hooksFilePath,
3999
3767
  line: 1,
@@ -4003,7 +3771,7 @@ async function validateHooksFile(hooksFilePath, options = {}) {
4003
3771
  });
4004
3772
  }
4005
3773
  for (const dep of hook.deps) {
4006
- if (!existsSync7(dep)) {
3774
+ if (!existsSync6(dep)) {
4007
3775
  diagnostics.push({
4008
3776
  file: hooksFilePath,
4009
3777
  line: 1,
@@ -4013,7 +3781,7 @@ async function validateHooksFile(hooksFilePath, options = {}) {
4013
3781
  });
4014
3782
  }
4015
3783
  }
4016
- if (hook.packageFile && !existsSync7(hook.packageFile)) {
3784
+ if (hook.packageFile && !existsSync6(hook.packageFile)) {
4017
3785
  diagnostics.push({
4018
3786
  file: hooksFilePath,
4019
3787
  line: 1,
@@ -4127,7 +3895,7 @@ function validateRequiredWorkspaceFiles(workspace) {
4127
3895
  ["workspace.envFile", workspace.envFile],
4128
3896
  ["workspace.secretsFile", workspace.secretsFile]
4129
3897
  ]) {
4130
- if (!existsSync7(file.absolutePath)) {
3898
+ if (!existsSync6(file.absolutePath)) {
4131
3899
  diagnostics.push(buildDiagnostic(file.absolutePath, `Configured workspace file not found: ${key} -> ${file.absolutePath}`));
4132
3900
  }
4133
3901
  }
@@ -4188,7 +3956,7 @@ async function validateProject(options = {}) {
4188
3956
  if (d.severity === "error") result.errorCount++;
4189
3957
  else result.warningCount++;
4190
3958
  }
4191
- if (hooksFilePath && hooksDiags.length === 0 && existsSync7(hooksFilePath)) {
3959
+ if (hooksFilePath && hooksDiags.length === 0 && existsSync6(hooksFilePath)) {
4192
3960
  const { hooks } = await parseHooksFile(hooksFilePath);
4193
3961
  const hookIds = new Set(hooks.map((h) => h.id));
4194
3962
  for (const filePath of filePaths) {
@@ -4293,7 +4061,7 @@ function warnIfOutOfBounds(coords, viewport, actionType) {
4293
4061
  }
4294
4062
 
4295
4063
  // src/hooks/index.ts
4296
- import { RUNTIME_IMAGE_MAP } from "@vostride/agent-qa-runner";
4064
+ import { RUNTIME_IMAGE_MAP, hookRunnerImage } from "@vostride/agent-qa-runner";
4297
4065
 
4298
4066
  // src/ids/hook-id.ts
4299
4067
  import { generateHookId as generateSharedHookId } from "@vostride/agent-qa-ids";
@@ -4317,8 +4085,6 @@ var ATTR_RUNNER = "agent-qa.runner";
4317
4085
  var ATTR_GATE_RUN = "agent-qa.gate-run";
4318
4086
  var RUN_ATTRIBUTE_KEY_MAX_LENGTH = 256;
4319
4087
  var RUN_ATTRIBUTE_VALUE_MAX_LENGTH = 4096;
4320
- var RUN_ATTRIBUTE_TRIGGERS = ["cli", "dashboard", "api", "mcp"];
4321
- var RUN_ATTRIBUTE_RUNNERS = ["local", "browserstack"];
4322
4088
  function isPlainRecord(value) {
4323
4089
  return typeof value === "object" && value !== null && !Array.isArray(value);
4324
4090
  }
@@ -4333,43 +4099,6 @@ function assertValidUserAttributeKey(key, sourceLabel) {
4333
4099
  throw new Error(`${sourceLabel}: Attribute key "${key}" exceeds ${RUN_ATTRIBUTE_KEY_MAX_LENGTH} characters`);
4334
4100
  }
4335
4101
  }
4336
- function assertValidAttributeKeyShape(key, sourceLabel) {
4337
- if (key.length === 0) {
4338
- throw new Error(`${sourceLabel}: Attribute key must be non-empty`);
4339
- }
4340
- if (key.length > RUN_ATTRIBUTE_KEY_MAX_LENGTH) {
4341
- throw new Error(`${sourceLabel}: Attribute key "${key}" exceeds ${RUN_ATTRIBUTE_KEY_MAX_LENGTH} characters`);
4342
- }
4343
- }
4344
- function assertValidAttributeValue(key, value, sourceLabel) {
4345
- if (typeof value !== "string") {
4346
- throw new Error(`${sourceLabel}: Attribute value for "${key}" must be a string`);
4347
- }
4348
- if (value.length > RUN_ATTRIBUTE_VALUE_MAX_LENGTH) {
4349
- throw new Error(`${sourceLabel}: Attribute value for "${key}" exceeds ${RUN_ATTRIBUTE_VALUE_MAX_LENGTH} characters`);
4350
- }
4351
- }
4352
- function assertValidInternalAttribute(key, value, sourceLabel) {
4353
- if (key === ATTR_TRIGGER) {
4354
- if (!RUN_ATTRIBUTE_TRIGGERS.includes(value)) {
4355
- throw new Error(`${sourceLabel}: Attribute value for "${key}" must be one of: ${RUN_ATTRIBUTE_TRIGGERS.join(", ")}`);
4356
- }
4357
- return;
4358
- }
4359
- if (key === ATTR_RUNNER) {
4360
- if (!RUN_ATTRIBUTE_RUNNERS.includes(value)) {
4361
- throw new Error(`${sourceLabel}: Attribute value for "${key}" must be one of: ${RUN_ATTRIBUTE_RUNNERS.join(", ")}`);
4362
- }
4363
- return;
4364
- }
4365
- if (key === ATTR_GATE_RUN) {
4366
- if (value !== "true") {
4367
- throw new Error(`${sourceLabel}: Attribute value for "${key}" must be "true"`);
4368
- }
4369
- return;
4370
- }
4371
- throw new Error(`${sourceLabel}: Attribute key "${key}" uses the reserved prefix "${INTERNAL_ATTRIBUTE_PREFIX}"`);
4372
- }
4373
4102
  function validateUserRunAttributes(input, sourceLabel = "run attributes") {
4374
4103
  if (input === void 0 || input === null) return {};
4375
4104
  if (!isPlainRecord(input)) {
@@ -4388,22 +4117,6 @@ function validateUserRunAttributes(input, sourceLabel = "run attributes") {
4388
4117
  }
4389
4118
  return attributes;
4390
4119
  }
4391
- function validateTrustedRunAttributes(input, sourceLabel = "run attributes") {
4392
- if (input === void 0 || input === null) return {};
4393
- if (!isPlainRecord(input)) {
4394
- throw new Error(`${sourceLabel}: attributes must be a plain object`);
4395
- }
4396
- const attributes = /* @__PURE__ */ Object.create(null);
4397
- for (const [key, value] of Object.entries(input)) {
4398
- assertValidAttributeKeyShape(key, sourceLabel);
4399
- assertValidAttributeValue(key, value, sourceLabel);
4400
- if (key.startsWith(INTERNAL_ATTRIBUTE_PREFIX)) {
4401
- assertValidInternalAttribute(key, value, sourceLabel);
4402
- }
4403
- attributes[key] = value;
4404
- }
4405
- return attributes;
4406
- }
4407
4120
  function parseRunAttrFlags(flags) {
4408
4121
  const rawAttributes = /* @__PURE__ */ Object.create(null);
4409
4122
  const seen = /* @__PURE__ */ new Set();
@@ -4463,8 +4176,8 @@ function formatRunAttributesBlock(attributes) {
4463
4176
  }
4464
4177
 
4465
4178
  // src/version.ts
4466
- import { createRequire as createRequire3 } from "module";
4467
- var require2 = createRequire3(typeof __filename === "string" ? __filename : import.meta.url);
4179
+ import { createRequire as createRequire2 } from "module";
4180
+ var require2 = createRequire2(typeof __filename === "string" ? __filename : import.meta.url);
4468
4181
  function readPackageVersion() {
4469
4182
  try {
4470
4183
  const manifest = require2("../package.json");
@@ -4482,10 +4195,10 @@ function getAgentQaVersion() {
4482
4195
 
4483
4196
  // src/version-check/index.ts
4484
4197
  import { chmod, mkdir as mkdir3, readFile as readFile9, writeFile as writeFile3 } from "fs/promises";
4485
- import { createRequire as createRequire4 } from "module";
4198
+ import { createRequire as createRequire3 } from "module";
4486
4199
  import { homedir as homedir2 } from "os";
4487
4200
  import { dirname as dirname7, join as join3 } from "path";
4488
- var require3 = createRequire4(typeof __filename === "string" ? __filename : import.meta.url);
4201
+ var require3 = createRequire3(typeof __filename === "string" ? __filename : import.meta.url);
4489
4202
  var AGENT_QA_NPM_REGISTRY_URL = "https://registry.npmjs.org/agent-qa";
4490
4203
  var VERSION_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
4491
4204
  var VERSION_CHECK_TIMEOUT_MS = 5e3;
@@ -4670,7 +4383,6 @@ export {
4670
4383
  AUTH_STATE_SCHEMA_VERSION,
4671
4384
  AUTH_STATE_SLUG_PATTERN,
4672
4385
  AccessibilityConfigSchema,
4673
- ActionPlanSchema,
4674
4386
  AdapterLogDataSchema,
4675
4387
  AgentLogDataSchema,
4676
4388
  AgentQaConfigSchema,
@@ -4727,7 +4439,6 @@ export {
4727
4439
  PlannerConfigSchema,
4728
4440
  PlannerLogDataSchema,
4729
4441
  PluginsSchema,
4730
- ProviderConfigSchema,
4731
4442
  ProviderPluginDeclarationSchema,
4732
4443
  RUNTIME_IMAGE_MAP,
4733
4444
  RUN_ATTRIBUTE_KEY_MAX_LENGTH,
@@ -4745,7 +4456,6 @@ export {
4745
4456
  TestMetaSchema,
4746
4457
  TestStepSchema,
4747
4458
  TimeoutConfigSchema,
4748
- TransportSchema,
4749
4459
  UseSchema,
4750
4460
  VALID_FILENAME_RE,
4751
4461
  VERSION_CHECK_CACHE_TTL_MS,
@@ -4753,7 +4463,6 @@ export {
4753
4463
  WEBVIEW_SCROLLERS_SCRIPT,
4754
4464
  WorkspaceSchema,
4755
4465
  applyPatchSetToTestDoc,
4756
- browserstackProvider,
4757
4466
  buildAuthStateHookEnv,
4758
4467
  buildInternalRunAttributes,
4759
4468
  buildRefEntries,
@@ -4766,7 +4475,6 @@ export {
4766
4475
  createAuthFetch,
4767
4476
  deriveMobileScroll,
4768
4477
  detectGlobOverlap,
4769
- detectPlaywrightVersion,
4770
4478
  discoverWorkspaceFiles,
4771
4479
  estimateTokens,
4772
4480
  formatAppiumInstallGuidance,
@@ -4785,21 +4493,21 @@ export {
4785
4493
  getAuthPath,
4786
4494
  getCredential,
4787
4495
  getLLMAuthProviderPlugin,
4788
- getProvider,
4496
+ guardJsonText,
4497
+ guardRunText,
4498
+ hookRunnerImage,
4789
4499
  isCanonicalRunId,
4500
+ isGitHubActions,
4790
4501
  isLoopbackHost,
4791
4502
  isPathInsideDir,
4792
4503
  isReservedAuthStateHookEnvKey,
4793
4504
  isWorkspacePathMatch,
4794
4505
  listAuthStateMetadata,
4795
4506
  listLLMAuthProviderPlugins,
4796
- listProviders,
4797
4507
  loadLLMAuthPlugins,
4798
4508
  loadProviderPlugins,
4799
4509
  localAppiumBinaryNames,
4800
4510
  locateRunsDbPath,
4801
- mapMobileCapabilities,
4802
- mapWebCapabilities,
4803
4511
  mergeRunAttributes,
4804
4512
  normalizeAuthStateUse,
4805
4513
  normalizeRole,
@@ -4814,21 +4522,21 @@ export {
4814
4522
  parseTestFileWithTokens,
4815
4523
  parseWebViewScrollers,
4816
4524
  patchTestForSuite,
4525
+ printRunText,
4817
4526
  readAuth,
4818
4527
  readAuthStateMetadata,
4819
- registerAllProviders,
4528
+ refuseRetiredLocalBindings,
4820
4529
  registerLLMAuthProviderPlugin,
4821
4530
  registerLLMAuthProviderPlugins,
4822
- registerProvider,
4823
4531
  removeAuth,
4824
4532
  removeAuthStateFiles,
4825
4533
  removeAuthStateTarget,
4826
4534
  renderPatchedTestDoc,
4535
+ reserveStdoutForData,
4827
4536
  resolveAppiumExecutable,
4828
4537
  resolveAuthStateForRun,
4829
4538
  resolveAuthStatePaths,
4830
4539
  resolveAuthStateRoot,
4831
- resolveFarmCredentials,
4832
4540
  resolveFileReferences,
4833
4541
  resolveKnowledgeRoot,
4834
4542
  resolveLLMAuth,
@@ -4844,7 +4552,6 @@ export {
4844
4552
  validateProject,
4845
4553
  validateSuiteFile,
4846
4554
  validateTestFile,
4847
- validateTrustedRunAttributes,
4848
4555
  validateUserRunAttributes,
4849
4556
  warnIfOutOfBounds,
4850
4557
  webViewSwipeSignal,