@vostride/agent-qa-core 0.2.3 → 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.cjs CHANGED
@@ -43,7 +43,6 @@ __export(index_exports, {
43
43
  AUTH_STATE_SCHEMA_VERSION: () => AUTH_STATE_SCHEMA_VERSION,
44
44
  AUTH_STATE_SLUG_PATTERN: () => AUTH_STATE_SLUG_PATTERN,
45
45
  AccessibilityConfigSchema: () => AccessibilityConfigSchema,
46
- ActionPlanSchema: () => ActionPlanSchema,
47
46
  AdapterLogDataSchema: () => AdapterLogDataSchema,
48
47
  AgentLogDataSchema: () => AgentLogDataSchema,
49
48
  AgentQaConfigSchema: () => AgentQaConfigSchema,
@@ -100,7 +99,6 @@ __export(index_exports, {
100
99
  PlannerConfigSchema: () => PlannerConfigSchema,
101
100
  PlannerLogDataSchema: () => PlannerLogDataSchema,
102
101
  PluginsSchema: () => PluginsSchema,
103
- ProviderConfigSchema: () => ProviderConfigSchema,
104
102
  ProviderPluginDeclarationSchema: () => ProviderPluginDeclarationSchema,
105
103
  RUNTIME_IMAGE_MAP: () => import_agent_qa_runner6.RUNTIME_IMAGE_MAP,
106
104
  RUN_ATTRIBUTE_KEY_MAX_LENGTH: () => RUN_ATTRIBUTE_KEY_MAX_LENGTH,
@@ -118,7 +116,6 @@ __export(index_exports, {
118
116
  TestMetaSchema: () => TestMetaSchema,
119
117
  TestStepSchema: () => TestStepSchema,
120
118
  TimeoutConfigSchema: () => TimeoutConfigSchema,
121
- TransportSchema: () => TransportSchema,
122
119
  UseSchema: () => UseSchema,
123
120
  VALID_FILENAME_RE: () => VALID_FILENAME_RE,
124
121
  VERSION_CHECK_CACHE_TTL_MS: () => VERSION_CHECK_CACHE_TTL_MS,
@@ -126,7 +123,6 @@ __export(index_exports, {
126
123
  WEBVIEW_SCROLLERS_SCRIPT: () => WEBVIEW_SCROLLERS_SCRIPT,
127
124
  WorkspaceSchema: () => WorkspaceSchema,
128
125
  applyPatchSetToTestDoc: () => applyPatchSetToTestDoc,
129
- browserstackProvider: () => browserstackProvider,
130
126
  buildAuthStateHookEnv: () => import_agent_qa_runner4.buildAuthStateHookEnv,
131
127
  buildInternalRunAttributes: () => buildInternalRunAttributes,
132
128
  buildRefEntries: () => buildRefEntries,
@@ -139,7 +135,6 @@ __export(index_exports, {
139
135
  createAuthFetch: () => createAuthFetch,
140
136
  deriveMobileScroll: () => deriveMobileScroll,
141
137
  detectGlobOverlap: () => detectGlobOverlap,
142
- detectPlaywrightVersion: () => detectPlaywrightVersion,
143
138
  discoverWorkspaceFiles: () => discoverWorkspaceFiles,
144
139
  estimateTokens: () => estimateTokens,
145
140
  formatAppiumInstallGuidance: () => formatAppiumInstallGuidance,
@@ -158,21 +153,21 @@ __export(index_exports, {
158
153
  getAuthPath: () => getAuthPath,
159
154
  getCredential: () => getCredential,
160
155
  getLLMAuthProviderPlugin: () => getLLMAuthProviderPlugin,
161
- getProvider: () => getProvider,
156
+ guardJsonText: () => guardJsonText,
157
+ guardRunText: () => guardRunText,
158
+ hookRunnerImage: () => import_agent_qa_runner6.hookRunnerImage,
162
159
  isCanonicalRunId: () => isCanonicalRunId,
160
+ isGitHubActions: () => isGitHubActions,
163
161
  isLoopbackHost: () => isLoopbackHost,
164
162
  isPathInsideDir: () => isPathInsideDir,
165
163
  isReservedAuthStateHookEnvKey: () => import_agent_qa_runner4.isReservedAuthStateHookEnvKey,
166
164
  isWorkspacePathMatch: () => isWorkspacePathMatch,
167
165
  listAuthStateMetadata: () => listAuthStateMetadata,
168
166
  listLLMAuthProviderPlugins: () => listLLMAuthProviderPlugins,
169
- listProviders: () => listProviders,
170
167
  loadLLMAuthPlugins: () => loadLLMAuthPlugins,
171
168
  loadProviderPlugins: () => loadProviderPlugins,
172
169
  localAppiumBinaryNames: () => localAppiumBinaryNames,
173
170
  locateRunsDbPath: () => locateRunsDbPath,
174
- mapMobileCapabilities: () => mapMobileCapabilities,
175
- mapWebCapabilities: () => mapWebCapabilities,
176
171
  mergeRunAttributes: () => mergeRunAttributes,
177
172
  normalizeAuthStateUse: () => normalizeAuthStateUse,
178
173
  normalizeRole: () => normalizeRole,
@@ -187,21 +182,21 @@ __export(index_exports, {
187
182
  parseTestFileWithTokens: () => parseTestFileWithTokens,
188
183
  parseWebViewScrollers: () => parseWebViewScrollers,
189
184
  patchTestForSuite: () => patchTestForSuite,
185
+ printRunText: () => printRunText,
190
186
  readAuth: () => readAuth,
191
187
  readAuthStateMetadata: () => readAuthStateMetadata,
192
- registerAllProviders: () => registerAllProviders,
188
+ refuseRetiredLocalBindings: () => refuseRetiredLocalBindings,
193
189
  registerLLMAuthProviderPlugin: () => registerLLMAuthProviderPlugin,
194
190
  registerLLMAuthProviderPlugins: () => registerLLMAuthProviderPlugins,
195
- registerProvider: () => registerProvider,
196
191
  removeAuth: () => removeAuth,
197
192
  removeAuthStateFiles: () => removeAuthStateFiles,
198
193
  removeAuthStateTarget: () => removeAuthStateTarget,
199
194
  renderPatchedTestDoc: () => renderPatchedTestDoc,
195
+ reserveStdoutForData: () => reserveStdoutForData,
200
196
  resolveAppiumExecutable: () => resolveAppiumExecutable,
201
197
  resolveAuthStateForRun: () => resolveAuthStateForRun,
202
198
  resolveAuthStatePaths: () => resolveAuthStatePaths,
203
199
  resolveAuthStateRoot: () => resolveAuthStateRoot,
204
- resolveFarmCredentials: () => resolveFarmCredentials,
205
200
  resolveFileReferences: () => resolveFileReferences,
206
201
  resolveKnowledgeRoot: () => resolveKnowledgeRoot,
207
202
  resolveLLMAuth: () => resolveLLMAuth,
@@ -217,7 +212,6 @@ __export(index_exports, {
217
212
  validateProject: () => validateProject,
218
213
  validateSuiteFile: () => validateSuiteFile,
219
214
  validateTestFile: () => validateTestFile,
220
- validateTrustedRunAttributes: () => validateTrustedRunAttributes,
221
215
  validateUserRunAttributes: () => validateUserRunAttributes,
222
216
  warnIfOutOfBounds: () => warnIfOutOfBounds,
223
217
  webViewSwipeSignal: () => webViewSwipeSignal,
@@ -352,7 +346,9 @@ var NamedLLMConfigSchema = ModelConfigBaseSchema.extend({
352
346
  var TimeoutConfigSchema = import_zod2.z.object({
353
347
  step: DurationString,
354
348
  test: DurationString,
355
- navigation: DurationString
349
+ navigation: DurationString.describe(
350
+ "How long a web page load or URL wait can take. Default 2m. Android and iOS runs do not use it."
351
+ )
356
352
  }).strict();
357
353
  var BrowserConfigSchema = import_zod2.z.object({
358
354
  name: import_zod2.z.enum(["chromium", "firefox", "webkit"]),
@@ -362,7 +358,6 @@ var BrowserConfigSchema = import_zod2.z.object({
362
358
  height: import_zod2.z.number()
363
359
  }).strict().optional()
364
360
  }).strict();
365
- var TransportSchema = import_zod2.z.enum(["local", "browserstack"]);
366
361
  var PlannerConfigSchema = import_zod2.z.object({
367
362
  maxActions: import_zod2.z.number().int().positive().optional(),
368
363
  maxPlanningTurns: import_zod2.z.number().int().positive().optional(),
@@ -386,69 +381,28 @@ var DashboardConfigSchema = import_zod3.z.object({
386
381
  port: import_zod3.z.number().optional(),
387
382
  dbPath: import_zod3.z.string().optional(),
388
383
  artifactsDir: import_zod3.z.string().optional(),
389
- // Phase 387, D-10: where an entity card's `url` points. BOTH optional, so the v52 default
390
- // is a card with no `url` at all rather than a link to a route nothing serves. The card
391
- // producer reads these through the masked config reader and renders
392
- // `ENTITY_URL_ROUTE` (@vostride/agent-qa-schema) against them.
393
- //
394
- // They live on the EXISTING `services.dashboard` block rather than on a new top-level
395
- // `dashboard` key: this project already has one dashboard configuration home, and a second
396
- // one would be two places an operator could reasonably write the same fact.
397
- //
398
- // 2026-09-01 (Phase 408 plan 10, WR-04 — T-408-16): REFINED TO AN ABSOLUTE http(s) URL, because
399
- // this value stops being a display string in M5. It composes the tokenless live link
400
- // (`live-link.ts`'s `liveRunLinkOf`, which only strips trailing slashes), and that link is
401
- // assigned to an `href` INSIDE the served App document — which runs with `allow-scripts` at an
402
- // opaque origin — as well as appended verbatim to every carrier tool result's model-readable
403
- // text. A `javascript:` base therefore composed a script-executing anchor. The trust boundary is
404
- // the operator, so this is not remote-triggerable; the finding is that a URL-shaped value reached
405
- // an `href` with no scheme check at any hop, and the guard costs one refinement.
406
- //
407
- // AT THE PARSE BOUNDARY, so there is exactly one place that decides. The defensive drop at the
408
- // anchor itself is the SECOND half of the same finding and lives in `app-wrapper.ts`.
409
- //
410
- // STILL `.optional()`: an absent base is a real and honest posture — a standalone `agent-qa mcp`
411
- // with no dashboard — and the carrier result names that absence rather than fabricating a URL.
412
- // An EMPTY STRING is now REFUSED, which is stricter than before: `""` is not an absence an
413
- // operator wrote by leaving the key out, it is a value that composes a relative fragment reading
414
- // as a link and resolving against nothing.
384
+ // Only an absolute http(s) URL: the base composes an `href` inside the served App document.
415
385
  baseUrl: import_zod3.z.string().refine(
416
386
  isAbsoluteHttpUrl,
417
387
  { message: "services.dashboard.baseUrl must be an absolute http(s) URL" }
418
388
  ).optional(),
419
389
  workspace: import_zod3.z.string().optional(),
420
- // 2026-10-01 (432-22, D-12/D-22): `runServiceTokens` is deleted. The run service is tokenless
421
- // since 432-21, and a file that still declares the key gets the normal strict unrecognized-key
422
- // error, with no migration and no special message. The local doors never needed a credential.
423
- // Phase 402 D-07: the interface this server binds. Consumed by 402-05's pre-listen bind guard,
424
- // which REFUSES any non-loopback value — unconditionally since the phase-402 verification round
425
- // (a declared run-service credential authenticates the three run-service paths and nothing else,
426
- // so it was never evidence that the rest of this server's surface was authed).
427
- //
428
- // DELIBERATELY NOT `LoopbackHostSchema` even so. That schema refuses at PARSE time, which would
429
- // make the runtime refusal unreachable and would report the problem as a config-shape error
430
- // rather than as the security refusal it is — and it would also refuse a loopback SPELLING this
431
- // field exists to allow (`localhost`, `::1`, `127.0.0.2` is refused on purpose).
390
+ // The run service and the other local doors are tokenless, so this block declares no token key.
391
+ // The interface the dashboard binds. A plain string, because the pre-listen bind guard refuses
392
+ // any non-loopback value at runtime, as the security refusal it is rather than a config-shape
393
+ // error, and a loopback spelling other than the default (`localhost`, `::1`) stays configurable.
432
394
  host: import_zod3.z.string().trim().min(1).optional()
433
395
  }).strict();
434
396
  var LOOPBACK_HOSTS = Object.freeze(["localhost", "127.0.0.1", "::1"]);
435
397
  function isLoopbackHost(host) {
436
398
  return LOOPBACK_HOSTS.includes(host.trim());
437
399
  }
438
- var LoopbackHostSchema = import_zod3.z.string().trim().min(1).refine(
439
- isLoopbackHost,
440
- { message: "MCP host must be localhost, 127.0.0.1, or ::1 in local-only mode" }
441
- );
442
400
  var McpConfigSchema = import_zod3.z.object({
443
401
  enabled: import_zod3.z.boolean().optional(),
444
- transport: import_zod3.z.enum(["http", "stdio"]).optional(),
445
- host: LoopbackHostSchema.optional(),
446
- port: import_zod3.z.number().int().min(1).max(65535).optional(),
447
- path: import_zod3.z.string().trim().min(1).regex(/^\//, "MCP path must start with /").optional()
402
+ port: import_zod3.z.number().int().min(1).max(65535).optional()
448
403
  }).strict();
449
404
  var CacheConfigSchema = import_zod3.z.object({
450
- dir: import_zod3.z.string(),
451
- ttl: DurationString
405
+ dir: import_zod3.z.string()
452
406
  }).strict();
453
407
  var AuthStateConfigSchema = import_zod3.z.object({
454
408
  dir: import_zod3.z.string().trim().min(1)
@@ -463,7 +417,7 @@ var RecordingConfigSchema = import_zod3.z.object({
463
417
  var AccessibilityConfigSchema = import_zod3.z.object({
464
418
  enabled: import_zod3.z.boolean(),
465
419
  standard: import_zod3.z.enum(["wcag2a", "wcag2aa", "wcag2aaa"]).optional(),
466
- runAfter: import_zod3.z.enum(["every-step", "navigation", "test-end"]).optional(),
420
+ runAfter: import_zod3.z.enum(["every-step", "test-end"]).optional(),
467
421
  failOnViolation: import_zod3.z.boolean().optional(),
468
422
  disableRules: import_zod3.z.array(import_zod3.z.string()).optional(),
469
423
  exclude: import_zod3.z.array(import_zod3.z.string()).optional()
@@ -490,14 +444,7 @@ var isRepoRelative = (dir) => !/^[/\\]|^[A-Za-z]:[\\/]/.test(dir) && !/^\.\.(?:[
490
444
  var KnowledgeConfigSchema = import_zod4.z.object({
491
445
  dir: import_zod4.z.string().trim().min(1).refine(isRepoRelative, {
492
446
  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"
493
- }).default("agent-qa-knowledge"),
494
- // D-08: closed enum — deliberately no 'full' mode; full-res artifacts ride the
495
- // artifact store/git-LFS side channel per knowledge-store.md §5.
496
- commitArtifacts: import_zod4.z.enum(["none", "thumbnails"]).default("none"),
497
- // Absent = no cap: the engine's derived budget serves everything it ranks useful; when set
498
- // it is a hard ceiling min(cap, derived), clamped above min_brief — efficacy over token
499
- // saving (D-09, binds Phase 380 brief budgeting).
500
- contextBudgetTokens: import_zod4.z.number().int().positive().optional()
447
+ }).default("agent-qa-knowledge")
501
448
  }).strict();
502
449
 
503
450
  // src/schema/retired-keys.ts
@@ -518,6 +465,39 @@ var RETIRED_MEMORY_KEY_MESSAGES = {
518
465
  circuitBreakerBaselineSize: "services.memory.circuitBreakerBaselineSize was removed \u2014 no circuit breaker exists",
519
466
  circuitBreakerThreshold: "services.memory.circuitBreakerThreshold was removed \u2014 no circuit breaker exists"
520
467
  };
468
+ var RETIRED_EVAL_KEY_MESSAGES = {
469
+ gate1: "eval.gate1 was removed: memory is no longer screened by replay before promotion. Delete this block.",
470
+ gate2: "eval.gate2 was removed: completed runs no longer replay served memory. Delete this block.",
471
+ adversarial: "eval.adversarial was removed: runs no longer stage adversarial probes. Delete this block.",
472
+ seeding: "eval.seeding was removed: runs no longer seed an eval corpus. Delete this block.",
473
+ judge: "eval.judge was removed: pass --judge <name> to agent-qa eval replay. Delete this key.",
474
+ judges: "eval.judges was removed: repeat --judge <name> on agent-qa eval replay. Delete this key.",
475
+ judgeAllowSameFamily: "eval.judgeAllowSameFamily was removed: pass --judge-allow-same-family to agent-qa eval replay. Delete this key.",
476
+ k: "eval.k was removed: pass --k <n> to agent-qa eval replay. Delete this key.",
477
+ seed: "eval.seed was removed: pass --seed <n> to agent-qa eval replay. Delete this key."
478
+ };
479
+ var RETIRED_STEP_KEY_MESSAGES = {
480
+ retries: "steps[].retries was removed: a test retries as a whole; set meta.retries. Delete this line.",
481
+ screenshot: "steps[].screenshot was removed: screenshots are set per run; pass --screenshot-mode every-step to agent-qa run. Delete this line."
482
+ };
483
+ var RETIRED_EVAL_BLOCK_MESSAGE = "eval was removed: agent-qa eval replay takes its settings as flags. Delete this block.";
484
+ var RETIRED_ALLOW_PRODUCTION_MESSAGE = "allowProduction was removed: production targets run like any other target. Delete this line.";
485
+ var RETIRED_REGISTRY_PROVIDERS_MESSAGE = "registry.providers was removed: BrowserStack is no longer supported; runs use local Playwright and Appium. Delete this block.";
486
+ var RETIRED_DEVICE_TRANSPORT_MESSAGE = "registry.devices.<name>.transport was removed: devices run on local Appium. Delete this line.";
487
+ 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.";
488
+ var RETIRED_CACHE_TTL_MESSAGE = "services.cache.ttl was removed: cached plans do not expire by age. Delete this line.";
489
+ var RETIRED_MCP_KEY_MESSAGES = {
490
+ host: "services.mcp.host was removed: agent-qa mcp always listens on 127.0.0.1. Delete this line.",
491
+ path: "services.mcp.path was removed: the MCP server answers on any path. Delete this line.",
492
+ transport: "services.mcp.transport was removed: agent-qa mcp uses stdio, or HTTP with --http. Delete this line."
493
+ };
494
+ 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.";
495
+ var RETIRED_KNOWLEDGE_KEY_MESSAGES = {
496
+ contextBudgetTokens: "knowledge.contextBudgetTokens was removed: agent-qa sizes each run's knowledge itself. Delete this line.",
497
+ commitArtifacts: "knowledge.commitArtifacts was removed: knowledge bundles never commit images. Delete this line."
498
+ };
499
+ var RETIRED_LOCAL_PROVIDERS_MESSAGE = "providers was removed: BrowserStack is no longer supported. Delete this block.";
500
+ 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.";
521
501
  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)";
522
502
  var RETIRED_PLANNER_KEY = "maxSubActions";
523
503
  function isPlainObject(value) {
@@ -531,7 +511,7 @@ var retiredKeyScan = import_zod5.z.any().superRefine((value, ctx) => {
531
511
  if (isPlainObject(value.services) && value.services.memory !== void 0) {
532
512
  ctx.addIssue({
533
513
  code: import_zod5.z.ZodIssueCode.custom,
534
- params: { retired: true },
514
+ params: { retired: true, hardCut: true },
535
515
  path: ["services", "memory"],
536
516
  message: "services.memory was removed in v2 \u2014 the memory config surface collapsed into the top-level knowledge block"
537
517
  });
@@ -542,7 +522,7 @@ var retiredKeyScan = import_zod5.z.any().superRefine((value, ctx) => {
542
522
  if (message) {
543
523
  ctx.addIssue({
544
524
  code: import_zod5.z.ZodIssueCode.custom,
545
- params: { retired: true },
525
+ params: { retired: true, hardCut: true },
546
526
  path: ["services", "memory", key],
547
527
  message
548
528
  });
@@ -553,30 +533,164 @@ var retiredKeyScan = import_zod5.z.any().superRefine((value, ctx) => {
553
533
  if (isPlainObject(value.workspace) && value.workspace.agentRules !== void 0) {
554
534
  ctx.addIssue({
555
535
  code: import_zod5.z.ZodIssueCode.custom,
556
- params: { retired: true },
536
+ params: { retired: true, hardCut: true },
557
537
  path: ["workspace", "agentRules"],
558
538
  message: "workspace.agentRules was removed in v2 \u2014 rules dissolve into agent-qa-knowledge/policies/<scope>.md files"
559
539
  });
560
540
  }
561
- if (hasRetiredPlannerKey(value.use)) {
541
+ if (Object.hasOwn(value, "eval")) {
542
+ let named = 0;
543
+ if (isPlainObject(value.eval)) {
544
+ for (const key of Object.keys(value.eval)) {
545
+ if (Object.hasOwn(RETIRED_EVAL_KEY_MESSAGES, key)) {
546
+ named++;
547
+ ctx.addIssue({
548
+ code: import_zod5.z.ZodIssueCode.custom,
549
+ params: { retired: true },
550
+ path: ["eval", key],
551
+ message: RETIRED_EVAL_KEY_MESSAGES[key]
552
+ });
553
+ }
554
+ }
555
+ }
556
+ if (named === 0) {
557
+ ctx.addIssue({
558
+ code: import_zod5.z.ZodIssueCode.custom,
559
+ params: { retired: true },
560
+ path: ["eval"],
561
+ message: RETIRED_EVAL_BLOCK_MESSAGE
562
+ });
563
+ }
564
+ }
565
+ if (Object.hasOwn(value, "allowProduction")) {
566
+ ctx.addIssue({
567
+ code: import_zod5.z.ZodIssueCode.custom,
568
+ params: { retired: true },
569
+ path: ["allowProduction"],
570
+ message: RETIRED_ALLOW_PRODUCTION_MESSAGE
571
+ });
572
+ }
573
+ if (isPlainObject(value.registry) && Object.hasOwn(value.registry, "providers")) {
574
+ ctx.addIssue({
575
+ code: import_zod5.z.ZodIssueCode.custom,
576
+ params: { retired: true },
577
+ path: ["registry", "providers"],
578
+ message: RETIRED_REGISTRY_PROVIDERS_MESSAGE
579
+ });
580
+ }
581
+ if (isPlainObject(value.registry) && isPlainObject(value.registry.devices)) {
582
+ for (const [name, device] of Object.entries(value.registry.devices)) {
583
+ if (isPlainObject(device) && Object.hasOwn(device, "transport")) {
584
+ ctx.addIssue({
585
+ code: import_zod5.z.ZodIssueCode.custom,
586
+ params: { retired: true },
587
+ path: ["registry", "devices", name, "transport"],
588
+ message: RETIRED_DEVICE_TRANSPORT_MESSAGE
589
+ });
590
+ }
591
+ }
592
+ }
593
+ if (isPlainObject(value.registry) && isPlainObject(value.registry.targets)) {
594
+ for (const [name, target] of Object.entries(value.registry.targets)) {
595
+ if (isPlainObject(target) && isPlainObject(target.app) && Object.hasOwn(target.app, "browserstack")) {
596
+ ctx.addIssue({
597
+ code: import_zod5.z.ZodIssueCode.custom,
598
+ params: { retired: true },
599
+ path: ["registry", "targets", name, "app", "browserstack"],
600
+ message: RETIRED_APP_BROWSERSTACK_MESSAGE
601
+ });
602
+ }
603
+ }
604
+ }
605
+ if (isPlainObject(value.services) && isPlainObject(value.services.cache) && Object.hasOwn(value.services.cache, "ttl")) {
562
606
  ctx.addIssue({
563
607
  code: import_zod5.z.ZodIssueCode.custom,
564
608
  params: { retired: true },
609
+ path: ["services", "cache", "ttl"],
610
+ message: RETIRED_CACHE_TTL_MESSAGE
611
+ });
612
+ }
613
+ if (isPlainObject(value.services) && isPlainObject(value.services.mcp)) {
614
+ for (const key of Object.keys(value.services.mcp)) {
615
+ if (Object.hasOwn(RETIRED_MCP_KEY_MESSAGES, key)) {
616
+ ctx.addIssue({
617
+ code: import_zod5.z.ZodIssueCode.custom,
618
+ params: { retired: true },
619
+ path: ["services", "mcp", key],
620
+ message: RETIRED_MCP_KEY_MESSAGES[key]
621
+ });
622
+ }
623
+ }
624
+ }
625
+ if (isPlainObject(value.services) && isPlainObject(value.services.accessibility) && value.services.accessibility.runAfter === "navigation") {
626
+ ctx.addIssue({
627
+ code: import_zod5.z.ZodIssueCode.custom,
628
+ params: { retired: true },
629
+ path: ["services", "accessibility", "runAfter"],
630
+ message: RETIRED_RUN_AFTER_NAVIGATION_MESSAGE
631
+ });
632
+ }
633
+ if (isPlainObject(value.knowledge)) {
634
+ for (const key of Object.keys(value.knowledge)) {
635
+ if (Object.hasOwn(RETIRED_KNOWLEDGE_KEY_MESSAGES, key)) {
636
+ ctx.addIssue({
637
+ code: import_zod5.z.ZodIssueCode.custom,
638
+ params: { retired: true },
639
+ path: ["knowledge", key],
640
+ message: RETIRED_KNOWLEDGE_KEY_MESSAGES[key]
641
+ });
642
+ }
643
+ }
644
+ }
645
+ if (hasRetiredPlannerKey(value.use)) {
646
+ ctx.addIssue({
647
+ code: import_zod5.z.ZodIssueCode.custom,
648
+ params: { retired: true, hardCut: true },
565
649
  path: ["use", "planner", RETIRED_PLANNER_KEY],
566
650
  message: RETIRED_MAX_SUB_ACTIONS_MESSAGE
567
651
  });
568
652
  }
569
653
  });
654
+ function refuseRetiredLocalBindings(raw, filePath) {
655
+ if (!isPlainObject(raw)) return;
656
+ const issues = [];
657
+ if (Object.hasOwn(raw, "providers")) {
658
+ issues.push({ path: "providers", message: RETIRED_LOCAL_PROVIDERS_MESSAGE });
659
+ }
660
+ if (isPlainObject(raw.apps)) {
661
+ for (const [name, app] of Object.entries(raw.apps)) {
662
+ if (isPlainObject(app) && Object.hasOwn(app, "browserstack")) {
663
+ issues.push({ path: `apps.${name}.browserstack`, message: RETIRED_LOCAL_APP_BROWSERSTACK_MESSAGE });
664
+ }
665
+ }
666
+ }
667
+ if (issues.length === 0) return;
668
+ throw new Error(
669
+ [`${filePath}:`, ...issues.map((issue) => ` ${issue.path}: ${issue.message}`)].join("\n")
670
+ );
671
+ }
570
672
  var retiredUseKeyScan = import_zod5.z.any().superRefine((value, ctx) => {
571
673
  if (hasRetiredPlannerKey(value)) {
572
674
  ctx.addIssue({
573
675
  code: import_zod5.z.ZodIssueCode.custom,
574
- params: { retired: true },
676
+ params: { retired: true, hardCut: true },
575
677
  path: ["planner", RETIRED_PLANNER_KEY],
576
678
  message: RETIRED_MAX_SUB_ACTIONS_MESSAGE
577
679
  });
578
680
  }
579
681
  });
682
+ var retiredStepKeyScan = import_zod5.z.any().superRefine((value, ctx) => {
683
+ if (!isPlainObject(value)) return;
684
+ for (const [key, message] of Object.entries(RETIRED_STEP_KEY_MESSAGES)) {
685
+ if (!Object.hasOwn(value, key)) continue;
686
+ ctx.addIssue({
687
+ code: import_zod5.z.ZodIssueCode.custom,
688
+ params: { retired: true },
689
+ path: [key],
690
+ message
691
+ });
692
+ }
693
+ });
580
694
 
581
695
  // src/schema/registry-schema.ts
582
696
  var import_zod7 = require("zod");
@@ -606,8 +720,7 @@ function isAbsoluteAppPath(value) {
606
720
  return value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
607
721
  }
608
722
  var AppInstallSchema = import_zod7.z.object({
609
- path: import_zod7.z.string().refine((v) => !isAbsoluteAppPath(v), { message: "app.path must be relative" }).optional(),
610
- browserstack: import_zod7.z.string().optional()
723
+ path: import_zod7.z.string().refine((v) => !isAbsoluteAppPath(v), { message: "app.path must be relative" }).optional()
611
724
  }).strict();
612
725
  var TargetSchema = import_zod7.z.object({
613
726
  product: import_zod7.z.string().refine(
@@ -620,15 +733,8 @@ var TargetSchema = import_zod7.z.object({
620
733
  appActivity: import_zod7.z.string().optional(),
621
734
  app: AppInstallSchema.optional(),
622
735
  url: import_zod7.z.string().optional(),
623
- // D-06 (phase 401): which environment a run against this target actually touches. Deliberately
624
- // `.optional()` with NO default — an undeclared target must stay visibly undeclared; since 432
625
- // (D-18) undeclared means non-production, decided where the production refusal reads it (the
626
- // run gate that decided "shared" is deleted). A default here would make silence and an honest
627
- // declaration indistinguishable at every later reader, and
628
- // `.strict()` above is what turns a typoed key into a refusal rather than a silent undeclared.
629
- // 432 D-18: `production` is the third declared class; only top-level `allowProduction: true`
630
- // opts a run into it.
631
- environment: import_zod7.z.enum(["local", "shared", "production"]).optional()
736
+ // A label only: agent-qa runs every target the same way, whatever it declares.
737
+ environment: import_zod7.z.enum(["local", "shared", "production"]).optional().describe("A label for this target's environment. agent-qa runs every target the same way.")
632
738
  }).strict().superRefine((val, ctx) => {
633
739
  if (val.platform === "web" && !val.url) {
634
740
  ctx.addIssue({ code: import_zod7.z.ZodIssueCode.custom, message: "Web targets must have a url" });
@@ -636,30 +742,25 @@ var TargetSchema = import_zod7.z.object({
636
742
  });
637
743
  var DeviceProfileSchema = import_zod7.z.object({
638
744
  platform: import_zod7.z.enum(["android", "ios"]),
639
- transport: TransportSchema,
640
745
  match: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.unknown()).optional().default({})
641
746
  }).strict().superRefine((val, ctx) => {
642
- if (val.transport === "local") {
643
- const androidOnly = ["avd", "serial", "appPackage", "appActivity", "automationName", "browserName", "platformVersion"];
644
- const iosOnly = ["udid", "bundleId", "automationName", "platformVersion"];
645
- const allowed = val.platform === "android" ? androidOnly : iosOnly;
646
- for (const key of Object.keys(val.match)) {
647
- if (!allowed.includes(key)) {
648
- ctx.addIssue({
649
- code: import_zod7.z.ZodIssueCode.custom,
650
- path: ["match", key],
651
- message: `Match field "${key}" is not valid for ${val.platform} local transport. Allowed: ${allowed.join(", ")}`
652
- });
653
- }
747
+ const androidOnly = ["avd", "serial", "appPackage", "appActivity", "automationName", "browserName", "platformVersion"];
748
+ const iosOnly = ["udid", "bundleId", "automationName", "platformVersion"];
749
+ const allowed = val.platform === "android" ? androidOnly : iosOnly;
750
+ for (const key of Object.keys(val.match)) {
751
+ if (!allowed.includes(key)) {
752
+ ctx.addIssue({
753
+ code: import_zod7.z.ZodIssueCode.custom,
754
+ path: ["match", key],
755
+ message: `Match field "${key}" is not valid for an ${val.platform} device. Allowed: ${allowed.join(", ")}`
756
+ });
654
757
  }
655
758
  }
656
759
  });
657
- var ProviderConfigSchema = import_zod7.z.record(import_zod7.z.string(), import_zod7.z.unknown());
658
760
  var RegistrySchema = import_zod7.z.object({
659
761
  llms: import_zod7.z.array(NamedLLMConfigSchema).optional().default([]),
660
762
  targets: import_zod7.z.record(TargetNameSchema, TargetSchema).optional(),
661
- devices: import_zod7.z.record(import_zod7.z.string(), DeviceProfileSchema).optional(),
662
- providers: import_zod7.z.record(import_zod7.z.string(), ProviderConfigSchema).optional()
763
+ devices: import_zod7.z.record(import_zod7.z.string(), DeviceProfileSchema).optional()
663
764
  }).strict();
664
765
 
665
766
  // src/schema/use-schema.ts
@@ -703,28 +804,11 @@ var UseSchema = import_zod8.z.object({
703
804
  logCapture: LogCaptureConfigSchema.optional(),
704
805
  cache: import_zod8.z.boolean().optional(),
705
806
  llm: import_zod8.z.string().optional(),
706
- /**
707
- * Names the app-under-test build for the DCAY-01 buildTag-to-ordinal mapping.
708
- * Precedence: CLI --build-tag > AGENT_QA_BUILD_TAG > this config key.
709
- */
807
+ // Precedence: CLI `--build-tag`, then `AGENT_QA_BUILD_TAG`, then this key.
710
808
  buildTag: import_zod8.z.string().trim().min(1).optional(),
711
- /**
712
- * 402-03 (D-12): run the members of one run CONCURRENTLY. Absent or false is one member at a
713
- * time — today's behaviour, byte for byte.
714
- */
809
+ // Runs one run's members concurrently; absent or false runs one member at a time.
715
810
  parallel: import_zod8.z.boolean().optional(),
716
- /**
717
- * 402-03 (D-12): the ceiling on concurrent members when `parallel` is true. Absent resolves to
718
- * `DEFAULT_PARALLEL_WORKERS` (3). Effective concurrency is `min(memberCount, workers)`, decided
719
- * in exactly one place — `resolveEffectiveWorkers` in `@vostride/agent-qa-schema`.
720
- *
721
- * A POSITIVE INTEGER, refused otherwise: an unbounded or nonsensical count here is the only
722
- * reachable input to a concurrency denial-of-service (T-402-12), and it is a config typo rather
723
- * than a caller's field — `RunsStartInput` names neither of these two.
724
- *
725
- * NOT `.default(...)`: a defaulted key changes the PARSED config type and every hand-built
726
- * config fixture would then need it. The default is the resolver's, not the schema's.
727
- */
811
+ // Not defaulted, so the parsed type is unchanged; `resolveEffectiveWorkers` owns the default.
728
812
  workers: import_zod8.z.number().int().positive().optional()
729
813
  }).strict();
730
814
  var UseOverrideSchema = import_zod8.z.object({
@@ -739,7 +823,9 @@ var UseOverrideSchema = import_zod8.z.object({
739
823
  timeout: import_zod8.z.object({
740
824
  step: DurationString.optional(),
741
825
  test: DurationString.optional(),
742
- navigation: DurationString.optional()
826
+ navigation: DurationString.optional().describe(
827
+ "How long a web page load or URL wait can take. Default 2m. Android and iOS runs do not use it."
828
+ )
743
829
  }).strict().optional(),
744
830
  planner: import_zod8.z.object({
745
831
  maxActions: import_zod8.z.number().int().positive().optional(),
@@ -755,7 +841,7 @@ var UseOverrideSchema = import_zod8.z.object({
755
841
  mobile: MobileUseOverrideSchema.optional(),
756
842
  llm: import_zod8.z.string().optional(),
757
843
  parallel: import_zod8.z.boolean().optional(),
758
- /** 402-03 (D-12): the override half of `use.workers`. Same shape as `UseSchema`'s. */
844
+ // The override half of `use.workers`, shaped like `UseSchema`'s.
759
845
  workers: import_zod8.z.number().int().positive().optional(),
760
846
  device: import_zod8.z.string().optional()
761
847
  }).strict();
@@ -799,82 +885,15 @@ var PluginsSchema = import_zod10.z.object({
799
885
  var AgentQaConfigSchemaInner = import_zod10.z.object({
800
886
  workspace: WorkspaceSchema,
801
887
  services: ServicesSchema.prefault({}).optional(),
802
- // D-08: slim replacement for the retired v1 memory config surface; prefaulted so parsed
803
- // configs always materialize knowledge defaults (zod 4 .default() short-circuits without
804
- // parsing — .prefault() runs the inner defaults) — matches the providers `.default([])`
805
- // precedent in spirit.
888
+ // `.prefault` runs the inner defaults; zod 4's `.default()` short-circuits without parsing.
806
889
  knowledge: KnowledgeConfigSchema.prefault({}),
807
890
  registry: RegistrySchema.optional(),
808
891
  use: UseSchema.optional(),
809
892
  plugins: PluginsSchema.optional(),
810
- analytics: AnalyticsSchema.optional(),
811
- // 432 D-18: the one production opt-in. Optional with no default: its consumer reads absence as
812
- // false, so only an explicit `true` in the validated config authorizes a production target.
813
- allowProduction: import_zod10.z.boolean().optional(),
814
- // 2026-10-01 (432-22, D-15/D-18/D-22): the `safety` section (`safety.trustMode`) is deleted with
815
- // trust mode. `allowProduction` above is the only production opt-in, and a file that still
816
- // declares `safety` gets the normal strict unrecognized-key error.
817
- // D-03 (Phase 372): eval substrate params. `judge` references a NAMED registry.llms entry;
818
- // strict so unknown eval subkeys are rejected.
819
- eval: import_zod10.z.object({
820
- judge: import_zod10.z.string().min(1).refine((name) => name.trim().length > 0).optional(),
821
- judges: import_zod10.z.array(import_zod10.z.string().min(1).refine((name) => name.trim().length > 0)).max(3).optional(),
822
- // Owner override of D-10 (2026-07-12): explicit default-off opt-in that lets a
823
- // KNOWN-same-family judge run; the eligibility law discloses the override on every
824
- // scored entry (familyOverride) and the report projection renders one honesty line.
825
- // Unknown families stay forbidden regardless.
826
- judgeAllowSameFamily: import_zod10.z.boolean().optional(),
827
- k: import_zod10.z.number().int().positive().optional(),
828
- seed: import_zod10.z.number().int().optional(),
829
- // 2026-09-07, 417 D-19: Gate-1 screen cap; recorded default lives in the CLI.
830
- // 2026-09-11, 422 D-14: `maxPerJob` bounds how many Gate-1 CALLS one post-run proposal job
831
- // may make; recorded default lives beside the handler (PROPOSE_DEFAULT_MAX_GATE1_PER_JOB).
832
- // A SECOND axis beside `maxScreens`, not a refinement of it — that one bounds how much a
833
- // single screening looks at, this one bounds how many screenings a run's tail buys — and an
834
- // operator can legitimately want a generous first and a tight second. Positive-integer only,
835
- // for the sharper version of the usual reason: a bound that silently fell back to the
836
- // recorded default because the operator typed `0` would buy replay work they refused. There
837
- // is deliberately NO CLI flag: the bound is charged durably per job and read back by a
838
- // resumed attempt, so a per-invocation flag would be a bound that could change between the
839
- // attempt that spent it and the attempt that has to honour what was spent.
840
- gate1: import_zod10.z.object({
841
- maxScreens: import_zod10.z.number().int().positive().optional(),
842
- maxPerJob: import_zod10.z.number().int().positive().optional()
843
- }).strict().optional(),
844
- // 2026-09-07, 418 D-04/D-13/D-14: Gate-2 ablation cap, canary horizon (build ordinals)
845
- // and per-scope lower-trust cap; recorded defaults live in the CLI (GATE2_DEFAULT_*).
846
- gate2: import_zod10.z.object({
847
- maxAblations: import_zod10.z.number().int().positive().optional(),
848
- canaryHorizonBuilds: import_zod10.z.number().int().positive().optional(),
849
- maxLowerTrustPerScope: import_zod10.z.number().int().positive().optional()
850
- }).strict().optional(),
851
- // 2026-09-11, 422 D-04: how many NOVEL seeds one adversarial refresh may derive from
852
- // before the rest are counted and deferred; recorded default lives in eval core
853
- // (ADVERSARIAL_DEFAULT_MAX_SEEDS_PER_REFRESH). Strict, and positive-integer only: a cap
854
- // that silently fell back to the default because the operator typed `0` is not a cap.
855
- // 2026-09-11, 422 D-04/D-10: `maxProbeCalls` bounds PROVIDER CALLS one refresh may make to
856
- // author probes; recorded default lives in eval-local
857
- // (ADVERSARIAL_DEFAULT_MAX_PROBE_CALLS). It is a SECOND budget beside the seed cap on
858
- // purpose — deterministic seeds cost CPU and authored probes cost money no later run can
859
- // recover — and it is held to the same positive-integer strictness for the sharper version
860
- // of the same reason: a cap that silently fell back to the default because the operator
861
- // typed `0` would buy calls they refused.
862
- adversarial: import_zod10.z.object({
863
- maxSeedsPerRefresh: import_zod10.z.number().int().positive().optional(),
864
- maxProbeCalls: import_zod10.z.number().int().positive().optional()
865
- }).strict().optional(),
866
- seeding: import_zod10.z.object({
867
- silverPerTestCap: import_zod10.z.number().int().positive().optional()
868
- }).strict().optional()
869
- }).strict().optional()
893
+ analytics: AnalyticsSchema.optional()
894
+ // There is no `safety` section: a file that still declares one gets the normal strict
895
+ // unrecognized-key error.
870
896
  }).strict().superRefine((val, ctx) => {
871
- if (val.eval?.judge !== void 0 && val.eval.judges !== void 0) {
872
- ctx.addIssue({
873
- code: import_zod10.z.ZodIssueCode.custom,
874
- path: ["eval"],
875
- message: "Set either eval.judge or eval.judges, never both; remove one declaration."
876
- });
877
- }
878
897
  const llms = val.registry?.llms ?? [];
879
898
  const names = llms.map((c) => c.name);
880
899
  const dupes = names.filter((n, i) => names.indexOf(n) !== i);
@@ -899,13 +918,6 @@ var AgentQaConfigSchemaInner = import_zod10.z.object({
899
918
  message: `use.llm "${val.use.llm}" does not match any name in registry.llms`
900
919
  });
901
920
  }
902
- if (!MobileAppStateSchema.safeParse(val.use?.mobile?.appState).success) {
903
- ctx.addIssue({
904
- code: import_zod10.z.ZodIssueCode.custom,
905
- path: ["use", "mobile", "appState"],
906
- message: "use.mobile.appState is required and must be one of: preserve | reset"
907
- });
908
- }
909
921
  });
910
922
  var AgentQaConfigSchema = retiredKeyScan.pipe(AgentQaConfigSchemaInner);
911
923
 
@@ -928,8 +940,6 @@ var TestStepSchema = import_zod11.z.union([
928
940
  import_zod11.z.object({
929
941
  step: import_zod11.z.string(),
930
942
  timeout: OptionalDuration,
931
- retries: import_zod11.z.number().optional(),
932
- screenshot: import_zod11.z.boolean().optional(),
933
943
  capture: CaptureConfigSchema.optional()
934
944
  }).strict()
935
945
  ]);
@@ -952,19 +962,9 @@ var TestDefinitionSchema = import_zod11.z.object({
952
962
  meta: TestMetaSchema.optional(),
953
963
  setup: import_zod11.z.array(HookIdSchema).optional(),
954
964
  teardown: import_zod11.z.array(HookIdSchema).optional(),
955
- steps: import_zod11.z.array(TestStepSchema).min(1)
965
+ steps: import_zod11.z.array(retiredStepKeyScan.pipe(TestStepSchema)).min(1)
956
966
  }).strict();
957
967
 
958
- // src/schema/action-schema.ts
959
- var import_zod12 = require("zod");
960
- var ActionPlanSchema = import_zod12.z.object({
961
- reasoning: import_zod12.z.string().describe("Brief explanation of why this action was chosen"),
962
- action: import_zod12.z.object({ type: import_zod12.z.string() }).passthrough(),
963
- confidence: import_zod12.z.number().describe("Confidence level from 0 to 1"),
964
- stepComplete: import_zod12.z.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."),
965
- stepFailed: import_zod12.z.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.")
966
- });
967
-
968
968
  // src/schema/resolve-knowledge-root.ts
969
969
  var import_node_path = __toESM(require("path"), 1);
970
970
  function resolveKnowledgeRoot(knowledge, configDir) {
@@ -972,66 +972,66 @@ function resolveKnowledgeRoot(knowledge, configDir) {
972
972
  }
973
973
 
974
974
  // src/schema/suite-schema.ts
975
- var import_zod13 = require("zod");
975
+ var import_zod12 = require("zod");
976
976
  var import_agent_qa_ids2 = require("@vostride/agent-qa-ids");
977
- var SuiteTestEntrySchema = import_zod13.z.object({
978
- test: import_zod13.z.string(),
979
- id: import_zod13.z.string().refine((value) => (0, import_agent_qa_ids2.isCanonicalTestId)(value), {
977
+ var SuiteTestEntrySchema = import_zod12.z.object({
978
+ test: import_zod12.z.string(),
979
+ id: import_zod12.z.string().refine((value) => (0, import_agent_qa_ids2.isCanonicalTestId)(value), {
980
980
  message: "Test ID must be t_ followed by 10 id-agent words"
981
981
  })
982
982
  }).strict();
983
- var HookIdSchema2 = import_zod13.z.string().refine((value) => (0, import_agent_qa_ids2.isCanonicalHookId)(value), {
983
+ var HookIdSchema2 = import_zod12.z.string().refine((value) => (0, import_agent_qa_ids2.isCanonicalHookId)(value), {
984
984
  message: "Hook ID must be h_ followed by 10 id-agent words"
985
985
  });
986
- var SuiteDefinitionSchema = import_zod13.z.object({
987
- "suite-id": import_zod13.z.string().refine((value) => (0, import_agent_qa_ids2.isCanonicalSuiteId)(value), {
986
+ var SuiteDefinitionSchema = import_zod12.z.object({
987
+ "suite-id": import_zod12.z.string().refine((value) => (0, import_agent_qa_ids2.isCanonicalSuiteId)(value), {
988
988
  message: "Suite ID must be s_ followed by 10 id-agent words"
989
989
  }).optional(),
990
- name: import_zod13.z.string(),
991
- target: import_zod13.z.string(),
992
- context: import_zod13.z.string().optional(),
993
- setup: import_zod13.z.array(HookIdSchema2).optional(),
994
- teardown: import_zod13.z.array(HookIdSchema2).optional(),
995
- tests: import_zod13.z.array(SuiteTestEntrySchema).min(1),
990
+ name: import_zod12.z.string(),
991
+ target: import_zod12.z.string(),
992
+ context: import_zod12.z.string().optional(),
993
+ setup: import_zod12.z.array(HookIdSchema2).optional(),
994
+ teardown: import_zod12.z.array(HookIdSchema2).optional(),
995
+ tests: import_zod12.z.array(SuiteTestEntrySchema).min(1),
996
996
  use: retiredUseKeyScan.pipe(UseOverrideSchema).optional()
997
997
  }).strict();
998
998
 
999
999
  // src/hooks/schema.ts
1000
- var import_zod14 = require("zod");
1000
+ var import_zod13 = require("zod");
1001
1001
  var import_ms3 = __toESM(require("ms"), 1);
1002
1002
  var import_agent_qa_ids3 = require("@vostride/agent-qa-ids");
1003
- var HookDurationString = import_zod14.z.string().refine((v) => (0, import_ms3.default)(v) !== void 0, {
1003
+ var HookDurationString = import_zod13.z.string().refine((v) => (0, import_ms3.default)(v) !== void 0, {
1004
1004
  message: 'Invalid duration format (e.g., "30s", "2m", "1h")'
1005
1005
  }).transform((v) => (0, import_ms3.default)(v));
1006
1006
  var HOOK_ID_MESSAGE = "Hook ID must be h_ followed by 10 id-agent words";
1007
- var HookDefinitionSchema = import_zod14.z.object({
1008
- id: import_zod14.z.string().refine((value) => (0, import_agent_qa_ids3.isCanonicalHookId)(value), {
1007
+ var HookDefinitionSchema = import_zod13.z.object({
1008
+ id: import_zod13.z.string().refine((value) => (0, import_agent_qa_ids3.isCanonicalHookId)(value), {
1009
1009
  message: HOOK_ID_MESSAGE
1010
1010
  }),
1011
- name: import_zod14.z.string().min(1),
1012
- runtime: import_zod14.z.enum(["node", "bun", "python", "bash"]),
1013
- file: import_zod14.z.string().min(1),
1014
- deps: import_zod14.z.array(import_zod14.z.string()).optional().default([]),
1015
- packageFile: import_zod14.z.string().optional(),
1011
+ name: import_zod13.z.string().min(1),
1012
+ runtime: import_zod13.z.enum(["node", "bun", "python", "bash"]),
1013
+ file: import_zod13.z.string().min(1),
1014
+ deps: import_zod13.z.array(import_zod13.z.string()).optional().default([]),
1015
+ packageFile: import_zod13.z.string().optional(),
1016
1016
  timeout: HookDurationString,
1017
- network: import_zod14.z.boolean().optional().default(true)
1017
+ network: import_zod13.z.boolean().optional().default(true)
1018
1018
  });
1019
- var HooksFileSchema = import_zod14.z.object({
1020
- hooks: import_zod14.z.array(HookDefinitionSchema)
1019
+ var HooksFileSchema = import_zod13.z.object({
1020
+ hooks: import_zod13.z.array(HookDefinitionSchema)
1021
1021
  }).superRefine((val, ctx) => {
1022
1022
  const ids = /* @__PURE__ */ new Set();
1023
1023
  const names = /* @__PURE__ */ new Set();
1024
1024
  for (let i = 0; i < val.hooks.length; i++) {
1025
1025
  if (ids.has(val.hooks[i].id)) {
1026
1026
  ctx.addIssue({
1027
- code: import_zod14.z.ZodIssueCode.custom,
1027
+ code: import_zod13.z.ZodIssueCode.custom,
1028
1028
  path: ["hooks", i, "id"],
1029
1029
  message: `Duplicate hook id: "${val.hooks[i].id}"`
1030
1030
  });
1031
1031
  }
1032
1032
  if (names.has(val.hooks[i].name)) {
1033
1033
  ctx.addIssue({
1034
- code: import_zod14.z.ZodIssueCode.custom,
1034
+ code: import_zod13.z.ZodIssueCode.custom,
1035
1035
  path: ["hooks", i, "name"],
1036
1036
  message: `Duplicate hook name: "${val.hooks[i].name}"`
1037
1037
  });
@@ -1386,7 +1386,7 @@ var PatchOpUnsupportedTargetError = class extends Error {
1386
1386
  var PatchedDocumentInvalidError = class extends Error {
1387
1387
  constructor(phase, detail) {
1388
1388
  super(
1389
- `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)`
1389
+ `cst-patch: the ${phase} document is not a valid test: ${detail}; a patched document is schema-validated before any caller may write it`
1390
1390
  );
1391
1391
  this.phase = phase;
1392
1392
  this.detail = detail;
@@ -1482,7 +1482,7 @@ function resolveSeqSlot(doc, pointer, segments, op) {
1482
1482
  if (segments.length < 2) {
1483
1483
  throw new PatchOpUnsupportedTargetError(
1484
1484
  pointer,
1485
- `${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`
1485
+ `${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`
1486
1486
  );
1487
1487
  }
1488
1488
  const parent = resolveNode(doc, segments.slice(0, -1));
@@ -2151,9 +2151,6 @@ var MobileSetupError = class extends Error {
2151
2151
  function isMobilePlatform(value) {
2152
2152
  return value === "android" || value === "ios";
2153
2153
  }
2154
- function isMobileTransport(value) {
2155
- return value === "local" || value === "browserstack";
2156
- }
2157
2154
  function asOptionalString(value) {
2158
2155
  return typeof value === "string" && value.length > 0 ? value : void 0;
2159
2156
  }
@@ -2225,11 +2222,6 @@ function resolveAppInstall(target, input, platform, deviceName, sourceTrace) {
2225
2222
  const pathValue = localPath ?? targetPath;
2226
2223
  const pathSource = localPath ? `agent-qa.local.yaml apps.${input.targetName}.path` : targetPath ? `agent-qa.config.yaml registry.targets.${input.targetName}.app.path` : void 0;
2227
2224
  const pathBaseDir = localPath ? baseDirFor(input.localConfigFilePath ?? input.localBindings?.filePath) : targetPath ? baseDirFor(input.configFilePath) : void 0;
2228
- const localBrowserStack = asOptionalString(localApp?.browserstack);
2229
- const targetBrowserStack = asOptionalString(target.app?.browserstack);
2230
- const browserstackValue = localBrowserStack ?? targetBrowserStack;
2231
- const browserstackSource = localBrowserStack ? `agent-qa.local.yaml apps.${input.targetName}.browserstack` : targetBrowserStack ? `agent-qa.config.yaml registry.targets.${input.targetName}.app.browserstack` : void 0;
2232
- const browserstackBaseDir = localBrowserStack ? baseDirFor(input.localConfigFilePath ?? input.localBindings?.filePath) : targetBrowserStack ? baseDirFor(input.configFilePath) : void 0;
2233
2225
  const install = { sourceTrace: installSourceTrace };
2234
2226
  if (pathValue) {
2235
2227
  if (isAbsoluteAppInstallValue(pathValue)) {
@@ -2255,23 +2247,7 @@ function resolveAppInstall(target, input, platform, deviceName, sourceTrace) {
2255
2247
  sourceTrace.push(`app.path=${pathSource}`);
2256
2248
  }
2257
2249
  }
2258
- if (browserstackValue) {
2259
- if (isAbsoluteAppInstallValue(browserstackValue)) {
2260
- throw appInstallError(`Configured BrowserStack app value must be relative or a BrowserStack reference: ${browserstackValue}`, input, {
2261
- platform,
2262
- deviceName,
2263
- appId: browserstackValue,
2264
- sourceTrace: [...sourceTrace, `app.browserstack=${browserstackSource}`]
2265
- });
2266
- }
2267
- install.browserstack = browserstackValue;
2268
- install.browserstackBaseDir = browserstackBaseDir;
2269
- if (browserstackSource) {
2270
- installSourceTrace["app.browserstack"] = browserstackSource;
2271
- sourceTrace.push(`app.browserstack=${browserstackSource}`);
2272
- }
2273
- }
2274
- return install.path || install.browserstack ? install : void 0;
2250
+ return install.path ? install : void 0;
2275
2251
  }
2276
2252
  function resolveMobileRunConfig(input) {
2277
2253
  const targets = input.config.registry?.targets;
@@ -2325,7 +2301,6 @@ function resolveMobileRunConfig(input) {
2325
2301
  { platform, deviceName }
2326
2302
  );
2327
2303
  }
2328
- const transport = isMobileTransport(deviceProfile.transport) ? deviceProfile.transport : "local";
2329
2304
  const localMatch = input.localBindings?.devices?.[deviceName] ?? null;
2330
2305
  const mergedMatch = { ...deviceProfile.match ?? {}, ...localMatch ?? {} };
2331
2306
  const appSourceTrace = {};
@@ -2367,11 +2342,9 @@ function resolveMobileRunConfig(input) {
2367
2342
  platform,
2368
2343
  targetName: input.targetName,
2369
2344
  deviceName,
2370
- transport,
2371
2345
  device: {
2372
2346
  name: deviceName,
2373
2347
  platform,
2374
- transport,
2375
2348
  match: mergedMatch
2376
2349
  },
2377
2350
  app,
@@ -3321,7 +3294,7 @@ function clearLoadedProviderPluginModules() {
3321
3294
  }
3322
3295
 
3323
3296
  // src/logging/types.ts
3324
- var import_zod15 = require("zod");
3297
+ var import_zod14 = require("zod");
3325
3298
  var LOG_LEVEL_PRIORITY = {
3326
3299
  silent: 0,
3327
3300
  error: 1,
@@ -3329,46 +3302,45 @@ var LOG_LEVEL_PRIORITY = {
3329
3302
  info: 3,
3330
3303
  debug: 4
3331
3304
  };
3332
- var AgentLogDataSchema = import_zod15.z.object({
3333
- extractDom: import_zod15.z.boolean().optional(),
3334
- hasScreenshot: import_zod15.z.boolean().optional(),
3335
- duration: import_zod15.z.number().optional(),
3336
- elementCount: import_zod15.z.number().optional()
3305
+ var AgentLogDataSchema = import_zod14.z.object({
3306
+ hasScreenshot: import_zod14.z.boolean().optional(),
3307
+ duration: import_zod14.z.number().optional(),
3308
+ elementCount: import_zod14.z.number().optional()
3337
3309
  });
3338
- var AdapterLogDataSchema = import_zod15.z.object({
3339
- actionType: import_zod15.z.string().optional(),
3340
- selector: import_zod15.z.string().optional(),
3341
- coordinates: import_zod15.z.object({ x: import_zod15.z.number(), y: import_zod15.z.number() }).optional(),
3342
- elementProperties: import_zod15.z.record(import_zod15.z.string(), import_zod15.z.unknown()).optional(),
3343
- duration: import_zod15.z.number().optional()
3310
+ var AdapterLogDataSchema = import_zod14.z.object({
3311
+ actionType: import_zod14.z.string().optional(),
3312
+ selector: import_zod14.z.string().optional(),
3313
+ coordinates: import_zod14.z.object({ x: import_zod14.z.number(), y: import_zod14.z.number() }).optional(),
3314
+ elementProperties: import_zod14.z.record(import_zod14.z.string(), import_zod14.z.unknown()).optional(),
3315
+ duration: import_zod14.z.number().optional()
3344
3316
  });
3345
- var CacheLogDataSchema = import_zod15.z.object({
3346
- operation: import_zod15.z.enum(["get", "set", "invalidate", "bypass", "batch-purge"]),
3347
- stepHash: import_zod15.z.string().optional(),
3348
- screenHash: import_zod15.z.string().optional(),
3349
- hit: import_zod15.z.boolean().optional(),
3350
- age: import_zod15.z.number().optional(),
3351
- entriesDeleted: import_zod15.z.number().optional()
3317
+ var CacheLogDataSchema = import_zod14.z.object({
3318
+ operation: import_zod14.z.enum(["get", "set", "invalidate", "bypass", "batch-purge"]),
3319
+ stepHash: import_zod14.z.string().optional(),
3320
+ screenHash: import_zod14.z.string().optional(),
3321
+ hit: import_zod14.z.boolean().optional(),
3322
+ age: import_zod14.z.number().optional(),
3323
+ entriesDeleted: import_zod14.z.number().optional()
3352
3324
  });
3353
- var PlannerLogDataSchema = import_zod15.z.object({
3354
- model: import_zod15.z.string().optional(),
3355
- promptTokens: import_zod15.z.number().optional(),
3356
- completionTokens: import_zod15.z.number().optional(),
3357
- latencyMs: import_zod15.z.number().optional(),
3358
- confidence: import_zod15.z.number().optional(),
3359
- actionType: import_zod15.z.string().optional()
3325
+ var PlannerLogDataSchema = import_zod14.z.object({
3326
+ model: import_zod14.z.string().optional(),
3327
+ promptTokens: import_zod14.z.number().optional(),
3328
+ completionTokens: import_zod14.z.number().optional(),
3329
+ latencyMs: import_zod14.z.number().optional(),
3330
+ confidence: import_zod14.z.number().optional(),
3331
+ actionType: import_zod14.z.string().optional()
3360
3332
  });
3361
- var HookLogDataSchema = import_zod15.z.object({
3362
- hookName: import_zod15.z.string().optional(),
3363
- phase: import_zod15.z.enum(["setup", "teardown"]).optional(),
3364
- duration: import_zod15.z.number().optional(),
3365
- success: import_zod15.z.boolean().optional()
3333
+ var HookLogDataSchema = import_zod14.z.object({
3334
+ hookName: import_zod14.z.string().optional(),
3335
+ phase: import_zod14.z.enum(["setup", "teardown"]).optional(),
3336
+ duration: import_zod14.z.number().optional(),
3337
+ success: import_zod14.z.boolean().optional()
3366
3338
  });
3367
- var RunnerLogDataSchema = import_zod15.z.object({
3368
- stepIndex: import_zod15.z.number().optional(),
3369
- totalSteps: import_zod15.z.number().optional(),
3370
- testName: import_zod15.z.string().optional(),
3371
- variablesCaptured: import_zod15.z.number().optional()
3339
+ var RunnerLogDataSchema = import_zod14.z.object({
3340
+ stepIndex: import_zod14.z.number().optional(),
3341
+ totalSteps: import_zod14.z.number().optional(),
3342
+ testName: import_zod14.z.string().optional(),
3343
+ variablesCaptured: import_zod14.z.number().optional()
3372
3344
  });
3373
3345
  var LogSourceDataSchemas = {
3374
3346
  agent: AgentLogDataSchema,
@@ -3380,7 +3352,7 @@ var LogSourceDataSchemas = {
3380
3352
  };
3381
3353
 
3382
3354
  // src/logging/log-manager.ts
3383
- var import_node_crypto4 = require("crypto");
3355
+ var import_node_crypto5 = require("crypto");
3384
3356
 
3385
3357
  // src/logging/cli-formatter.ts
3386
3358
  var import_picocolors2 = __toESM(require("picocolors"), 1);
@@ -3399,12 +3371,60 @@ function formatLogEntry(entry) {
3399
3371
  return `${time} ${level} ${source} ${entry.message}${dataSuffix}`;
3400
3372
  }
3401
3373
 
3374
+ // src/logging/run-text-output.ts
3375
+ var import_node_crypto4 = require("crypto");
3376
+ var STDOUT_RESERVATIONS = /* @__PURE__ */ Symbol.for("agent-qa.run-text-output.stdout-reservations");
3377
+ function stdoutReservations() {
3378
+ const holder = globalThis;
3379
+ holder[STDOUT_RESERVATIONS] ??= { count: 0 };
3380
+ return holder[STDOUT_RESERVATIONS];
3381
+ }
3382
+ function isGitHubActions(env = process.env) {
3383
+ const value = env.GITHUB_ACTIONS;
3384
+ return value !== void 0 && value !== "" && value.toLowerCase() !== "false" && value !== "0";
3385
+ }
3386
+ function guardRunText(text, env = process.env) {
3387
+ if (!isGitHubActions(env)) return text;
3388
+ const token = (0, import_node_crypto4.randomBytes)(16).toString("hex");
3389
+ const block = text.endsWith("\n") ? text : `${text}
3390
+ `;
3391
+ return `::stop-commands::${token}
3392
+ ${block}::${token}::
3393
+ `;
3394
+ }
3395
+ function guardJsonText(serialized, env = process.env) {
3396
+ return isGitHubActions(env) ? serialized.replaceAll("#", "\\u0023") : serialized;
3397
+ }
3398
+ function reserveStdoutForData() {
3399
+ const reservations = stdoutReservations();
3400
+ reservations.count += 1;
3401
+ let released = false;
3402
+ return () => {
3403
+ if (released) return;
3404
+ released = true;
3405
+ reservations.count -= 1;
3406
+ };
3407
+ }
3408
+ function printRunText(text, print = (line) => console.log(line), options = {}) {
3409
+ const env = options.env ?? process.env;
3410
+ if (!isGitHubActions(env)) {
3411
+ print(text);
3412
+ return;
3413
+ }
3414
+ const block = guardRunText(text, env);
3415
+ if (options.stdoutIsData ?? stdoutReservations().count > 0) {
3416
+ process.stderr.write(block);
3417
+ return;
3418
+ }
3419
+ process.stdout.write(block);
3420
+ }
3421
+
3402
3422
  // src/logging/log-manager.ts
3403
3423
  var import_agent_qa_runner5 = require("@vostride/agent-qa-runner");
3404
3424
  var MemberLogScopeUnavailableError = class extends Error {
3405
3425
  constructor() {
3406
3426
  super(
3407
- "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)."
3427
+ "LogManager.runInMemberScope requires a memberScope store. Construct the manager with `memberScope: new AsyncLocalStorage()` \u2014 without it, concurrent members would silently share one run id."
3408
3428
  );
3409
3429
  this.name = "MemberLogScopeUnavailableError";
3410
3430
  }
@@ -3419,6 +3439,7 @@ var LogManager = class {
3419
3439
  ndjson;
3420
3440
  redactor;
3421
3441
  memberScope;
3442
+ env;
3422
3443
  constructor(opts) {
3423
3444
  this.runId = opts.runId ?? "";
3424
3445
  this.runIdSet = !!opts.runId;
@@ -3427,21 +3448,12 @@ var LogManager = class {
3427
3448
  this.ndjson = opts.ndjson ?? false;
3428
3449
  this.redactor = opts.redactor;
3429
3450
  this.memberScope = opts.memberScope;
3451
+ this.env = opts.env;
3430
3452
  }
3431
3453
  setRedactor(redactor) {
3432
3454
  this.redactor = redactor;
3433
3455
  }
3434
- /**
3435
- * Run `fn` with its own run-id binding, so concurrent members cannot overwrite each other.
3436
- *
3437
- * ONE `LogManager` SERVES EVERY MEMBER of a run — `commands/run.ts` builds it once, outside the
3438
- * per-member closure — and since 402-02/402-03 the direct path runs members concurrently. Without
3439
- * a scope, `setRunId` is a single mutable field plus a whole-buffer backfill, so the LAST member
3440
- * to mint an id claims every line in the buffer, its siblings' included (measured, not inferred).
3441
- *
3442
- * Outside a scope nothing changes: `setRunId` keeps its original whole-buffer behaviour, which is
3443
- * what the sequential path and the run-level `onRunCreated` stamp rely on.
3444
- */
3456
+ // One manager serves every concurrent member; outside a scope `setRunId` backfills the buffer.
3445
3457
  runInMemberScope(fn) {
3446
3458
  if (!this.memberScope) throw new MemberLogScopeUnavailableError();
3447
3459
  return this.memberScope.run({ runId: this.runId, pending: [] }, fn);
@@ -3449,7 +3461,7 @@ var LogManager = class {
3449
3461
  log(level, source, message, data) {
3450
3462
  const scope = this.memberScope?.getStore();
3451
3463
  const entry = {
3452
- id: (0, import_node_crypto4.randomUUID)(),
3464
+ id: (0, import_node_crypto5.randomUUID)(),
3453
3465
  stepId: this.currentStepId,
3454
3466
  runId: scope ? scope.runId : this.runId,
3455
3467
  level,
@@ -3461,7 +3473,14 @@ var LogManager = class {
3461
3473
  this.buffer.push(entry);
3462
3474
  if (scope && !entry.runId) scope.pending.push(entry);
3463
3475
  if (this.shouldDisplay(level)) {
3464
- process.stderr.write(formatLogEntry(entry) + "\n");
3476
+ printRunText(
3477
+ formatLogEntry(entry),
3478
+ (line) => {
3479
+ process.stderr.write(`${line}
3480
+ `);
3481
+ },
3482
+ { env: this.env }
3483
+ );
3465
3484
  }
3466
3485
  if (this.ndjson) {
3467
3486
  this.emitNdjson(entry);
@@ -3482,7 +3501,7 @@ var LogManager = class {
3482
3501
  */
3483
3502
  emitNdjson(entry) {
3484
3503
  process.stdout.write(
3485
- `AGENT_QA_EVENT:${JSON.stringify({ type: "log", ...entry })}
3504
+ `AGENT_QA_EVENT:${guardJsonText(JSON.stringify({ type: "log", ...entry }), this.env)}
3486
3505
  `
3487
3506
  );
3488
3507
  }
@@ -3530,264 +3549,6 @@ var LogManager = class {
3530
3549
  }
3531
3550
  };
3532
3551
 
3533
- // src/farm/credentials.ts
3534
- var ENV_MAP = {
3535
- browserstack: {
3536
- username: "BROWSERSTACK_USERNAME",
3537
- accessKey: "BROWSERSTACK_ACCESS_KEY"
3538
- }
3539
- };
3540
- var DISPLAY_NAMES = {
3541
- browserstack: "BrowserStack"
3542
- };
3543
- function resolveFarmCredentials(provider, providerConfig) {
3544
- const envKeys = ENV_MAP[provider];
3545
- if (!envKeys) {
3546
- throw new Error(`Unknown farm provider: ${provider}`);
3547
- }
3548
- const envUsername = process.env[envKeys.username];
3549
- const envAccessKey = process.env[envKeys.accessKey];
3550
- if (envUsername && envAccessKey) {
3551
- return { username: envUsername, accessKey: envAccessKey };
3552
- }
3553
- if (providerConfig?.username && providerConfig?.accessKey) {
3554
- return { username: providerConfig.username, accessKey: providerConfig.accessKey };
3555
- }
3556
- const displayName = DISPLAY_NAMES[provider] ?? provider;
3557
- throw new Error(
3558
- `${displayName} credentials not found. Set ${envKeys.username} and ${envKeys.accessKey} env vars, or add provider credentials to agent-qa.local.yaml.`
3559
- );
3560
- }
3561
-
3562
- // src/farm/capability-mapper.ts
3563
- var import_node_module2 = require("module");
3564
- var import_meta2 = {};
3565
- var BROWSER_MAP = {
3566
- chromium: "playwright-chromium",
3567
- firefox: "playwright-firefox",
3568
- webkit: "playwright-webkit"
3569
- };
3570
- function detectPlaywrightVersion() {
3571
- try {
3572
- const require4 = (0, import_node_module2.createRequire)(typeof __filename === "string" ? __filename : import_meta2.url);
3573
- const pkg = require4("playwright-core/package.json");
3574
- return pkg.version;
3575
- } catch {
3576
- return "1.59.1";
3577
- }
3578
- }
3579
- function mapWebCapabilities(browser, credentials, opts) {
3580
- if (!browser.viewport) {
3581
- throw new Error("Farm mode requires browser.viewport to be set (e.g. { width: 1280, height: 720 })");
3582
- }
3583
- const bstackBrowser = BROWSER_MAP[browser.name];
3584
- if (!bstackBrowser) {
3585
- throw new Error(`Unsupported browser for BrowserStack: "${browser.name}". Supported: ${Object.keys(BROWSER_MAP).join(", ")}`);
3586
- }
3587
- return {
3588
- browser: bstackBrowser,
3589
- os: "os x",
3590
- os_version: "sonoma",
3591
- resolution: "1920x1080",
3592
- "browserstack.username": credentials.username,
3593
- "browserstack.accessKey": credentials.accessKey,
3594
- "client.playwrightVersion": opts?.playwrightVersion ?? detectPlaywrightVersion(),
3595
- project: "agent-qa",
3596
- build: "agent-qa-" + Date.now(),
3597
- name: opts?.testName
3598
- };
3599
- }
3600
- function mapMobileCapabilities(match, platform, credentials, opts) {
3601
- if (!opts?.testTimeout) {
3602
- throw new Error("Farm mode requires timeout.test to be set in config");
3603
- }
3604
- return {
3605
- "bstack:options": {
3606
- userName: credentials.username,
3607
- accessKey: credentials.accessKey,
3608
- deviceName: match.deviceName,
3609
- osVersion: match.platformVersion,
3610
- projectName: "agent-qa",
3611
- buildName: "agent-qa-" + Date.now(),
3612
- sessionName: opts.testName,
3613
- idleTimeout: Math.ceil(opts.testTimeout / 1e3)
3614
- }
3615
- };
3616
- }
3617
-
3618
- // src/farm/browserstack.ts
3619
- var import_node_fs5 = require("fs");
3620
- var import_node_path10 = require("path");
3621
- function isAbsoluteAppValue(value) {
3622
- return value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
3623
- }
3624
- function isExplicitRelativePath(value) {
3625
- return value.startsWith("./") || value.startsWith("../");
3626
- }
3627
- async function resolveBrowserStackApp(config) {
3628
- if (!config.app) return void 0;
3629
- if (isAbsoluteAppValue(config.app)) {
3630
- throw new MobileSetupError({
3631
- category: "app-install",
3632
- message: `BrowserStack app value must be a BrowserStack reference or relative upload path: ${config.app}`,
3633
- platform: config.platform,
3634
- appId: config.app,
3635
- sourceTrace: config.appSourceTrace
3636
- });
3637
- }
3638
- const appBaseDir = config.appBaseDir ?? process.cwd();
3639
- const resolvedPath = (0, import_node_path10.resolve)(appBaseDir, config.app);
3640
- const mustExist = isExplicitRelativePath(config.app);
3641
- if (!(0, import_node_fs5.existsSync)(resolvedPath)) {
3642
- if (mustExist) {
3643
- throw new MobileSetupError({
3644
- category: "app-install",
3645
- message: `Configured BrowserStack app upload path not found: ${resolvedPath}`,
3646
- platform: config.platform,
3647
- appId: config.app,
3648
- sourceTrace: config.appSourceTrace
3649
- });
3650
- }
3651
- return config.app;
3652
- }
3653
- const auth = Buffer.from(`${config.credentials.username}:${config.credentials.accessKey}`).toString("base64");
3654
- const form = new FormData();
3655
- form.append("file", new Blob([(0, import_node_fs5.readFileSync)(resolvedPath)]), (0, import_node_path10.basename)(resolvedPath));
3656
- let res;
3657
- try {
3658
- res = await fetch("https://api-cloud.browserstack.com/app-automate/upload", {
3659
- method: "POST",
3660
- headers: { Authorization: `Basic ${auth}` },
3661
- body: form
3662
- });
3663
- } catch (err) {
3664
- throw new MobileSetupError({
3665
- category: "app-install",
3666
- message: `Failed to upload BrowserStack app: ${err instanceof Error ? err.message : String(err)}`,
3667
- platform: config.platform,
3668
- appId: config.app,
3669
- sourceTrace: config.appSourceTrace,
3670
- cause: err
3671
- });
3672
- }
3673
- if (!res.ok) {
3674
- throw new MobileSetupError({
3675
- category: "app-install",
3676
- message: `Failed to upload BrowserStack app: HTTP ${res.status}`,
3677
- platform: config.platform,
3678
- appId: config.app,
3679
- sourceTrace: config.appSourceTrace
3680
- });
3681
- }
3682
- const body = await res.json();
3683
- if (!body.app_url) {
3684
- throw new MobileSetupError({
3685
- category: "app-install",
3686
- message: "Failed to upload BrowserStack app: response missing app_url",
3687
- platform: config.platform,
3688
- appId: config.app,
3689
- sourceTrace: config.appSourceTrace
3690
- });
3691
- }
3692
- return body.app_url;
3693
- }
3694
- var browserstackProvider = {
3695
- name: "BrowserStack",
3696
- slug: "browserstack",
3697
- async resolveWebEndpoint(config) {
3698
- const caps = mapWebCapabilities(config.browser, config.credentials, {
3699
- testName: config.testName,
3700
- playwrightVersion: config.playwrightVersion
3701
- });
3702
- const wsEndpoint = `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify(caps))}`;
3703
- return { wsEndpoint, capabilities: caps };
3704
- },
3705
- async resolveMobileCapabilities(config) {
3706
- const bstackCaps = mapMobileCapabilities(config.match, config.platform, config.credentials, {
3707
- testName: config.testName,
3708
- testTimeout: config.testTimeout
3709
- });
3710
- const automationName = config.match.automationName ?? (config.platform === "android" ? "UiAutomator2" : "XCUITest");
3711
- const platformName = config.platform === "android" ? "Android" : "iOS";
3712
- const capabilities = {
3713
- platformName,
3714
- "appium:automationName": automationName
3715
- };
3716
- for (const [key, value] of Object.entries(config.match)) {
3717
- if (key !== "automationName" && key !== "noReset") {
3718
- capabilities[`appium:${key}`] = value;
3719
- }
3720
- }
3721
- Object.assign(capabilities, bstackCaps);
3722
- const browserName = typeof config.match.browserName === "string" ? config.match.browserName.trim() : "";
3723
- const appStateNoReset = config.appState === "preserve" ? true : config.appState === "reset" ? false : void 0;
3724
- if (!browserName && appStateNoReset !== void 0) {
3725
- capabilities["appium:noReset"] = appStateNoReset;
3726
- }
3727
- const resolvedAppValue = await resolveBrowserStackApp(config);
3728
- if (resolvedAppValue) {
3729
- capabilities["appium:app"] = resolvedAppValue;
3730
- }
3731
- return {
3732
- hostname: "hub.browserstack.com",
3733
- port: 443,
3734
- path: "/wd/hub",
3735
- capabilities
3736
- };
3737
- },
3738
- resolveCredentials(providerConfig) {
3739
- return resolveFarmCredentials("browserstack", providerConfig);
3740
- },
3741
- async validateDevice(deviceName, platform) {
3742
- let credentials;
3743
- try {
3744
- credentials = resolveFarmCredentials("browserstack");
3745
- } catch {
3746
- return { valid: false, suggestions: ["Could not validate \u2014 credentials not configured"] };
3747
- }
3748
- const auth = Buffer.from(`${credentials.username}:${credentials.accessKey}`).toString("base64");
3749
- try {
3750
- const res = await fetch("https://api-cloud.browserstack.com/app-automate/devices.json", {
3751
- headers: { Authorization: `Basic ${auth}` }
3752
- });
3753
- if (!res.ok) {
3754
- return { valid: false, suggestions: ["Could not validate \u2014 API returned " + res.status] };
3755
- }
3756
- const devices = await res.json();
3757
- const targetOs = platform === "android" ? "android" : "ios";
3758
- const match = devices.find(
3759
- (d) => d.device.toLowerCase() === deviceName.toLowerCase() && d.os.toLowerCase() === targetOs
3760
- );
3761
- if (match) return { valid: true };
3762
- 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);
3763
- return { valid: false, suggestions: similar.length > 0 ? similar : void 0 };
3764
- } catch {
3765
- return { valid: false, suggestions: ["Could not validate \u2014 network error"] };
3766
- }
3767
- },
3768
- getSessionUrl(sessionId, type) {
3769
- if (type === "web") {
3770
- return `https://automate.browserstack.com/sessions/${sessionId}`;
3771
- }
3772
- return `https://app-automate.browserstack.com/sessions/${sessionId}`;
3773
- }
3774
- };
3775
-
3776
- // src/farm/registry.ts
3777
- var providers = /* @__PURE__ */ new Map();
3778
- function registerProvider(provider) {
3779
- providers.set(provider.slug, provider);
3780
- }
3781
- function getProvider(slug) {
3782
- return providers.get(slug);
3783
- }
3784
- function listProviders() {
3785
- return Array.from(providers.values());
3786
- }
3787
- function registerAllProviders() {
3788
- registerProvider(browserstackProvider);
3789
- }
3790
-
3791
3552
  // src/suite/member.ts
3792
3553
  function isPlainObject2(val) {
3793
3554
  return typeof val === "object" && val !== null && !Array.isArray(val);
@@ -3823,14 +3584,14 @@ function patchTestForSuite(test, suite, _index) {
3823
3584
 
3824
3585
  // src/validation/validate.ts
3825
3586
  var import_promises8 = require("fs/promises");
3826
- var import_node_fs7 = require("fs");
3827
- var import_node_path14 = __toESM(require("path"), 1);
3587
+ var import_node_fs6 = require("fs");
3588
+ var import_node_path13 = __toESM(require("path"), 1);
3828
3589
  var import_yaml5 = require("yaml");
3829
3590
  var import_glob2 = require("glob");
3830
3591
 
3831
3592
  // src/hooks/parser.ts
3832
3593
  var import_promises7 = require("fs/promises");
3833
- var import_node_path11 = require("path");
3594
+ var import_node_path10 = require("path");
3834
3595
  var import_yaml4 = require("yaml");
3835
3596
  async function parseHooksFile(filePath) {
3836
3597
  let raw;
@@ -3852,31 +3613,31 @@ async function parseHooksFile(filePath) {
3852
3613
  );
3853
3614
  return { hooks: [], errors };
3854
3615
  }
3855
- const dir = (0, import_node_path11.dirname)(filePath);
3616
+ const dir = (0, import_node_path10.dirname)(filePath);
3856
3617
  const hooks = result.data.hooks.map((h) => ({
3857
3618
  ...h,
3858
- file: (0, import_node_path11.resolve)(dir, h.file),
3859
- deps: h.deps.map((d) => (0, import_node_path11.resolve)(dir, d)),
3860
- packageFile: h.packageFile ? (0, import_node_path11.resolve)(dir, h.packageFile) : void 0
3619
+ file: (0, import_node_path10.resolve)(dir, h.file),
3620
+ deps: h.deps.map((d) => (0, import_node_path10.resolve)(dir, d)),
3621
+ packageFile: h.packageFile ? (0, import_node_path10.resolve)(dir, h.packageFile) : void 0
3861
3622
  }));
3862
3623
  return { hooks, errors: [] };
3863
3624
  }
3864
3625
 
3865
3626
  // src/utils/path-validation.ts
3866
- var import_node_path12 = __toESM(require("path"), 1);
3627
+ var import_node_path11 = __toESM(require("path"), 1);
3867
3628
  function isPathInsideDir(childPath, parentDir) {
3868
3629
  if (!childPath) return false;
3869
- const resolved = import_node_path12.default.resolve(parentDir, childPath);
3870
- const relative2 = import_node_path12.default.relative(parentDir, resolved);
3871
- return !!relative2 && !relative2.startsWith("..") && !import_node_path12.default.isAbsolute(relative2);
3630
+ const resolved = import_node_path11.default.resolve(parentDir, childPath);
3631
+ const relative2 = import_node_path11.default.relative(parentDir, resolved);
3632
+ return !!relative2 && !relative2.startsWith("..") && !import_node_path11.default.isAbsolute(relative2);
3872
3633
  }
3873
3634
 
3874
3635
  // src/workspace/workspace-paths.ts
3875
- var import_node_fs6 = require("fs");
3876
- var import_node_path13 = __toESM(require("path"), 1);
3636
+ var import_node_fs5 = require("fs");
3637
+ var import_node_path12 = __toESM(require("path"), 1);
3877
3638
  var import_glob = require("glob");
3878
3639
  function toForwardSlash(value) {
3879
- return value.split(import_node_path13.default.sep).join("/");
3640
+ return value.split(import_node_path12.default.sep).join("/");
3880
3641
  }
3881
3642
  function trimLeadingCurrentDir(value) {
3882
3643
  return toForwardSlash(value).replace(/^\.\//, "");
@@ -3895,22 +3656,22 @@ function assertNonEmptyString(value, key) {
3895
3656
  }
3896
3657
  function resolveWorkspaceScalar(configDir, key, configuredPath) {
3897
3658
  const value = assertNonEmptyString(configuredPath, key);
3898
- const absolutePath = import_node_path13.default.isAbsolute(value) ? import_node_path13.default.resolve(value) : import_node_path13.default.resolve(configDir, value);
3659
+ const absolutePath = import_node_path12.default.isAbsolute(value) ? import_node_path12.default.resolve(value) : import_node_path12.default.resolve(configDir, value);
3899
3660
  return {
3900
3661
  configuredPath: value,
3901
3662
  absolutePath,
3902
- workspaceRelativePath: trimLeadingCurrentDir(import_node_path13.default.relative(configDir, absolutePath))
3663
+ workspaceRelativePath: trimLeadingCurrentDir(import_node_path12.default.relative(configDir, absolutePath))
3903
3664
  };
3904
3665
  }
3905
3666
  function assertInsideWorkspace(configDir, absolutePath, filePath) {
3906
- const relativePath = import_node_path13.default.relative(configDir, absolutePath);
3907
- if (!relativePath || relativePath.startsWith("..") || import_node_path13.default.isAbsolute(relativePath)) {
3667
+ const relativePath = import_node_path12.default.relative(configDir, absolutePath);
3668
+ if (!relativePath || relativePath.startsWith("..") || import_node_path12.default.isAbsolute(relativePath)) {
3908
3669
  throw new Error(`Workspace file path escapes config directory: ${filePath}`);
3909
3670
  }
3910
3671
  return trimLeadingCurrentDir(relativePath);
3911
3672
  }
3912
3673
  function workspaceRelativeIdentityPath(configDir, filePath) {
3913
- const absolutePath = import_node_path13.default.isAbsolute(filePath) ? import_node_path13.default.resolve(filePath) : import_node_path13.default.resolve(configDir, filePath);
3674
+ const absolutePath = import_node_path12.default.isAbsolute(filePath) ? import_node_path12.default.resolve(filePath) : import_node_path12.default.resolve(configDir, filePath);
3914
3675
  return assertInsideWorkspace(configDir, absolutePath, filePath);
3915
3676
  }
3916
3677
  function escapeRegexChar(char) {
@@ -3965,8 +3726,8 @@ function isIgnored(workspace, workspaceRelativePath) {
3965
3726
  return workspace.testPathIgnore.length > 0 && matchesAnyPattern(workspaceRelativePath, workspace.testPathIgnore);
3966
3727
  }
3967
3728
  function resolveWorkspacePaths(input) {
3968
- const configPath = import_node_path13.default.resolve(input.configPath);
3969
- const configDir = import_node_path13.default.dirname(configPath);
3729
+ const configPath = import_node_path12.default.resolve(input.configPath);
3730
+ const configDir = import_node_path12.default.dirname(configPath);
3970
3731
  const workspace = input.config.workspace;
3971
3732
  if (!workspace) {
3972
3733
  throw new Error("Missing required workspace config key: workspace");
@@ -3987,7 +3748,7 @@ function resolveWorkspacePaths(input) {
3987
3748
  ["workspace.envFile", resolved.envFile],
3988
3749
  ["workspace.secretsFile", resolved.secretsFile]
3989
3750
  ]) {
3990
- if (!(0, import_node_fs6.existsSync)(file.absolutePath)) {
3751
+ if (!(0, import_node_fs5.existsSync)(file.absolutePath)) {
3991
3752
  throw new Error(`Configured workspace file not found: ${key} -> ${file.absolutePath}`);
3992
3753
  }
3993
3754
  }
@@ -4002,14 +3763,14 @@ async function discoverWorkspaceFiles(input) {
4002
3763
  absolute: true,
4003
3764
  nodir: true
4004
3765
  });
4005
- return matches.map((match) => import_node_path13.default.resolve(match)).map((absolutePath) => ({
3766
+ return matches.map((match) => import_node_path12.default.resolve(match)).map((absolutePath) => ({
4006
3767
  kind: input.kind,
4007
3768
  absolutePath,
4008
- workspaceRelativePath: trimLeadingCurrentDir(import_node_path13.default.relative(input.workspace.configDir, absolutePath))
3769
+ workspaceRelativePath: trimLeadingCurrentDir(import_node_path12.default.relative(input.workspace.configDir, absolutePath))
4009
3770
  })).sort((a, b) => a.workspaceRelativePath.localeCompare(b.workspaceRelativePath));
4010
3771
  }
4011
3772
  async function resolveWorkspaceFileTarget(input) {
4012
- const absolutePath = import_node_path13.default.isAbsolute(input.filePath) ? import_node_path13.default.resolve(input.filePath) : import_node_path13.default.resolve(input.workspace.configDir, input.filePath);
3773
+ const absolutePath = import_node_path12.default.isAbsolute(input.filePath) ? import_node_path12.default.resolve(input.filePath) : import_node_path12.default.resolve(input.workspace.configDir, input.filePath);
4013
3774
  const workspaceRelativePath = assertInsideWorkspace(input.workspace.configDir, absolutePath, input.filePath);
4014
3775
  if (!isWorkspacePathMatch({
4015
3776
  workspace: input.workspace,
@@ -4018,7 +3779,7 @@ async function resolveWorkspaceFileTarget(input) {
4018
3779
  })) {
4019
3780
  throw new Error(`Workspace ${input.kind} file is not matched by configured workspace patterns: ${workspaceRelativePath}`);
4020
3781
  }
4021
- if (input.requireExisting && !(0, import_node_fs6.existsSync)(absolutePath)) {
3782
+ if (input.requireExisting && !(0, import_node_fs5.existsSync)(absolutePath)) {
4022
3783
  throw new Error(`Workspace ${input.kind} file not found: ${workspaceRelativePath}`);
4023
3784
  }
4024
3785
  return {
@@ -4139,8 +3900,8 @@ function validateSuiteFile(filePath, content, basedir) {
4139
3900
  });
4140
3901
  continue;
4141
3902
  }
4142
- const resolved = import_node_path14.default.resolve(basedir, testPath);
4143
- if (!(0, import_node_fs7.existsSync)(resolved)) {
3903
+ const resolved = import_node_path13.default.resolve(basedir, testPath);
3904
+ if (!(0, import_node_fs6.existsSync)(resolved)) {
4144
3905
  let line = 1;
4145
3906
  let column = 1;
4146
3907
  const seqNode = testsNode;
@@ -4161,13 +3922,13 @@ function validateSuiteFile(filePath, content, basedir) {
4161
3922
  return diagnostics;
4162
3923
  }
4163
3924
  function validateFilename(filePath) {
4164
- const basename3 = import_node_path14.default.basename(filePath);
4165
- if (!VALID_FILENAME_RE.test(basename3)) {
3925
+ const basename2 = import_node_path13.default.basename(filePath);
3926
+ if (!VALID_FILENAME_RE.test(basename2)) {
4166
3927
  return [{
4167
3928
  file: filePath,
4168
3929
  line: 1,
4169
3930
  column: 1,
4170
- message: `Invalid filename "${basename3}" \u2014 expected pattern: [a-zA-Z0-9._-]+.(suite.)?(yaml|yml)`,
3931
+ message: `Invalid filename "${basename2}" \u2014 expected pattern: [a-zA-Z0-9._-]+.(suite.)?(yaml|yml)`,
4171
3932
  severity: "warning"
4172
3933
  }];
4173
3934
  }
@@ -4175,7 +3936,7 @@ function validateFilename(filePath) {
4175
3936
  }
4176
3937
  async function validateHooksFile(hooksFilePath, options = {}) {
4177
3938
  const diagnostics = [];
4178
- if (!(0, import_node_fs7.existsSync)(hooksFilePath)) {
3939
+ if (!(0, import_node_fs6.existsSync)(hooksFilePath)) {
4179
3940
  if (options.required) {
4180
3941
  return [buildDiagnostic(hooksFilePath, `Configured hooks file not found: ${hooksFilePath}`)];
4181
3942
  }
@@ -4195,7 +3956,7 @@ async function validateHooksFile(hooksFilePath, options = {}) {
4195
3956
  return diagnostics;
4196
3957
  }
4197
3958
  for (const hook of hooks) {
4198
- if (!(0, import_node_fs7.existsSync)(hook.file)) {
3959
+ if (!(0, import_node_fs6.existsSync)(hook.file)) {
4199
3960
  diagnostics.push({
4200
3961
  file: hooksFilePath,
4201
3962
  line: 1,
@@ -4205,7 +3966,7 @@ async function validateHooksFile(hooksFilePath, options = {}) {
4205
3966
  });
4206
3967
  }
4207
3968
  for (const dep of hook.deps) {
4208
- if (!(0, import_node_fs7.existsSync)(dep)) {
3969
+ if (!(0, import_node_fs6.existsSync)(dep)) {
4209
3970
  diagnostics.push({
4210
3971
  file: hooksFilePath,
4211
3972
  line: 1,
@@ -4215,7 +3976,7 @@ async function validateHooksFile(hooksFilePath, options = {}) {
4215
3976
  });
4216
3977
  }
4217
3978
  }
4218
- if (hook.packageFile && !(0, import_node_fs7.existsSync)(hook.packageFile)) {
3979
+ if (hook.packageFile && !(0, import_node_fs6.existsSync)(hook.packageFile)) {
4219
3980
  diagnostics.push({
4220
3981
  file: hooksFilePath,
4221
3982
  line: 1,
@@ -4329,7 +4090,7 @@ function validateRequiredWorkspaceFiles(workspace) {
4329
4090
  ["workspace.envFile", workspace.envFile],
4330
4091
  ["workspace.secretsFile", workspace.secretsFile]
4331
4092
  ]) {
4332
- if (!(0, import_node_fs7.existsSync)(file.absolutePath)) {
4093
+ if (!(0, import_node_fs6.existsSync)(file.absolutePath)) {
4333
4094
  diagnostics.push(buildDiagnostic(file.absolutePath, `Configured workspace file not found: ${key} -> ${file.absolutePath}`));
4334
4095
  }
4335
4096
  }
@@ -4347,7 +4108,7 @@ async function validateProject(options = {}) {
4347
4108
  }
4348
4109
  filePaths = resolvedFiles;
4349
4110
  } else if (options.files && options.files.length > 0) {
4350
- filePaths = options.files.map((f) => import_node_path14.default.resolve(f));
4111
+ filePaths = options.files.map((f) => import_node_path13.default.resolve(f));
4351
4112
  } else if (options.workspace) {
4352
4113
  const [testFiles, suiteFiles] = await Promise.all([
4353
4114
  discoverWorkspaceFiles({ workspace: options.workspace, kind: "test" }),
@@ -4390,11 +4151,11 @@ async function validateProject(options = {}) {
4390
4151
  if (d.severity === "error") result.errorCount++;
4391
4152
  else result.warningCount++;
4392
4153
  }
4393
- if (hooksFilePath && hooksDiags.length === 0 && (0, import_node_fs7.existsSync)(hooksFilePath)) {
4154
+ if (hooksFilePath && hooksDiags.length === 0 && (0, import_node_fs6.existsSync)(hooksFilePath)) {
4394
4155
  const { hooks } = await parseHooksFile(hooksFilePath);
4395
4156
  const hookIds = new Set(hooks.map((h) => h.id));
4396
4157
  for (const filePath of filePaths) {
4397
- const workspaceRelativePath = options.workspace ? import_node_path14.default.relative(options.workspace.configDir, filePath).split(import_node_path14.default.sep).join("/") : filePath;
4158
+ const workspaceRelativePath = options.workspace ? import_node_path13.default.relative(options.workspace.configDir, filePath).split(import_node_path13.default.sep).join("/") : filePath;
4398
4159
  const isWorkspaceFile = options.workspace ? isWorkspacePathMatch({ workspace: options.workspace, kind: isSuiteFile(filePath) ? "suite" : "test", workspaceRelativePath }) : true;
4399
4160
  if (!isWorkspaceFile) continue;
4400
4161
  try {
@@ -4519,8 +4280,6 @@ var ATTR_RUNNER = "agent-qa.runner";
4519
4280
  var ATTR_GATE_RUN = "agent-qa.gate-run";
4520
4281
  var RUN_ATTRIBUTE_KEY_MAX_LENGTH = 256;
4521
4282
  var RUN_ATTRIBUTE_VALUE_MAX_LENGTH = 4096;
4522
- var RUN_ATTRIBUTE_TRIGGERS = ["cli", "dashboard", "api", "mcp"];
4523
- var RUN_ATTRIBUTE_RUNNERS = ["local", "browserstack"];
4524
4283
  function isPlainRecord(value) {
4525
4284
  return typeof value === "object" && value !== null && !Array.isArray(value);
4526
4285
  }
@@ -4535,43 +4294,6 @@ function assertValidUserAttributeKey(key, sourceLabel) {
4535
4294
  throw new Error(`${sourceLabel}: Attribute key "${key}" exceeds ${RUN_ATTRIBUTE_KEY_MAX_LENGTH} characters`);
4536
4295
  }
4537
4296
  }
4538
- function assertValidAttributeKeyShape(key, sourceLabel) {
4539
- if (key.length === 0) {
4540
- throw new Error(`${sourceLabel}: Attribute key must be non-empty`);
4541
- }
4542
- if (key.length > RUN_ATTRIBUTE_KEY_MAX_LENGTH) {
4543
- throw new Error(`${sourceLabel}: Attribute key "${key}" exceeds ${RUN_ATTRIBUTE_KEY_MAX_LENGTH} characters`);
4544
- }
4545
- }
4546
- function assertValidAttributeValue(key, value, sourceLabel) {
4547
- if (typeof value !== "string") {
4548
- throw new Error(`${sourceLabel}: Attribute value for "${key}" must be a string`);
4549
- }
4550
- if (value.length > RUN_ATTRIBUTE_VALUE_MAX_LENGTH) {
4551
- throw new Error(`${sourceLabel}: Attribute value for "${key}" exceeds ${RUN_ATTRIBUTE_VALUE_MAX_LENGTH} characters`);
4552
- }
4553
- }
4554
- function assertValidInternalAttribute(key, value, sourceLabel) {
4555
- if (key === ATTR_TRIGGER) {
4556
- if (!RUN_ATTRIBUTE_TRIGGERS.includes(value)) {
4557
- throw new Error(`${sourceLabel}: Attribute value for "${key}" must be one of: ${RUN_ATTRIBUTE_TRIGGERS.join(", ")}`);
4558
- }
4559
- return;
4560
- }
4561
- if (key === ATTR_RUNNER) {
4562
- if (!RUN_ATTRIBUTE_RUNNERS.includes(value)) {
4563
- throw new Error(`${sourceLabel}: Attribute value for "${key}" must be one of: ${RUN_ATTRIBUTE_RUNNERS.join(", ")}`);
4564
- }
4565
- return;
4566
- }
4567
- if (key === ATTR_GATE_RUN) {
4568
- if (value !== "true") {
4569
- throw new Error(`${sourceLabel}: Attribute value for "${key}" must be "true"`);
4570
- }
4571
- return;
4572
- }
4573
- throw new Error(`${sourceLabel}: Attribute key "${key}" uses the reserved prefix "${INTERNAL_ATTRIBUTE_PREFIX}"`);
4574
- }
4575
4297
  function validateUserRunAttributes(input, sourceLabel = "run attributes") {
4576
4298
  if (input === void 0 || input === null) return {};
4577
4299
  if (!isPlainRecord(input)) {
@@ -4590,22 +4312,6 @@ function validateUserRunAttributes(input, sourceLabel = "run attributes") {
4590
4312
  }
4591
4313
  return attributes;
4592
4314
  }
4593
- function validateTrustedRunAttributes(input, sourceLabel = "run attributes") {
4594
- if (input === void 0 || input === null) return {};
4595
- if (!isPlainRecord(input)) {
4596
- throw new Error(`${sourceLabel}: attributes must be a plain object`);
4597
- }
4598
- const attributes = /* @__PURE__ */ Object.create(null);
4599
- for (const [key, value] of Object.entries(input)) {
4600
- assertValidAttributeKeyShape(key, sourceLabel);
4601
- assertValidAttributeValue(key, value, sourceLabel);
4602
- if (key.startsWith(INTERNAL_ATTRIBUTE_PREFIX)) {
4603
- assertValidInternalAttribute(key, value, sourceLabel);
4604
- }
4605
- attributes[key] = value;
4606
- }
4607
- return attributes;
4608
- }
4609
4315
  function parseRunAttrFlags(flags) {
4610
4316
  const rawAttributes = /* @__PURE__ */ Object.create(null);
4611
4317
  const seen = /* @__PURE__ */ new Set();
@@ -4665,9 +4371,9 @@ function formatRunAttributesBlock(attributes) {
4665
4371
  }
4666
4372
 
4667
4373
  // src/version.ts
4668
- var import_node_module3 = require("module");
4669
- var import_meta3 = {};
4670
- var require2 = (0, import_node_module3.createRequire)(typeof __filename === "string" ? __filename : import_meta3.url);
4374
+ var import_node_module2 = require("module");
4375
+ var import_meta2 = {};
4376
+ var require2 = (0, import_node_module2.createRequire)(typeof __filename === "string" ? __filename : import_meta2.url);
4671
4377
  function readPackageVersion() {
4672
4378
  try {
4673
4379
  const manifest = require2("../package.json");
@@ -4685,17 +4391,17 @@ function getAgentQaVersion() {
4685
4391
 
4686
4392
  // src/version-check/index.ts
4687
4393
  var import_promises9 = require("fs/promises");
4688
- var import_node_module4 = require("module");
4394
+ var import_node_module3 = require("module");
4689
4395
  var import_node_os2 = require("os");
4690
- var import_node_path15 = require("path");
4691
- var import_meta4 = {};
4692
- var require3 = (0, import_node_module4.createRequire)(typeof __filename === "string" ? __filename : import_meta4.url);
4396
+ var import_node_path14 = require("path");
4397
+ var import_meta3 = {};
4398
+ var require3 = (0, import_node_module3.createRequire)(typeof __filename === "string" ? __filename : import_meta3.url);
4693
4399
  var AGENT_QA_NPM_REGISTRY_URL = "https://registry.npmjs.org/agent-qa";
4694
4400
  var VERSION_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
4695
4401
  var VERSION_CHECK_TIMEOUT_MS = 5e3;
4696
4402
  var semverApi;
4697
4403
  function getAgentQaVersionCheckCachePath(homeDir) {
4698
- return (0, import_node_path15.join)(homeDir ?? (0, import_node_os2.homedir)(), ".agent-qa", "version-check.json");
4404
+ return (0, import_node_path14.join)(homeDir ?? (0, import_node_os2.homedir)(), ".agent-qa", "version-check.json");
4699
4405
  }
4700
4406
  async function getAgentQaUpdateStatus(options = {}) {
4701
4407
  const installedVersion = getInstalledVersion();
@@ -4829,7 +4535,7 @@ async function readVersionCheckCache(cachePath) {
4829
4535
  }
4830
4536
  async function writeVersionCheckCache(cachePath, cache) {
4831
4537
  try {
4832
- await (0, import_promises9.mkdir)((0, import_node_path15.dirname)(cachePath), { recursive: true });
4538
+ await (0, import_promises9.mkdir)((0, import_node_path14.dirname)(cachePath), { recursive: true });
4833
4539
  await (0, import_promises9.writeFile)(cachePath, JSON.stringify(cache, null, 2), {
4834
4540
  encoding: "utf-8",
4835
4541
  mode: 384
@@ -4875,7 +4581,6 @@ function isRecord2(value) {
4875
4581
  AUTH_STATE_SCHEMA_VERSION,
4876
4582
  AUTH_STATE_SLUG_PATTERN,
4877
4583
  AccessibilityConfigSchema,
4878
- ActionPlanSchema,
4879
4584
  AdapterLogDataSchema,
4880
4585
  AgentLogDataSchema,
4881
4586
  AgentQaConfigSchema,
@@ -4932,7 +4637,6 @@ function isRecord2(value) {
4932
4637
  PlannerConfigSchema,
4933
4638
  PlannerLogDataSchema,
4934
4639
  PluginsSchema,
4935
- ProviderConfigSchema,
4936
4640
  ProviderPluginDeclarationSchema,
4937
4641
  RUNTIME_IMAGE_MAP,
4938
4642
  RUN_ATTRIBUTE_KEY_MAX_LENGTH,
@@ -4950,7 +4654,6 @@ function isRecord2(value) {
4950
4654
  TestMetaSchema,
4951
4655
  TestStepSchema,
4952
4656
  TimeoutConfigSchema,
4953
- TransportSchema,
4954
4657
  UseSchema,
4955
4658
  VALID_FILENAME_RE,
4956
4659
  VERSION_CHECK_CACHE_TTL_MS,
@@ -4958,7 +4661,6 @@ function isRecord2(value) {
4958
4661
  WEBVIEW_SCROLLERS_SCRIPT,
4959
4662
  WorkspaceSchema,
4960
4663
  applyPatchSetToTestDoc,
4961
- browserstackProvider,
4962
4664
  buildAuthStateHookEnv,
4963
4665
  buildInternalRunAttributes,
4964
4666
  buildRefEntries,
@@ -4971,7 +4673,6 @@ function isRecord2(value) {
4971
4673
  createAuthFetch,
4972
4674
  deriveMobileScroll,
4973
4675
  detectGlobOverlap,
4974
- detectPlaywrightVersion,
4975
4676
  discoverWorkspaceFiles,
4976
4677
  estimateTokens,
4977
4678
  formatAppiumInstallGuidance,
@@ -4990,21 +4691,21 @@ function isRecord2(value) {
4990
4691
  getAuthPath,
4991
4692
  getCredential,
4992
4693
  getLLMAuthProviderPlugin,
4993
- getProvider,
4694
+ guardJsonText,
4695
+ guardRunText,
4696
+ hookRunnerImage,
4994
4697
  isCanonicalRunId,
4698
+ isGitHubActions,
4995
4699
  isLoopbackHost,
4996
4700
  isPathInsideDir,
4997
4701
  isReservedAuthStateHookEnvKey,
4998
4702
  isWorkspacePathMatch,
4999
4703
  listAuthStateMetadata,
5000
4704
  listLLMAuthProviderPlugins,
5001
- listProviders,
5002
4705
  loadLLMAuthPlugins,
5003
4706
  loadProviderPlugins,
5004
4707
  localAppiumBinaryNames,
5005
4708
  locateRunsDbPath,
5006
- mapMobileCapabilities,
5007
- mapWebCapabilities,
5008
4709
  mergeRunAttributes,
5009
4710
  normalizeAuthStateUse,
5010
4711
  normalizeRole,
@@ -5019,21 +4720,21 @@ function isRecord2(value) {
5019
4720
  parseTestFileWithTokens,
5020
4721
  parseWebViewScrollers,
5021
4722
  patchTestForSuite,
4723
+ printRunText,
5022
4724
  readAuth,
5023
4725
  readAuthStateMetadata,
5024
- registerAllProviders,
4726
+ refuseRetiredLocalBindings,
5025
4727
  registerLLMAuthProviderPlugin,
5026
4728
  registerLLMAuthProviderPlugins,
5027
- registerProvider,
5028
4729
  removeAuth,
5029
4730
  removeAuthStateFiles,
5030
4731
  removeAuthStateTarget,
5031
4732
  renderPatchedTestDoc,
4733
+ reserveStdoutForData,
5032
4734
  resolveAppiumExecutable,
5033
4735
  resolveAuthStateForRun,
5034
4736
  resolveAuthStatePaths,
5035
4737
  resolveAuthStateRoot,
5036
- resolveFarmCredentials,
5037
4738
  resolveFileReferences,
5038
4739
  resolveKnowledgeRoot,
5039
4740
  resolveLLMAuth,
@@ -5049,7 +4750,6 @@ function isRecord2(value) {
5049
4750
  validateProject,
5050
4751
  validateSuiteFile,
5051
4752
  validateTestFile,
5052
- validateTrustedRunAttributes,
5053
4753
  validateUserRunAttributes,
5054
4754
  warnIfOutOfBounds,
5055
4755
  webViewSwipeSignal,