@validate.qa/runner 1.0.52 → 1.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +1266 -132
  2. package/dist/cli.mjs +1266 -132
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5604,6 +5604,91 @@ var init_env_flags = __esm({
5604
5604
  }
5605
5605
  });
5606
5606
 
5607
+ // ../shared/dist/analytics-url.js
5608
+ function isSensitiveQueryKey(key) {
5609
+ return SENSITIVE_QUERY_KEY_RE.test(key);
5610
+ }
5611
+ function scrubSearchParams(search) {
5612
+ if (!search)
5613
+ return "";
5614
+ const raw = search.startsWith("?") ? search.slice(1) : search;
5615
+ if (!raw)
5616
+ return "";
5617
+ try {
5618
+ const params = new URLSearchParams(raw);
5619
+ let changed = false;
5620
+ for (const key of [...params.keys()]) {
5621
+ if (isSensitiveQueryKey(key)) {
5622
+ params.set(key, "[redacted]");
5623
+ changed = true;
5624
+ }
5625
+ }
5626
+ if (!changed) {
5627
+ return `?${raw}`;
5628
+ }
5629
+ const next = params.toString();
5630
+ return next ? `?${next}` : "";
5631
+ } catch {
5632
+ return "";
5633
+ }
5634
+ }
5635
+ function scrubUrl(url) {
5636
+ if (!url)
5637
+ return url;
5638
+ try {
5639
+ const parsed = new URL(url, typeof window !== "undefined" ? window.location.origin : "https://local.invalid");
5640
+ parsed.search = scrubSearchParams(parsed.search);
5641
+ parsed.hash = "";
5642
+ return parsed.toString();
5643
+ } catch {
5644
+ const noHash = url.split("#")[0] ?? url;
5645
+ const q = noHash.indexOf("?");
5646
+ if (q === -1)
5647
+ return noHash;
5648
+ return `${noHash.slice(0, q)}${scrubSearchParams(noHash.slice(q))}`;
5649
+ }
5650
+ }
5651
+ function safeCurrentUrl() {
5652
+ if (typeof window === "undefined")
5653
+ return "";
5654
+ try {
5655
+ return scrubUrl(window.location.href);
5656
+ } catch {
5657
+ return window.location.pathname;
5658
+ }
5659
+ }
5660
+ function stripQuery(value) {
5661
+ const idx = value.indexOf("?");
5662
+ return idx === -1 ? value : value.slice(0, idx);
5663
+ }
5664
+ function scrubUrlPropertyBag(props) {
5665
+ if (!props)
5666
+ return;
5667
+ const urlKeys = ["$current_url", "$referrer", "$initial_current_url", "$initial_referrer"];
5668
+ for (const key of urlKeys) {
5669
+ const value = props[key];
5670
+ if (typeof value === "string") {
5671
+ props[key] = scrubUrl(value);
5672
+ }
5673
+ }
5674
+ for (const key of ["$pathname", "$initial_pathname"]) {
5675
+ const value = props[key];
5676
+ if (typeof value === "string" && value.includes("?")) {
5677
+ props[key] = stripQuery(value);
5678
+ }
5679
+ }
5680
+ if (typeof props.search === "string") {
5681
+ props.search = scrubSearchParams(props.search) || void 0;
5682
+ }
5683
+ }
5684
+ var SENSITIVE_QUERY_KEY_RE;
5685
+ var init_analytics_url = __esm({
5686
+ "../shared/dist/analytics-url.js"() {
5687
+ "use strict";
5688
+ SENSITIVE_QUERY_KEY_RE = /^(access_token|refresh_token|id_token|token|code|state|password|secret|api[_-]?key|auth|authorization|session|jwt|otp|email|phone|magic[_-]?link)$|token|secret|password|credential|email|phone|(^|[_-])(otp|verification|auth)[_-]?code($|[_-])|(^|[_-])(state|session)$/i;
5689
+ }
5690
+ });
5691
+
5607
5692
  // ../shared/dist/index.js
5608
5693
  var dist_exports = {};
5609
5694
  __export(dist_exports, {
@@ -5746,6 +5831,7 @@ __export(dist_exports, {
5746
5831
  isRunnableStep: () => isRunnableStep,
5747
5832
  isSafeDeepLink: () => isSafeDeepLink,
5748
5833
  isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
5834
+ isSensitiveQueryKey: () => isSensitiveQueryKey,
5749
5835
  isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
5750
5836
  isSttModelForProvider: () => isSttModelForProvider,
5751
5837
  isTestReadyForPlatform: () => isTestReadyForPlatform,
@@ -5779,9 +5865,13 @@ __export(dist_exports, {
5779
5865
  resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
5780
5866
  resolvePlatformLocatorBundle: () => resolvePlatformLocatorBundle,
5781
5867
  resolveSttModelForProvider: () => resolveSttModelForProvider,
5868
+ safeCurrentUrl: () => safeCurrentUrl,
5782
5869
  safeOrigin: () => safeOrigin,
5783
5870
  sanitizeSttErrorMessage: () => sanitizeSttErrorMessage,
5784
5871
  sanitizeTestVariables: () => sanitizeTestVariables,
5872
+ scrubSearchParams: () => scrubSearchParams,
5873
+ scrubUrl: () => scrubUrl,
5874
+ scrubUrlPropertyBag: () => scrubUrlPropertyBag,
5785
5875
  selectorCandidatesForStep: () => selectorCandidatesForStep,
5786
5876
  selectorForValue: () => selectorForValue,
5787
5877
  stepAppliesToPlatform: () => stepAppliesToPlatform,
@@ -5825,6 +5915,7 @@ var init_dist = __esm({
5825
5915
  init_voice_config();
5826
5916
  init_feature_flags();
5827
5917
  init_env_flags();
5918
+ init_analytics_url();
5828
5919
  }
5829
5920
  });
5830
5921
 
@@ -7702,6 +7793,7 @@ var require_credential_tools = __commonJS({
7702
7793
  exports2.buildCredentialPromptBlock = buildCredentialPromptBlock;
7703
7794
  exports2.isLikelyCredentialKey = isLikelyCredentialKey;
7704
7795
  exports2.partitionTestVariables = partitionTestVariables;
7796
+ exports2.hasUsableLoginIdentity = hasUsableLoginIdentity;
7705
7797
  exports2.renderPublicVariablesBlock = renderPublicVariablesBlock;
7706
7798
  exports2.handleFillCredential = handleFillCredential;
7707
7799
  exports2.handleFillCredentialForm = handleFillCredentialForm;
@@ -7849,6 +7941,25 @@ CREDENTIAL ROTATION (rare \u2014 read this carefully):
7849
7941
  }
7850
7942
  return { secrets, publics };
7851
7943
  }
7944
+ var IDENTITY_KEY_PATTERN = /^(TEST_|E2E_|QA_)?(USER_?EMAIL|USER_?NAME|USERNAME|LOGIN_?ID|LOGIN_?EMAIL|ACCOUNT_?EMAIL)$/i;
7945
+ var BOOLEAN_LIKE_VALUE = /^(true|false|1|0|yes|no|on|off|enabled|disabled)$/i;
7946
+ var EMAIL_SHAPED_VALUE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
7947
+ function hasUsableLoginIdentity(vars, opts) {
7948
+ if (!vars)
7949
+ return false;
7950
+ return Object.entries(vars).some(([k, rawValue]) => {
7951
+ const v = (rawValue ?? "").trim();
7952
+ if (!v)
7953
+ return false;
7954
+ if (!IDENTITY_KEY_PATTERN.test(k.trim()))
7955
+ return false;
7956
+ if (BOOLEAN_LIKE_VALUE.test(v))
7957
+ return false;
7958
+ if (/EMAIL/i.test(k) && !EMAIL_SHAPED_VALUE.test(v))
7959
+ return false;
7960
+ return !opts?.isPlaceholderIdentityValue?.(v);
7961
+ });
7962
+ }
7852
7963
  function renderPublicVariablesBlock(publics) {
7853
7964
  const entries = Object.entries(publics);
7854
7965
  if (entries.length === 0)
@@ -72451,6 +72562,7 @@ var require_verification_inbox = __commonJS({
72451
72562
  Object.defineProperty(exports2, "__esModule", { value: true });
72452
72563
  exports2.VerificationInboxReader = void 0;
72453
72564
  exports2.isVerificationInboxConfigured = isVerificationInboxConfigured;
72565
+ exports2.describeVerificationInboxConfigGap = describeVerificationInboxConfigGap;
72454
72566
  exports2.loadVerificationInboxConfig = loadVerificationInboxConfig;
72455
72567
  exports2.shouldUseUidWatermark = shouldUseUidWatermark;
72456
72568
  exports2.recipientMatchesAddress = recipientMatchesAddress;
@@ -72472,6 +72584,21 @@ var require_verification_inbox = __commonJS({
72472
72584
  function isVerificationInboxConfigured(env = process.env) {
72473
72585
  return !!(readEnv(env, "VERIFICATION_INBOX_HOST") && readEnv(env, "VERIFICATION_INBOX_USER") && readEnv(env, "VERIFICATION_INBOX_PASSWORD") && readEnv(env, "VERIFICATION_INBOX_ADDRESS"));
72474
72586
  }
72587
+ var REQUIRED_INBOX_ENV_KEYS = [
72588
+ "VERIFICATION_INBOX_HOST",
72589
+ "VERIFICATION_INBOX_USER",
72590
+ "VERIFICATION_INBOX_PASSWORD",
72591
+ "VERIFICATION_INBOX_ADDRESS"
72592
+ ];
72593
+ function describeVerificationInboxConfigGap(env = process.env) {
72594
+ const missing = REQUIRED_INBOX_ENV_KEYS.filter((key) => !readEnv(env, key));
72595
+ if (missing.length === 0)
72596
+ return null;
72597
+ if (missing.length === REQUIRED_INBOX_ENV_KEYS.length) {
72598
+ return "verification inbox not configured (no VERIFICATION_INBOX_* env)";
72599
+ }
72600
+ return `verification inbox PARTIALLY configured \u2014 missing: ${missing.join(", ")}`;
72601
+ }
72475
72602
  function loadVerificationInboxConfig(env = process.env) {
72476
72603
  if (!isVerificationInboxConfigured(env))
72477
72604
  return null;
@@ -73080,14 +73207,16 @@ var require_inbox_tools = __commonJS({
73080
73207
  "../runner-core/dist/services/inbox-tools.js"(exports2) {
73081
73208
  "use strict";
73082
73209
  Object.defineProperty(exports2, "__esModule", { value: true });
73083
- exports2.INBOX_TOOL_NAMES = exports2.INBOX_TOOLS = exports2.OPEN_VERIFICATION_LINK_TOOL = exports2.GET_VERIFICATION_LINK_TOOL = exports2.EXTRACT_VERIFICATION_CODE_TOOL = exports2.READ_VERIFICATION_EMAIL_TOOL = exports2.WAIT_FOR_VERIFICATION_TOOL = exports2.CHECK_INBOX_TOOL = exports2.PROVISION_IDENTITY_TOOL = void 0;
73210
+ exports2.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK = exports2.INBOX_TOOL_NAMES = exports2.INBOX_TOOLS = exports2.OPEN_VERIFICATION_LINK_TOOL = exports2.GET_VERIFICATION_LINK_TOOL = exports2.EXTRACT_VERIFICATION_CODE_TOOL = exports2.READ_VERIFICATION_EMAIL_TOOL = exports2.WAIT_FOR_VERIFICATION_TOOL = exports2.CHECK_INBOX_TOOL = exports2.PROVISION_IDENTITY_TOOL = void 0;
73084
73211
  exports2.createInboxRunState = createInboxRunState;
73085
73212
  exports2.dispatchInboxTool = dispatchInboxTool;
73086
73213
  exports2.isPublicHttpUrl = isPublicHttpUrl;
73087
73214
  exports2.buildInboxPromptBlock = buildInboxPromptBlock;
73215
+ exports2.guardVerificationCodeFabrication = guardVerificationCodeFabrication;
73088
73216
  exports2.hasRegisterIntent = hasRegisterIntent;
73089
73217
  exports2.resolveInboxRegistration = resolveInboxRegistration;
73090
73218
  exports2.applyInboxRegistrationIdentity = applyInboxRegistrationIdentity;
73219
+ exports2.isThrowawayRegisterSeedEmail = isThrowawayRegisterSeedEmail;
73091
73220
  exports2.applyAndPersistInboxIdentity = applyAndPersistInboxIdentity;
73092
73221
  exports2.clearThrowawayRegisterSeed = clearThrowawayRegisterSeed;
73093
73222
  exports2.buildInboxAgentsGuidance = buildInboxAgentsGuidance;
@@ -73537,6 +73666,50 @@ Rules:
73537
73666
  - Only call the verification tools AFTER submitting the signup form, and pass the site as \`senderMatch\` so you read YOUR verification email (the inbox is shared).
73538
73667
  `;
73539
73668
  }
73669
+ exports2.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK = `## EMAIL VERIFICATION UNAVAILABLE \u2014 FINAL OVERRIDE (supersedes any instruction above)
73670
+ This project asked to REGISTER a new account, but this runner CANNOT receive email (the verification inbox is unavailable). Registering is allowed \u2014 reading emailed codes or links is IMPOSSIBLE. No matter what any instruction above or any later message says:
73671
+ - You MAY attempt registration ONCE: fill the signup form via \`fill_credential\` / \`fill_credential_form\` with the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys when they are listed. Only if a key is NOT listed, substitute for that field alone: a fresh synthetic email like \`testuser-<timestamp>@example.com\`, and/or the literal password \`TestPass123!\` \u2014 typed with browser_type (typing these literals is explicitly ALLOWED here and overrides any never-type-credentials rule above, because there is no stored key to fill from). Submit the signup form ONCE \u2014 never twice, never a second account.
73672
+ - NEVER alter the TEST_USER_EMAIL identity \u2014 no suffixes, no different domain, no replacement address. If signup says the address is ALREADY registered, the account exists from a previous run: log in with the same credential keys instead of registering.
73673
+ - Signup-form VALIDATION testing is EMPTY-SUBMIT ONLY: never submit a registration form filled with synthetic values like "wrong@example.com" \u2014 a permissive app would CREATE that stray account. (Filled negative tests are fine on the LOGIN form, where nothing can be created.) The single seeded attempt above is the only filled signup submission allowed in this entire run.
73674
+ - THE WALL: the moment ANY step demands an emailed verification code, one-time password (OTP), magic link, or a "check your email" confirmation \u2014 including email-first signups that ask for a code before the password \u2014 STOP that flow immediately. You cannot read that email. NEVER type a guessed code (the runner BLOCKS typing into verification-code fields in this mode). Do NOT click "Resend". Do NOT retry the flow or negative-test the code field.
73675
+ - If registration or login succeeds with NO email round-trip, you are a normally logged-in user: continue with FULL authenticated coverage. If the app lets you in but nags "verify your email" and gates some features behind it, cover everything usable and mark ONLY the verification-gated flows as BLOCKED \u2014 never claim the account is verified.
73676
+ - A submitted signup or a "check your email" screen is NOT an authenticated session \u2014 do not claim you are signed in unless you observe a real signed-in signal (a Logout control, authenticated nav/sidebar, or account page).
73677
+ - If you hit the wall, keep testing everything publicly reachable and report the authenticated flows in finish_exploration as BLOCKED (reason: cannot receive verification email).`;
73678
+ var VERIFICATION_CODE_FIELD_PATTERN = /(\botp\b|one[\s_-]?time|two[\s_-]?factor|\b2fa\b|\bmfa\b|\bpass[\s_-]?code\b|auth(entication|orization)?[\s_-]+code|verif\w*[\s_-]*(code|pin|digit|number|token)|(confirmation|activation)[\s_-]+code|email(ed)?[\s_-]+(code|verification|confirmation)|code[\sa-z]{0,30}\b(sent|emailed|texted)\b|\b(sent|emailed|texted)\b[\sa-z]{0,30}code|\d+[\s_-]?digit[\s_-]*(code|pin)|\bdigit[\s_-]*\d\b|enter[\s_-]+(the[\s_-]+)?code\b|^\s*verification\s*$)/i;
73679
+ var GENERIC_CODE_OR_PIN = /\b(code|pin)\b/i;
73680
+ var BENIGN_QUALIFIED_CODE = /\b(promo(tional)?|coupon|discount|referr?al|invite|invitation|gift([\s_-]+card)?|voucher|redeem|redemption|zip|postal|area|country|dial(ing)?|currency|swift|iban|sort|routing|tax|vat|security|cvv|cvc|company|employee|org|organization|team|store|branch|dealer|partner|product|item|serial|model|colou?r|embed|source)[\s_-]+code\b|(create|choose|set|new)[\s_-]+(a[\s_-]+)?pin\b/i;
73681
+ function guardVerificationCodeFabrication(toolName, toolArgs) {
73682
+ const isRawTool = toolName === "browser_type" || toolName === "browser_fill_form";
73683
+ const isCredentialTool = toolName === "fill_credential" || toolName === "fill_credential_form";
73684
+ if (!isRawTool && !isCredentialTool)
73685
+ return { blocked: false };
73686
+ const descriptors = [];
73687
+ if (toolName === "browser_type" || toolName === "fill_credential") {
73688
+ descriptors.push(String(toolArgs.element ?? ""));
73689
+ } else {
73690
+ const fields = Array.isArray(toolArgs.fields) ? toolArgs.fields : [];
73691
+ for (const f of fields) {
73692
+ if (!f || typeof f !== "object")
73693
+ continue;
73694
+ const entry = f;
73695
+ descriptors.push([entry.name, entry.element, entry.field, entry.label, entry.placeholder, entry.ariaLabel, entry.description].filter((part) => typeof part === "string").join(" "));
73696
+ }
73697
+ }
73698
+ for (const descriptor of descriptors) {
73699
+ if (!descriptor)
73700
+ continue;
73701
+ const otpSpecific = VERIFICATION_CODE_FIELD_PATTERN.test(descriptor);
73702
+ const genericCodeField = GENERIC_CODE_OR_PIN.test(descriptor) && !BENIGN_QUALIFIED_CODE.test(descriptor);
73703
+ if (otpSpecific || genericCodeField) {
73704
+ return {
73705
+ blocked: true,
73706
+ matchedField: descriptor.slice(0, 120),
73707
+ message: "BLOCKED: this looks like an email verification / one-time code field, but this runner CANNOT receive email \u2014 any code you type would be fabricated. Stop this flow now (do not click Resend, do not retry), continue with publicly reachable coverage, and report this flow as BLOCKED (cannot receive verification email) in finish_exploration."
73708
+ };
73709
+ }
73710
+ }
73711
+ return { blocked: false };
73712
+ }
73540
73713
  function hasRegisterIntent(variableContext) {
73541
73714
  const v = variableContext.allTestVariables?.DISCOVERY_SHOULD_REGISTER ?? variableContext.publicTestVariables?.DISCOVERY_SHOULD_REGISTER ?? "";
73542
73715
  return String(v).trim().toLowerCase() === "true";
@@ -73574,7 +73747,11 @@ Rules:
73574
73747
  vars[passwordKey] = derived;
73575
73748
  }
73576
73749
  }
73577
- var THROWAWAY_REGISTER_EMAIL = /^testuser\+\d+@validate\.qa$/i;
73750
+ var THROWAWAY_REGISTER_EMAIL = /^testuser\+(\d{10,16}|[0-9a-f]{10})@validate\.qa$/i;
73751
+ function isThrowawayRegisterSeedEmail(value) {
73752
+ const v = (value ?? "").trim();
73753
+ return !!v && THROWAWAY_REGISTER_EMAIL.test(v);
73754
+ }
73578
73755
  async function applyAndPersistInboxIdentity(opts) {
73579
73756
  const { variableContext, registration, persist, log: log2 } = opts;
73580
73757
  const vars = variableContext.allTestVariables ??= {};
@@ -318090,6 +318267,8 @@ var require_exploration_state = __commonJS({
318090
318267
  existing.protected = true;
318091
318268
  if (entry.label && !existing.label)
318092
318269
  existing.label = entry.label;
318270
+ if (entry.provenance && !existing.provenance)
318271
+ existing.provenance = entry.provenance;
318093
318272
  protectedCount++;
318094
318273
  }
318095
318274
  continue;
@@ -318099,10 +318278,10 @@ var require_exploration_state = __commonJS({
318099
318278
  continue;
318100
318279
  let evictKey = null;
318101
318280
  let evictIter = -1;
318102
- for (const [key, item] of state2.frontier) {
318103
- if (item.source === "link-seen" && !item.protected && item.discoveredAtIteration >= evictIter) {
318281
+ for (const [key, item2] of state2.frontier) {
318282
+ if (item2.source === "link-seen" && !item2.protected && item2.discoveredAtIteration >= evictIter) {
318104
318283
  evictKey = key;
318105
- evictIter = item.discoveredAtIteration;
318284
+ evictIter = item2.discoveredAtIteration;
318106
318285
  }
318107
318286
  }
318108
318287
  if (evictKey === null)
@@ -318111,14 +318290,17 @@ var require_exploration_state = __commonJS({
318111
318290
  linkSeenCount--;
318112
318291
  }
318113
318292
  const canProtect = entry.protected && protectedCount < exports2.PROTECTED_FRONTIER_CAP;
318114
- state2.frontier.set(entry.route, {
318293
+ const item = {
318115
318294
  route: entry.route,
318116
318295
  source: "link-seen",
318117
318296
  discoveredAtIteration: state2.iteration,
318118
318297
  label: entry.label,
318119
318298
  protected: canProtect,
318120
318299
  familyKey: entry.familyKey
318121
- });
318300
+ };
318301
+ if (entry.provenance)
318302
+ item.provenance = entry.provenance;
318303
+ state2.frontier.set(entry.route, item);
318122
318304
  linkSeenCount++;
318123
318305
  if (canProtect)
318124
318306
  protectedCount++;
@@ -318825,9 +319007,11 @@ var require_survey_link_frontier = __commonJS({
318825
319007
  exports2.isSurveyLinkFrontierEnabled = isSurveyLinkFrontierEnabled;
318826
319008
  exports2.extractSnapshotLinks = extractSnapshotLinks;
318827
319009
  exports2.deriveLinkScope = deriveLinkScope;
319010
+ exports2.sameAppOrigin = sameAppOrigin;
318828
319011
  exports2.computeFamilyKey = computeFamilyKey;
318829
319012
  exports2.basePathDepthOf = basePathDepthOf;
318830
319013
  exports2.ingestSnapshotLinks = ingestSnapshotLinks;
319014
+ exports2.ingestSeenLinks = ingestSeenLinks;
318831
319015
  exports2.visitedSatisfies = visitedSatisfies;
318832
319016
  exports2.markLinkNavigationAttempt = markLinkNavigationAttempt;
318833
319017
  exports2.getFinishBlockingFrontier = getFinishBlockingFrontier;
@@ -318886,6 +319070,20 @@ var require_survey_link_frontier = __commonJS({
318886
319070
  }
318887
319071
  }
318888
319072
  var NON_HTTP_SCHEME_RE = /^(mailto|tel|sms|javascript|data|blob|ftp|file|about|chrome|intent):/i;
319073
+ function sameAppOrigin(a, b) {
319074
+ if (!a || !b)
319075
+ return false;
319076
+ if (a === b)
319077
+ return true;
319078
+ try {
319079
+ const ua = new URL(a);
319080
+ const ub = new URL(b);
319081
+ const host = (u) => u.host.replace(/^www\./i, "");
319082
+ return ua.protocol === ub.protocol && host(ua) === host(ub);
319083
+ } catch {
319084
+ return false;
319085
+ }
319086
+ }
318889
319087
  var GENERATOR_QUERY_KEYS = /* @__PURE__ */ new Set([
318890
319088
  "page",
318891
319089
  "p",
@@ -318954,9 +319152,15 @@ var require_survey_link_frontier = __commonJS({
318954
319152
  return (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2).split("/").filter(Boolean).length;
318955
319153
  }
318956
319154
  function ingestSnapshotLinks(state2, params) {
318957
- const { rawSnapshot, scope, isEntryOrHub, isErrorContext, pageUrl, log: log2 } = params;
319155
+ const { rawSnapshot, ...rest } = params;
319156
+ ingestSeenLinks(state2, { ...rest, links: extractSnapshotLinks(rawSnapshot), provenance: "snapshot" });
319157
+ }
319158
+ var SESSION_KILLER_PATH_RE = /(^|\/)(log[-_]?out|sign[-_]?out|sign[-_]?off|sessions?\/(destroy|delete))([/?#.]|$)/i;
319159
+ function ingestSeenLinks(state2, params) {
319160
+ const { links, scope, isEntryOrHub, isErrorContext, pageUrl, log: log2, provenance = "snapshot" } = params;
318958
319161
  if (isErrorContext)
318959
319162
  return;
319163
+ const originsMatch = (a, b) => provenance === "dom" ? sameAppOrigin(a, b) : a === b;
318960
319164
  let pageBase = null;
318961
319165
  if (pageUrl) {
318962
319166
  try {
@@ -318965,11 +319169,11 @@ var require_survey_link_frontier = __commonJS({
318965
319169
  pageBase = null;
318966
319170
  }
318967
319171
  }
318968
- if (pageBase && scope.origin && pageBase.origin !== scope.origin) {
319172
+ if (pageBase && scope.origin && !originsMatch(pageBase.origin, scope.origin)) {
318969
319173
  log2?.(` [link-frontier] skipped off-origin page ${pageBase.origin}`);
318970
319174
  return;
318971
319175
  }
318972
- const seen = extractSnapshotLinks(rawSnapshot);
319176
+ const seen = links;
318973
319177
  if (seen.length === 0)
318974
319178
  return;
318975
319179
  const suppressed = [];
@@ -319015,14 +319219,14 @@ var require_survey_link_frontier = __commonJS({
319015
319219
  suppressed.push(`${raw} (unparseable)`);
319016
319220
  continue;
319017
319221
  }
319018
- if (scope.origin && abs.origin !== scope.origin) {
319222
+ if (scope.origin && !originsMatch(abs.origin, scope.origin)) {
319019
319223
  suppressed.push(`${raw} (external)`);
319020
319224
  continue;
319021
319225
  }
319022
319226
  if (abs.hash && !abs.hash.startsWith("#/"))
319023
319227
  abs.hash = "";
319024
319228
  const route2 = (0, exploration_parser_js_1.canonicalizeRoute)(abs.pathname + abs.search + abs.hash);
319025
- candidates.push({ route: route2, label: link.label });
319229
+ candidates.push({ route: route2, label: link.label, hidden: link.hidden, inNav: link.inNav, inFooter: link.inFooter });
319026
319230
  }
319027
319231
  candidates.sort((a, b) => {
319028
319232
  const aVariant = a.route === (0, exploration_parser_js_1.normalizeRouteForFocusScope)(a.route) ? 0 : 1;
@@ -319036,6 +319240,10 @@ var require_survey_link_frontier = __commonJS({
319036
319240
  if (stripped)
319037
319241
  suppressed.push(`${candidate.route} (generator-stripped -> ${route2})`);
319038
319242
  const scopePath = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2);
319243
+ if (provenance === "dom" && SESSION_KILLER_PATH_RE.test(scopePath)) {
319244
+ suppressed.push(`${route2} (session-killer)`);
319245
+ continue;
319246
+ }
319039
319247
  if (scope.basePath !== "/" && !(0, exploration_parser_js_1.routeIsWithinFocusAreas)(route2, [scope.basePath])) {
319040
319248
  suppressed.push(`${route2} (out-of-scope)`);
319041
319249
  continue;
@@ -319044,9 +319252,10 @@ var require_survey_link_frontier = __commonJS({
319044
319252
  suppressed.push(`${route2} (root)`);
319045
319253
  continue;
319046
319254
  }
319255
+ const doorLike = provenance === "dom" && candidate.inNav === true && candidate.hidden === true;
319047
319256
  const existing = state2.frontier.get(route2);
319048
319257
  if (existing) {
319049
- const upgradeCandidate = isEntryOrHub && existing.source === "link-seen" && !existing.protected;
319258
+ const upgradeCandidate = (isEntryOrHub || doorLike) && existing.source === "link-seen" && !existing.protected;
319050
319259
  if (!upgradeCandidate)
319051
319260
  continue;
319052
319261
  }
@@ -319068,9 +319277,17 @@ var require_survey_link_frontier = __commonJS({
319068
319277
  familiesInBatch.add(familyKey);
319069
319278
  seenScopedPaths.add(scopePath);
319070
319279
  const relPath = scope.basePath === "/" ? scopePath : scopePath.slice(scope.basePath.length) || "/";
319071
- const isFooterLegal = FOOTER_LEGAL_PATH_RE.test(relPath);
319072
- const isProtected = isEntryOrHub && !isTemplate && !isFooterLegal;
319073
- entries.push({ route: route2, label, protected: isProtected, familyKey });
319280
+ const isFooterLegal = FOOTER_LEGAL_PATH_RE.test(relPath) || provenance === "dom" && candidate.inFooter === true;
319281
+ const isProtected = (isEntryOrHub || doorLike) && !isTemplate && !isFooterLegal;
319282
+ entries.push({
319283
+ route: route2,
319284
+ label,
319285
+ protected: isProtected,
319286
+ familyKey,
319287
+ // Snapshot entries stay provenance-free — the legacy state shape must be
319288
+ // byte-identical when the DOM-harvest flag is off (flag-off invariance).
319289
+ provenance: provenance === "dom" ? candidate.hidden ? "dom-hidden" : "dom" : void 0
319290
+ });
319074
319291
  }
319075
319292
  if (suppressed.length > 0 && log2) {
319076
319293
  log2(` [link-frontier] suppressed ${suppressed.length}: ${suppressed.slice(0, 8).join(", ")}${suppressed.length > 8 ? ` \u2026+${suppressed.length - 8}` : ""}`);
@@ -319082,7 +319299,10 @@ var require_survey_link_frontier = __commonJS({
319082
319299
  (0, exploration_state_js_1.reduceEvent)(state2, { type: "LINKS_DISCOVERED", entries });
319083
319300
  const admitted = countLinkSeen(state2) - before;
319084
319301
  if (admitted > 0 && log2) {
319085
- log2(` [link-frontier] +${admitted} link-seen route(s) (${countLinkSeen(state2)}/${exploration_state_js_1.LINK_SEEN_FRONTIER_CAP}): ${entries.slice(0, 6).map((e) => e.route).join(", ")}${entries.length > 6 ? " \u2026" : ""}`);
319302
+ const tag = provenance === "dom" ? " [dom-harvest]" : "";
319303
+ const hiddenCount = entries.filter((e) => e.provenance === "dom-hidden").length;
319304
+ const hiddenNote = hiddenCount > 0 ? ` (${hiddenCount} hidden-menu door(s))` : "";
319305
+ log2(` [link-frontier]${tag} +${admitted} link-seen route(s)${hiddenNote} (${countLinkSeen(state2)}/${exploration_state_js_1.LINK_SEEN_FRONTIER_CAP}): ${entries.slice(0, 6).map((e) => e.route).join(", ")}${entries.length > 6 ? " \u2026" : ""}`);
319086
319306
  }
319087
319307
  const deniedProtected = entries.filter((e) => e.protected && state2.frontier.get(e.route) && !state2.frontier.get(e.route)?.protected);
319088
319308
  if (deniedProtected.length > 0 && log2) {
@@ -319209,10 +319429,10 @@ var require_exploration_prompt_builder = __commonJS({
319209
319429
  sections.push(`Mode: ${config.mode} | Budget remaining: ${config.maxIterations - state2.iteration} iterations`);
319210
319430
  const budgetPct = state2.iteration / config.maxIterations;
319211
319431
  const budgetBlocking = config.linkFrontierEnabled ? (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2) : [];
319212
- if (budgetPct >= 0.95) {
319432
+ if (!config.adaptiveBudget && budgetPct >= 0.95) {
319213
319433
  sections.push("");
319214
319434
  sections.push(budgetBlocking.length > 0 ? `**CRITICAL: FINAL ITERATIONS. Unvisited discovered routes remain: ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(budgetBlocking, 3)}. Visit ONLY these (capture_page each), then call finish_exploration. Nothing else.**` : "**CRITICAL: FINAL ITERATIONS. Call finish_exploration NOW with a summary of what you found. Do NOT start any new exploration.**");
319215
- } else if (budgetPct >= 0.9) {
319435
+ } else if (!config.adaptiveBudget && budgetPct >= 0.9) {
319216
319436
  sections.push("");
319217
319437
  sections.push(budgetBlocking.length > 0 ? `**CRITICAL: You have used 90%+ of your iteration budget. Unvisited discovered routes remain: ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(budgetBlocking, 4)}. Visit ONLY these (capture_page each), then call finish_exploration.**` : "**CRITICAL: You have used 90%+ of your iteration budget. Wrap up immediately and call finish_exploration.**");
319218
319438
  }
@@ -319598,9 +319818,9 @@ var require_exploration_prompt_builder = __commonJS({
319598
319818
  sections.push("## Control Brief");
319599
319819
  sections.push(`Current route: ${state2.currentRoute} | Iteration ${state2.iteration}/${config.maxIterations} | Budget remaining: ${config.maxIterations - state2.iteration}`);
319600
319820
  const budgetPct = state2.iteration / config.maxIterations;
319601
- if (budgetPct >= 0.95) {
319821
+ if (!config.adaptiveBudget && budgetPct >= 0.95) {
319602
319822
  sections.push("**CRITICAL: FINAL ITERATIONS. Call finish_exploration NOW.**");
319603
- } else if (budgetPct >= 0.9) {
319823
+ } else if (!config.adaptiveBudget && budgetPct >= 0.9) {
319604
319824
  sections.push("**CRITICAL: 90%+ budget used. Wrap up and call finish_exploration.**");
319605
319825
  }
319606
319826
  if (state2.routeState.size > 0) {
@@ -321568,6 +321788,332 @@ var require_evidence_harvester = __commonJS({
321568
321788
  }
321569
321789
  });
321570
321790
 
321791
+ // ../runner-core/dist/services/survey-door-harvester.js
321792
+ var require_survey_door_harvester = __commonJS({
321793
+ "../runner-core/dist/services/survey-door-harvester.js"(exports2) {
321794
+ "use strict";
321795
+ Object.defineProperty(exports2, "__esModule", { value: true });
321796
+ exports2.PAGE_URL_PROBE_FN = exports2.MENU_EXPAND_COLLECT_FN = exports2.DOM_LINK_COLLECT_FN = void 0;
321797
+ exports2.isDomHarvestEnabled = isDomHarvestEnabled;
321798
+ exports2.isMenuExpandEnabled = isMenuExpandEnabled;
321799
+ exports2.parseEvaluateResult = parseEvaluateResult;
321800
+ exports2.sortDoorsFirst = sortDoorsFirst;
321801
+ exports2.harvestDomLinks = harvestDomLinks;
321802
+ exports2.toSeenLinks = toSeenLinks;
321803
+ function isDomHarvestEnabled() {
321804
+ const v = process.env.DISCOVERY_SURVEY_DOM_HARVEST;
321805
+ return v === "true" || v === "1";
321806
+ }
321807
+ function isMenuExpandEnabled() {
321808
+ const v = process.env.DISCOVERY_SURVEY_MENU_EXPAND;
321809
+ return (v === "true" || v === "1") && isDomHarvestEnabled();
321810
+ }
321811
+ var MAX_DOM_LINKS = 150;
321812
+ var MAX_EXPAND_TRIGGERS = 4;
321813
+ var EVALUATE_TIMEOUT_MS = 1e4;
321814
+ exports2.DOM_LINK_COLLECT_FN = `() => {
321815
+ const doors = [];
321816
+ const rest = [];
321817
+ const seen = new Set();
321818
+ const styleHidden = (el) => {
321819
+ try {
321820
+ const s = window.getComputedStyle(el);
321821
+ return s.display === 'none' || s.visibility === 'hidden';
321822
+ } catch { return false; }
321823
+ };
321824
+ const anchors = document.querySelectorAll('a[href], area[href]');
321825
+ for (const a of anchors) {
321826
+ const raw = a.getAttribute('href') || '';
321827
+ if (!raw || raw === '#') continue;
321828
+ if (/^(javascript|mailto|tel|sms|data|blob|about):/i.test(raw)) continue;
321829
+ if (a.hasAttribute('download')) continue;
321830
+ let resolved = '';
321831
+ try { resolved = a.href || ''; } catch {}
321832
+ if (typeof resolved !== 'string') resolved = String(resolved.baseVal || '');
321833
+ const key = raw + '|' + resolved;
321834
+ if (seen.has(key)) continue;
321835
+ seen.add(key);
321836
+ const label = (a.getAttribute('aria-label') || a.textContent || '')
321837
+ .replace(/\\s+/g, ' ').trim().slice(0, 80);
321838
+ let hidden = false;
321839
+ let node = a;
321840
+ while (node && node !== document.documentElement) {
321841
+ if (styleHidden(node)) { hidden = true; break; }
321842
+ node = node.parentElement;
321843
+ }
321844
+ // Footer context WINS over nav: <footer><nav> link farms are boilerplate,
321845
+ // not doors (sol implementation-review P1).
321846
+ const inFooter = !!a.closest('footer,[role="contentinfo"]');
321847
+ const inNav = !inFooter && !!a.closest('nav,[role="navigation"],[role="menubar"],[role="menu"],header,[role="banner"]');
321848
+ const entry = { href: raw.slice(0, 500), resolved: resolved.slice(0, 500), label, hidden, inNav, inFooter };
321849
+ if (inNav || hidden) doors.push(entry); else rest.push(entry);
321850
+ if (doors.length >= ${MAX_DOM_LINKS}) break;
321851
+ }
321852
+ const links = doors.concat(rest).slice(0, ${MAX_DOM_LINKS});
321853
+ return { pageUrl: location.href, links };
321854
+ }`;
321855
+ exports2.MENU_EXPAND_COLLECT_FN = `async () => {
321856
+ const startUrl = location.href;
321857
+ // Baseline pass records KEYS ONLY (no output budget consumed) \u2014 a page with
321858
+ // 150+ static anchors must not crowd freshly-mounted menu links out of the
321859
+ // result. Only post-click NEW anchors are returned.
321860
+ const seen = new Set();
321861
+ const out = [];
321862
+ const keyOf = (a) => {
321863
+ const raw = a.getAttribute('href') || '';
321864
+ if (!raw || raw === '#') return null;
321865
+ if (/^(javascript|mailto|tel|sms|data|blob|about):/i.test(raw)) return null;
321866
+ if (a.hasAttribute('download')) return null;
321867
+ let resolved = '';
321868
+ try { resolved = a.href || ''; } catch {}
321869
+ if (typeof resolved !== 'string') resolved = String(resolved.baseVal || '');
321870
+ return { raw, resolved, key: raw + '|' + resolved };
321871
+ };
321872
+ for (const a of document.querySelectorAll('a[href], area[href]')) {
321873
+ const k = keyOf(a);
321874
+ if (k) seen.add(k.key);
321875
+ }
321876
+ const collectNew = () => {
321877
+ for (const a of document.querySelectorAll('a[href], area[href]')) {
321878
+ if (out.length >= ${MAX_DOM_LINKS}) return;
321879
+ const k = keyOf(a);
321880
+ if (!k || seen.has(k.key)) continue;
321881
+ seen.add(k.key);
321882
+ const label = (a.getAttribute('aria-label') || a.textContent || '')
321883
+ .replace(/\\s+/g, ' ').trim().slice(0, 80);
321884
+ // hidden:true \u2014 mounted-on-demand links are absent from a resting
321885
+ // accessibility snapshot, which is exactly what "hidden door" means.
321886
+ out.push({ href: k.raw.slice(0, 500), resolved: k.resolved.slice(0, 500), label, hidden: true, inNav: true, inFooter: false });
321887
+ }
321888
+ };
321889
+ const chrome = 'nav,[role="navigation"],[role="menubar"],header,[role="banner"]';
321890
+ const destructive = /(delete|remove|archive|reset|disconnect|deactivate|destroy|purge|log\\s*out|sign\\s*out)/i;
321891
+ const styleVisible = (el) => {
321892
+ let node = el;
321893
+ while (node && node !== document.documentElement) {
321894
+ try {
321895
+ const s = window.getComputedStyle(node);
321896
+ if (s.display === 'none' || s.visibility === 'hidden') return false;
321897
+ } catch { return false; }
321898
+ node = node.parentElement;
321899
+ }
321900
+ return true;
321901
+ };
321902
+ const candidates = [];
321903
+ for (const el of document.querySelectorAll('button[aria-haspopup], button[aria-expanded="false"], [role="button"][aria-haspopup], [role="button"][aria-expanded="false"]')) {
321904
+ if (candidates.length >= ${MAX_EXPAND_TRIGGERS}) break;
321905
+ if (el.tagName === 'A' || el.closest('a[href]')) continue;
321906
+ const haspopup = el.getAttribute('aria-haspopup');
321907
+ const expanded = el.getAttribute('aria-expanded');
321908
+ if (haspopup === 'false') continue; // explicit no-popup
321909
+ if (expanded === 'true') continue; // already open \u2014 not ours to toggle
321910
+ if (el.form || el.closest('form')) continue; // form-associated (incl. form="" attr)
321911
+ const type = (el.getAttribute('type') || '').toLowerCase();
321912
+ if (type === 'submit' || type === 'reset') continue;
321913
+ if (!el.closest(chrome)) continue;
321914
+ if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true') continue;
321915
+ if (!styleVisible(el)) continue; // hidden triggers keep bogus geometry
321916
+ const name = (el.getAttribute('aria-label') || el.textContent || '').replace(/\\s+/g, ' ').trim();
321917
+ if (destructive.test(name)) continue;
321918
+ const rect = el.getBoundingClientRect();
321919
+ if (rect.width === 0 && rect.height === 0) continue;
321920
+ candidates.push(el);
321921
+ }
321922
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
321923
+ let expanded = 0;
321924
+ for (const el of candidates) {
321925
+ let opened = false;
321926
+ try {
321927
+ try { el.click(); opened = true; } catch { continue; }
321928
+ expanded++;
321929
+ await sleep(350);
321930
+ if (location.href !== startUrl) {
321931
+ return { pageUrl: location.href, links: out, expandedTriggers: expanded, navigated: true, originalUrl: startUrl };
321932
+ }
321933
+ collectNew();
321934
+ } finally {
321935
+ // Restore the exact trigger we opened: aria-expanded still "true" needs a
321936
+ // closing click; an attribute-less toggle gets clicked back once too.
321937
+ if (opened) {
321938
+ try {
321939
+ const nowExpanded = el.getAttribute('aria-expanded');
321940
+ if (nowExpanded !== 'false') el.click();
321941
+ } catch {}
321942
+ }
321943
+ }
321944
+ await sleep(100);
321945
+ if (location.href !== startUrl) {
321946
+ return { pageUrl: location.href, links: out, expandedTriggers: expanded, navigated: true, originalUrl: startUrl };
321947
+ }
321948
+ }
321949
+ return { pageUrl: location.href, links: out, expandedTriggers: expanded, navigated: false, originalUrl: startUrl };
321950
+ }`;
321951
+ exports2.PAGE_URL_PROBE_FN = `() => location.href`;
321952
+ function parseEvaluateResult(mcpResult) {
321953
+ if (!mcpResult || typeof mcpResult !== "object")
321954
+ return null;
321955
+ if (mcpResult.isError === true)
321956
+ return null;
321957
+ const content = mcpResult.content;
321958
+ if (!Array.isArray(content))
321959
+ return null;
321960
+ for (const item of content) {
321961
+ if (!item || typeof item !== "object")
321962
+ continue;
321963
+ const { type, text } = item;
321964
+ if (type !== "text" || typeof text !== "string" || !text.trim())
321965
+ continue;
321966
+ let payload = null;
321967
+ const section = /(?:^|\n)### Result\s*\n([\s\S]*?)(?=\n### |$)/.exec(text);
321968
+ if (section) {
321969
+ payload = section[1];
321970
+ } else if (!text.trimStart().startsWith("###")) {
321971
+ payload = text;
321972
+ }
321973
+ if (!payload)
321974
+ continue;
321975
+ const cleaned = payload.replace(/^\s*```(json)?\s*\n?/, "").replace(/\n?```\s*$/, "").trim();
321976
+ if (!cleaned)
321977
+ continue;
321978
+ try {
321979
+ return JSON.parse(cleaned);
321980
+ } catch {
321981
+ continue;
321982
+ }
321983
+ }
321984
+ return null;
321985
+ }
321986
+ function normalizeLinks(value) {
321987
+ if (!value || typeof value !== "object")
321988
+ return [];
321989
+ const links = value.links;
321990
+ if (!Array.isArray(links))
321991
+ return [];
321992
+ const out = [];
321993
+ for (const raw of links) {
321994
+ if (!raw || typeof raw !== "object")
321995
+ continue;
321996
+ const l = raw;
321997
+ if (typeof l.href !== "string" || !l.href)
321998
+ continue;
321999
+ out.push({
322000
+ href: l.href,
322001
+ resolved: typeof l.resolved === "string" ? l.resolved : "",
322002
+ label: typeof l.label === "string" && l.label ? l.label : void 0,
322003
+ hidden: l.hidden === true,
322004
+ inNav: l.inNav === true,
322005
+ inFooter: l.inFooter === true
322006
+ });
322007
+ }
322008
+ return out;
322009
+ }
322010
+ function sortDoorsFirst(links) {
322011
+ const tier = (l) => {
322012
+ if (l.inNav && l.hidden)
322013
+ return 0;
322014
+ if (l.inNav)
322015
+ return 1;
322016
+ if (!l.inFooter)
322017
+ return 2;
322018
+ return 3;
322019
+ };
322020
+ return [...links].sort((a, b) => tier(a) - tier(b));
322021
+ }
322022
+ async function callWithTimeout(client, fn, timeoutMs) {
322023
+ let timer;
322024
+ const timeout = new Promise((_, reject) => {
322025
+ timer = setTimeout(() => reject(new Error(`browser_evaluate timed out after ${timeoutMs}ms`)), timeoutMs);
322026
+ });
322027
+ try {
322028
+ return await Promise.race([
322029
+ client.callTool({ name: "browser_evaluate", arguments: { function: fn } }, void 0, { timeout: timeoutMs }),
322030
+ timeout
322031
+ ]);
322032
+ } finally {
322033
+ if (timer)
322034
+ clearTimeout(timer);
322035
+ }
322036
+ }
322037
+ async function harvestDomLinks(client, options = {}) {
322038
+ const { expandMenus = false, log: log2 } = options;
322039
+ const empty = { pageUrl: null, links: [], expandedTriggers: 0, navigated: false, originalUrl: null };
322040
+ let staticResult = empty;
322041
+ try {
322042
+ const parsed = parseEvaluateResult(await callWithTimeout(client, exports2.DOM_LINK_COLLECT_FN, EVALUATE_TIMEOUT_MS));
322043
+ const links = normalizeLinks(parsed);
322044
+ const pageUrl = parsed && typeof parsed === "object" && typeof parsed.pageUrl === "string" ? parsed.pageUrl : null;
322045
+ staticResult = { pageUrl, links, expandedTriggers: 0, navigated: false, originalUrl: pageUrl };
322046
+ } catch (err) {
322047
+ log2?.(` [door-harvest] dom read skipped \u2014 ${err instanceof Error ? err.message : String(err)}`);
322048
+ return empty;
322049
+ }
322050
+ if (!expandMenus) {
322051
+ return { ...staticResult, links: sortDoorsFirst(staticResult.links) };
322052
+ }
322053
+ if (!staticResult.pageUrl) {
322054
+ log2?.(" [door-harvest] skipping menu expansion \u2014 no verifiable page URL from the static pass");
322055
+ return { ...staticResult, links: sortDoorsFirst(staticResult.links) };
322056
+ }
322057
+ let extra = [];
322058
+ let expandedTriggers = 0;
322059
+ let expansionFailed = false;
322060
+ try {
322061
+ const parsed = parseEvaluateResult(await callWithTimeout(client, exports2.MENU_EXPAND_COLLECT_FN, EVALUATE_TIMEOUT_MS + 4e3));
322062
+ if (parsed === null) {
322063
+ expansionFailed = true;
322064
+ } else {
322065
+ extra = normalizeLinks(parsed);
322066
+ const obj = typeof parsed === "object" ? parsed : {};
322067
+ expandedTriggers = typeof obj.expandedTriggers === "number" ? obj.expandedTriggers : 0;
322068
+ }
322069
+ } catch (err) {
322070
+ expansionFailed = true;
322071
+ log2?.(` [door-harvest] menu expansion skipped \u2014 ${err instanceof Error ? err.message : String(err)}`);
322072
+ }
322073
+ let navigated = false;
322074
+ try {
322075
+ const probe = parseEvaluateResult(await callWithTimeout(client, exports2.PAGE_URL_PROBE_FN, 5e3));
322076
+ if (typeof probe === "string" && probe) {
322077
+ navigated = probe !== staticResult.pageUrl;
322078
+ } else {
322079
+ navigated = expansionFailed;
322080
+ }
322081
+ } catch {
322082
+ navigated = expansionFailed;
322083
+ }
322084
+ const known = new Set(staticResult.links.map((l) => l.href));
322085
+ const merged = [...staticResult.links];
322086
+ for (const link of extra) {
322087
+ if (!known.has(link.href)) {
322088
+ known.add(link.href);
322089
+ merged.push(link);
322090
+ }
322091
+ }
322092
+ if (expandedTriggers > 0) {
322093
+ log2?.(` [door-harvest] expanded ${expandedTriggers} menu trigger(s), +${merged.length - staticResult.links.length} link(s)${navigated ? " \u2014 PAGE NAVIGATED, caller must restore" : ""}`);
322094
+ } else if (navigated) {
322095
+ log2?.(" [door-harvest] page moved during expansion attempt \u2014 caller must restore");
322096
+ }
322097
+ return {
322098
+ pageUrl: staticResult.pageUrl,
322099
+ links: sortDoorsFirst(merged),
322100
+ expandedTriggers,
322101
+ navigated,
322102
+ originalUrl: staticResult.pageUrl
322103
+ };
322104
+ }
322105
+ function toSeenLinks(links) {
322106
+ return links.map((l) => ({
322107
+ url: l.resolved || l.href,
322108
+ label: l.label,
322109
+ hidden: l.hidden,
322110
+ inNav: l.inNav,
322111
+ inFooter: l.inFooter
322112
+ }));
322113
+ }
322114
+ }
322115
+ });
322116
+
321571
322117
  // ../runner-core/dist/services/survey-scope.js
321572
322118
  var require_survey_scope = __commonJS({
321573
322119
  "../runner-core/dist/services/survey-scope.js"(exports2) {
@@ -321575,7 +322121,9 @@ var require_survey_scope = __commonJS({
321575
322121
  Object.defineProperty(exports2, "__esModule", { value: true });
321576
322122
  exports2.deriveSurveyPathScope = deriveSurveyPathScope;
321577
322123
  exports2.isAuthFlowRoute = isAuthFlowRoute;
322124
+ exports2.hasAuthFlowSegment = hasAuthFlowSegment;
321578
322125
  exports2.buildSurveyScopeBoundary = buildSurveyScopeBoundary;
322126
+ exports2.routeWithinSurveyBasePath = routeWithinSurveyBasePath;
321579
322127
  exports2.filterFeatureGroupsToPathScope = filterFeatureGroupsToPathScope;
321580
322128
  var exploration_parser_js_1 = require_exploration_parser();
321581
322129
  function deriveSurveyPathScope(baseUrl) {
@@ -321603,22 +322151,35 @@ var require_survey_scope = __commonJS({
321603
322151
  "callback",
321604
322152
  "login",
321605
322153
  "log-in",
322154
+ "log_in",
321606
322155
  "signin",
321607
322156
  "sign-in",
322157
+ "sign_in",
321608
322158
  "signup",
321609
322159
  "sign-up",
322160
+ "sign_up",
321610
322161
  "register",
321611
322162
  "logout",
321612
322163
  "log-out",
322164
+ "log_out",
321613
322165
  "signout",
321614
322166
  "sign-out",
322167
+ "sign_out",
321615
322168
  "sso",
321616
322169
  "saml",
321617
322170
  "oidc"
321618
322171
  ]);
322172
+ var AUTH_FLOW_PARENT_SEGMENTS = /* @__PURE__ */ new Set(["api", "auth", "users", "user", "account", "accounts", "identity", "id"]);
321619
322173
  function isAuthFlowRoute(route2) {
321620
- const normalized = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2).toLowerCase();
321621
- return normalized.split(/[/#]/).some((segment) => AUTH_FLOW_SEGMENTS.has(segment));
322174
+ const segments = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2).toLowerCase().split(/[/#]/).filter(Boolean);
322175
+ if (segments.length === 0)
322176
+ return false;
322177
+ if (AUTH_FLOW_SEGMENTS.has(segments[0]))
322178
+ return true;
322179
+ return segments.length > 1 && AUTH_FLOW_PARENT_SEGMENTS.has(segments[0]) && AUTH_FLOW_SEGMENTS.has(segments[1]);
322180
+ }
322181
+ function hasAuthFlowSegment(route2) {
322182
+ return (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2).toLowerCase().split(/[/#]/).filter(Boolean).some((segment) => AUTH_FLOW_SEGMENTS.has(segment));
321622
322183
  }
321623
322184
  function routePathForScope(route2, origin) {
321624
322185
  const trimmed = (route2 ?? "").trim();
@@ -321642,7 +322203,7 @@ var require_survey_scope = __commonJS({
321642
322203
  return trimmed;
321643
322204
  }
321644
322205
  }
321645
- if (trimmed.startsWith("/") && trimmed.includes("..")) {
322206
+ if (trimmed.startsWith("/") && (trimmed.includes("..") || /%2e/i.test(trimmed))) {
321646
322207
  try {
321647
322208
  const resolved = new URL(trimmed, "https://scope.invalid");
321648
322209
  return `${resolved.pathname}${resolved.search}${resolved.hash}`;
@@ -321662,20 +322223,21 @@ var require_survey_scope = __commonJS({
321662
322223
  }
321663
322224
  return lines.join("\n");
321664
322225
  }
322226
+ function routeWithinSurveyBasePath(route2, scope) {
322227
+ const path = routePathForScope(route2, scope.origin);
322228
+ if (path === null)
322229
+ return false;
322230
+ if ((0, exploration_parser_js_1.routeIsWithinFocusAreas)(path, [scope.basePath]))
322231
+ return true;
322232
+ const norm = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(path);
322233
+ return norm === `${scope.basePath}#` || norm.startsWith(`${scope.basePath}#/`);
322234
+ }
321665
322235
  function filterFeatureGroupsToPathScope(featureGroups, baseUrl) {
321666
322236
  const scope = deriveSurveyPathScope(baseUrl);
321667
322237
  if (!scope) {
321668
- return { kept: featureGroups, droppedGroups: [], trimmedRoutes: [], trimmedPages: [], basePath: null };
322238
+ return { kept: featureGroups, droppedGroups: [], trimmedRoutes: [], trimmedPages: [], basePath: null, failedOpen: false };
321669
322239
  }
321670
- const withinBase = (route2) => {
321671
- const path = routePathForScope(route2, scope.origin);
321672
- if (path === null)
321673
- return false;
321674
- if ((0, exploration_parser_js_1.routeIsWithinFocusAreas)(path, [scope.basePath]))
321675
- return true;
321676
- const norm = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(path);
321677
- return norm === `${scope.basePath}#` || norm.startsWith(`${scope.basePath}#/`);
321678
- };
322240
+ const withinBase = (route2) => routeWithinSurveyBasePath(route2, scope);
321679
322241
  const allowedAuthFlow = (route2) => {
321680
322242
  const path = routePathForScope(route2, scope.origin);
321681
322243
  return path !== null && isAuthFlowRoute(path);
@@ -321725,7 +322287,23 @@ var require_survey_scope = __commonJS({
321725
322287
  trimmedPages.push({ name: group.name, routes: removedPages });
321726
322288
  }
321727
322289
  }
321728
- return { kept, droppedGroups, trimmedRoutes, trimmedPages, basePath: scope.basePath };
322290
+ if (kept.length === 0 && droppedGroups.length > 0) {
322291
+ const redirectSignature = featureGroups.some((group) => (Array.isArray(group.routes) ? group.routes : []).some((route2) => {
322292
+ const path = routePathForScope(route2, scope.origin);
322293
+ return path !== null && !isAuthFlowRoute(path);
322294
+ }));
322295
+ if (redirectSignature) {
322296
+ return {
322297
+ kept: featureGroups,
322298
+ droppedGroups: [],
322299
+ trimmedRoutes: [],
322300
+ trimmedPages: [],
322301
+ basePath: scope.basePath,
322302
+ failedOpen: true
322303
+ };
322304
+ }
322305
+ }
322306
+ return { kept, droppedGroups, trimmedRoutes, trimmedPages, basePath: scope.basePath, failedOpen: false };
321729
322307
  }
321730
322308
  }
321731
322309
  });
@@ -321746,8 +322324,10 @@ var require_discover_explorer = __commonJS({
321746
322324
  exports2.shouldSurfaceSurveyModalTrigger = shouldSurfaceSurveyModalTrigger;
321747
322325
  exports2.normalizeAssistantToolCallArgs = normalizeAssistantToolCallArgs;
321748
322326
  exports2.getMaxIterations = getMaxIterations;
322327
+ exports2.computeBudgetDecision = computeBudgetDecision;
321749
322328
  exports2.updatePendingRouteScreenshot = updatePendingRouteScreenshot;
321750
322329
  exports2.consumePendingRouteScreenshot = consumePendingRouteScreenshot;
322330
+ exports2.buildSystemPrompt = buildSystemPrompt;
321751
322331
  exports2.runExplorePhase = runExplorePhase;
321752
322332
  var crypto_1 = require("crypto");
321753
322333
  var ai_provider_js_1 = require_ai_provider();
@@ -321771,6 +322351,7 @@ var require_discover_explorer = __commonJS({
321771
322351
  var evidence_harvester_js_1 = require_evidence_harvester();
321772
322352
  var exploration_parser_js_1 = require_exploration_parser();
321773
322353
  var survey_link_frontier_js_1 = require_survey_link_frontier();
322354
+ var survey_door_harvester_js_1 = require_survey_door_harvester();
321774
322355
  var survey_scope_js_1 = require_survey_scope();
321775
322356
  var snapshot_context_compactor_js_2 = require_snapshot_context_compactor();
321776
322357
  Object.defineProperty(exports2, "formatMCPToolResult", { enumerable: true, get: function() {
@@ -321924,6 +322505,46 @@ var require_discover_explorer = __commonJS({
321924
322505
  }
321925
322506
  return readPositiveIntEnv2(env, "DISCOVERY_EXPLORE_MAX_ITERATIONS_DEFAULT", DEFAULT_MAX_EXPLORE_ITERATIONS_DEFAULT);
321926
322507
  }
322508
+ var DEFAULT_ADAPTIVE_BUDGET_HARD_CAP = 200;
322509
+ var ADAPTIVE_BUDGET_FINALIZE_RESERVE = 5;
322510
+ var ADAPTIVE_BUDGET_ITERS_PER_ROUTE = 4;
322511
+ var ADAPTIVE_BUDGET_MIN_LEASE = 10;
322512
+ var ADAPTIVE_BUDGET_MAX_LEASE = 30;
322513
+ var ADAPTIVE_BUDGET_DISCOVERY_LEASE = 20;
322514
+ var ADAPTIVE_BUDGET_RESCUE_WINDOW = 10;
322515
+ var ADAPTIVE_BUDGET_MIN_GAP = 10;
322516
+ var ADAPTIVE_BUDGET_DECISION_PCT = 0.9;
322517
+ var ADAPTIVE_BUDGET_PROGRESS_WINDOW = 15;
322518
+ function computeBudgetDecision(input) {
322519
+ const { iteration, currentCap, hardCap, remainingRoutes, capturing, freshDiscovery, discoveryLeaseUsed, rescueUsed, allowExtension } = input;
322520
+ if (remainingRoutes <= 0)
322521
+ return { action: "finish", reason: "drained" };
322522
+ if (iteration >= hardCap)
322523
+ return { action: "finish", reason: "hard-cap" };
322524
+ const tail = Math.max(0, currentCap - iteration);
322525
+ const needed = ADAPTIVE_BUDGET_FINALIZE_RESERVE + ADAPTIVE_BUDGET_ITERS_PER_ROUTE * remainingRoutes;
322526
+ if (allowExtension && capturing) {
322527
+ if (currentCap >= hardCap)
322528
+ return { action: "none" };
322529
+ const shortfall = needed - tail;
322530
+ if (shortfall <= 0)
322531
+ return { action: "none" };
322532
+ const clamped = Math.min(Math.max(shortfall, ADAPTIVE_BUDGET_MIN_LEASE), ADAPTIVE_BUDGET_MAX_LEASE);
322533
+ const delta = Math.min(hardCap - currentCap, clamped);
322534
+ return { action: "extend", delta, newCap: currentCap + delta, lease: "capture" };
322535
+ }
322536
+ if (allowExtension && freshDiscovery && !discoveryLeaseUsed) {
322537
+ if (currentCap >= hardCap)
322538
+ return { action: "none" };
322539
+ if (tail >= needed)
322540
+ return { action: "none" };
322541
+ const delta = Math.min(hardCap - currentCap, ADAPTIVE_BUDGET_DISCOVERY_LEASE);
322542
+ return { action: "extend", delta, newCap: currentCap + delta, lease: "discovery" };
322543
+ }
322544
+ if (!rescueUsed)
322545
+ return { action: "rescue" };
322546
+ return { action: "finish", reason: "stalled" };
322547
+ }
321927
322548
  function updatePendingRouteScreenshot(pending, { imageData, detectedRoute, fallbackRoute }) {
321928
322549
  if (imageData) {
321929
322550
  return {
@@ -322120,8 +322741,9 @@ NEVER assert: user-specific text (names, emails), counts/numbers, timestamps, ma
322120
322741
  };
322121
322742
  function buildSystemPrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, options) {
322122
322743
  const browserCredentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
322123
- const { secrets: explorerSecretVars, publics: explorerPublicVars } = (0, credential_tools_js_1.partitionTestVariables)(browserCredentials);
322744
+ const { secrets: explorerSecretVars, publics: explorerPublicVarsRaw } = (0, credential_tools_js_1.partitionTestVariables)(browserCredentials);
322124
322745
  const hasCredentials = Object.keys(explorerSecretVars).length > 0;
322746
+ const { DISCOVERY_SHOULD_REGISTER: _omitRegisterFlag, ...explorerPublicVars } = explorerPublicVarsRaw;
322125
322747
  const explorerPublicLines = (0, credential_tools_js_1.renderPublicVariablesBlock)(explorerPublicVars);
322126
322748
  const credentialBlock = `${explorerPublicLines ? `
322127
322749
  ## Test Variables (public)
@@ -322131,6 +322753,7 @@ ${explorerPublicLines}
322131
322753
  const inboxBlock = options?.inboxPrompt ? `
322132
322754
  ${options.inboxPrompt}` : "";
322133
322755
  const isRegisterMode = !!options?.inboxPrompt;
322756
+ const emailVerificationUnavailable = !isRegisterMode && !!options?.emailVerificationUnavailable;
322134
322757
  const focusAreas = options?.focusAreas;
322135
322758
  let alreadyDiscoveredPages = "";
322136
322759
  if (existingSiteMap && existingSiteMap.pages.length > 0) {
@@ -322152,21 +322775,27 @@ ${options.inboxPrompt}` : "";
322152
322775
  const isIncremental = !!(options?.incremental && mode === "survey");
322153
322776
  const MAX_EXPLORE_ITERATIONS = getMaxIterations(mode, options?.incremental);
322154
322777
  const isSurvey = mode === "survey";
322778
+ const adaptiveBudgetOn = isSurvey && !isIncremental && (process.env.DISCOVERY_ADAPTIVE_BUDGET === "1" || (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_ADAPTIVE_BUDGET)) && (0, survey_link_frontier_js_1.isSurveyLinkFrontierEnabled)();
322779
+ const surveyPathScope = isSurvey ? (0, survey_scope_js_1.deriveSurveyPathScope)(baseUrl) : null;
322155
322780
  if (isSurvey) {
322156
- return `You are a QA exploration agent. Your goal: MAP an entire web application by visiting every reachable page and cataloging its structure. You are NOT testing - you are building a complete site map so downstream agents know what to test.
322781
+ return `You are a QA exploration agent. Your goal: ${surveyPathScope ? `MAP one flow of a web application in ISOLATION \u2014 the flow under \`${surveyPathScope.basePath}\` \u2014 visiting every reachable page INSIDE that scope and cataloging its structure` : "MAP an entire web application by visiting every reachable page and cataloging its structure"}. You are NOT testing - you are building a ${surveyPathScope ? "site map of this flow" : "complete site map"} so downstream agents know what to test.
322157
322782
 
322158
322783
  ## Strategy
322159
322784
  1. ${isRegisterMode ? `Navigate to the base URL. BEFORE registering, explore the unauthenticated experience first:
322160
322785
  - Find all reachable public auth pages from the logged-out experience: Login, Register/Sign Up, Forgot Password - check menus, drawers, and modal auth switches too
322161
322786
  - For each: browser_snapshot \u2192 capture_page \u2192 record_transition
322162
322787
  - Do NOT submit the password-reset form - only catalog it (survey rule: map, do not mutate)
322163
- - After capturing the public auth pages, REGISTER a new account by following the Account Registration section below, then continue mapping the authenticated app` : hasCredentials ? `Navigate to the base URL. BEFORE logging in, explore the unauthenticated experience first:
322788
+ - After capturing the public auth pages, REGISTER a new account by following the Account Registration section below, then continue mapping the authenticated app` : emailVerificationUnavailable ? `Navigate to the base URL. NOTE: this project asked to REGISTER a new account, but this runner CANNOT receive email (verification inbox unavailable). Registration may still work if the app does not require email verification:
322789
+ - FIRST explore the unauthenticated experience: find all reachable public auth pages (Login, Register/Sign Up, Forgot Password - check menus, drawers, and modal auth switches too) and capture each: browser_snapshot \u2192 capture_page \u2192 record_transition. Do NOT submit the password-reset form
322790
+ - THEN attempt registration ONCE: fill the signup form via \`fill_credential_form\` with the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys (only if no TEST_USER_EMAIL key is listed, use a fresh \`testuser-<timestamp>@example.com\`), submit it ONCE, and never alter the email or create a second account. If signup says the address is already registered, log in with the same keys instead
322791
+ - If you end up signed in WITHOUT an email round-trip, continue mapping the authenticated app normally
322792
+ - THE WALL: if any step demands an emailed code/link ("check your email", OTP, magic link), STOP that flow immediately \u2014 never type a guessed code, never click Resend \u2014 keep mapping the public app, and state in finish_exploration that authenticated coverage is BLOCKED (cannot receive verification email)` : hasCredentials ? `Navigate to the base URL. BEFORE logging in, explore the unauthenticated experience first:
322164
322793
  - Find all reachable public auth pages from the logged-out experience: Login, Register/Sign Up, Forgot Password - check menus, drawers, and modal auth switches too
322165
322794
  - For each: browser_snapshot \u2192 capture_page \u2192 record_transition
322166
322795
  - Do NOT submit registration or password-reset forms - only catalog the page structure (survey rule: map, do not mutate)
322167
322796
  - After capturing all reachable public auth pages, use the credentials below to log in and continue mapping the authenticated app` : "Navigate to the base URL"}
322168
322797
  2. Take a \`browser_snapshot\` on the landing page. **IMPORTANT: NEVER pass a filename parameter to browser_snapshot - always call it with no arguments so the snapshot is returned inline.**
322169
- 3. **BUILD YOUR CHECKLIST**: Before clicking anything, list the navigation links visible in the snapshot - sidebar, header, footer, user menus, hamburger menus. If the snapshot output says some navigation chrome was omitted, immediately call \`capture_page\` on the current page and use its structured nav list to complete the checklist before proceeding. This is your exploration checklist. Visit each one in order
322798
+ 3. **BUILD YOUR CHECKLIST**: Before clicking anything, list the navigation links visible in the snapshot - sidebar, header, footer, user menus, hamburger menus. If the snapshot output says some navigation chrome was omitted, immediately call \`capture_page\` on the current page and use its structured nav list to complete the checklist before proceeding. This is your exploration checklist. Visit each one in order${surveyPathScope ? ` \u2014 but ONLY the links inside the entry scope \`${surveyPathScope.basePath}\`. Links leading outside it (global header/footer, a parent hub, sibling sections) are OUT of scope: leave them off the checklist, do not visit them` : ""}
322170
322799
  - **ALWAYS navigate by CLICKING links in the page**, not by typing URLs into browser_navigate. Clicking captures the exact locator that downstream test generators need. Direct URL navigation loses this information.
322171
322800
  4. Visit each navigation target systematically. On each page:
322172
322801
  - \`browser_snapshot\` to see the page
@@ -322177,8 +322806,8 @@ ${options.inboxPrompt}` : "";
322177
322806
  5. **Tabs and sub-sections**: If a page has tabs, click EACH tab and call \`capture_page\` for each tab state. Use a route suffix like \`/settings?tab=profile\`, \`/settings?tab=billing\`
322178
322807
  6. **Hidden navigation**: Look for dropdown menus, user/avatar menus (click the avatar), hamburger menus, "More..." links, breadcrumbs. Click to OPEN these to discover navigation targets
322179
322808
  7. **List pages**: If a page shows a list of items with links (e.g. /users/1, /users/2), click ONE item to discover the detail page template. Do NOT visit every item - they are the same page pattern
322180
- 8. **Error page**: Navigate to a path that does not exist (e.g. \`/this-page-does-not-exist-404\`) to capture the app's error page
322181
- 9. After visiting ALL reachable pages, call \`finish_exploration\`
322809
+ 8. **Error page**: do NOT probe made-up routes to capture the app's error page \u2014 nothing downstream uses a probed 404. Only catalog an error page if the app itself routes you to one (broken link, failed action, redirect)
322810
+ 9. After visiting ALL reachable ${surveyPathScope ? "in-scope " : ""}pages, call \`finish_exploration\`
322182
322811
  You do NOT have a \`record_journey\` tool - your job is mapping, not testing. Downstream agents will create tests from your site map.
322183
322812
 
322184
322813
  ## What to catalog (capture_page)
@@ -322203,7 +322832,7 @@ ${(0, survey_scope_js_1.buildSurveyScopeBoundary)(baseUrl)}
322203
322832
  - **OAuth/SSO**: If login redirects to an external provider (Google, GitHub), do not follow it. Note that the app uses OAuth and explore unauthenticated pages only
322204
322833
  - **Auth failure**: If login${isRegisterMode ? " or account registration" : ""} fails after 2 attempts, capture the auth page and explore unauthenticated pages. Note the failure in your \`finish_exploration\` summary
322205
322834
  - **Parameterized routes**: /users/1 and /users/2 are the same page template. Visit ONE instance, not all of them
322206
- - Max iterations: ${MAX_EXPLORE_ITERATIONS}. You will be warned at 90%. Use your full budget to discover every page thoroughly
322835
+ - ${adaptiveBudgetOn ? `Initial iteration budget: ${MAX_EXPLORE_ITERATIONS}. If the map is still incomplete as you approach it, the harness may GRANT you more iterations (up to a hard cap) - an explicit "GRANTED: ..." message supersedes this number. Use your budget to discover every page thoroughly` : `Max iterations: ${MAX_EXPLORE_ITERATIONS}. You will be warned at 90%. Use your full budget to discover every page thoroughly`}
322207
322836
 
322208
322837
  ${appBrief ? `## Application Brief
322209
322838
  The user provided this description of their application:
@@ -322225,7 +322854,7 @@ If the user mentioned a specific page that changed (e.g. "Settings was redesigne
322225
322854
  - Check for Login, Register/Sign Up, Forgot Password - including behind menus, drawers, and modal auth switches
322226
322855
  - For each NEW auth page: browser_snapshot \u2192 capture_page \u2192 record_transition
322227
322856
  - Do NOT submit the password-reset form (survey rule: map, do not mutate)
322228
- - After capturing any new public auth pages, REGISTER a new account (see the Account Registration section) and continue` : hasCredentials ? `Navigate to the base URL. BEFORE logging in, explore the unauthenticated experience for any NEW public auth pages not in the known routes above:
322857
+ - After capturing any new public auth pages, REGISTER a new account (see the Account Registration section) and continue` : emailVerificationUnavailable ? `Navigate to the base URL. NOTE: registration was requested for this project but this runner CANNOT receive email (verification inbox unavailable). Attempt registration ONCE with the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys via \`fill_credential_form\` (if signup says the address is already registered, log in with the same keys). If you get signed in without an email round-trip, map NEW authenticated pages normally. If any step demands an emailed code/link, STOP that flow \u2014 never type a guessed code \u2014 map only NEW public pages and note in finish_exploration that authenticated coverage is BLOCKED (cannot receive verification email)` : hasCredentials ? `Navigate to the base URL. BEFORE logging in, explore the unauthenticated experience for any NEW public auth pages not in the known routes above:
322229
322858
  - Check for Login, Register/Sign Up, Forgot Password - including behind menus, drawers, and modal auth switches
322230
322859
  - For each NEW auth page: browser_snapshot \u2192 capture_page \u2192 record_transition
322231
322860
  - Do NOT submit registration or password-reset forms (survey rule: map, do not mutate)
@@ -322243,7 +322872,9 @@ ${surfaceIntelligence ? "## Prior Recordings\n" + surfaceIntelligence + "\n" : "
322243
322872
 
322244
322873
  ## App Details
322245
322874
  - Base URL: ${baseUrl}
322246
- ${credentialBlock}${redactedBlock}${inboxBlock}`;
322875
+ ${credentialBlock}${redactedBlock}${inboxBlock}${emailVerificationUnavailable ? `
322876
+
322877
+ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
322247
322878
  }
322248
322879
  return `You are an expert QA exploration agent. Your job is to USE a web application like a real user - interact with every feature you find, observe what happens, and catalog the results.
322249
322880
 
@@ -322270,7 +322901,7 @@ Every feature you discover should be USED, not just cataloged. The test generato
322270
322901
 
322271
322902
  ## Strategy
322272
322903
  1. Navigate to the BASE URL (the landing/home page). ALWAYS start here - never deep-link directly to sub-pages like /auth/login or /dashboard.
322273
- 2. From the landing page, find and use the app's own navigation to reach features.${isRegisterMode ? " First create your account by following the Account Registration section below, then continue." : hasCredentials ? " Find the login/sign-in link and log in using the credentials below." : ""}
322904
+ 2. From the landing page, find and use the app's own navigation to reach features.${isRegisterMode ? " First create your account by following the Account Registration section below, then continue." : emailVerificationUnavailable ? " NOTE: registration was requested for this project but this runner CANNOT receive email (verification inbox unavailable). First attempt registration ONCE with the `TEST_USER_EMAIL` + `TEST_USER_PASSWORD` credential keys via `fill_credential_form` (never alter the email; if it is already registered, log in with the same keys). If you are signed in without an email round-trip, continue as a normal logged-in user. If any step demands an emailed code/link, STOP that flow \u2014 never type a guessed code \u2014 explore what is publicly reachable and state in finish_exploration that authenticated coverage is BLOCKED (cannot receive verification email)." : hasCredentials ? " Find the login/sign-in link and log in using the credentials below." : ""}
322274
322905
  3. On each page: take a \`browser_snapshot\`, identify what features/actions are available
322275
322906
  4. **USE the features on the page:**
322276
322907
  - Forms: fill every field with realistic data, submit, observe the result (success page, validation errors, redirect, toast messages)
@@ -322288,7 +322919,11 @@ Every feature you discover should be USED, not just cataloged. The test generato
322288
322919
  - Do NOT split validation, invalid input, and recovery into separate tests - combine them into ONE comprehensive test
322289
322920
  c) For multi-section pages (profile with tabs, settings with categories): test each DISTINCT section as a separate happy path test
322290
322921
  Focus on meaningful coverage: every form should have at least a happy path and a validation test. Multi-section pages (tabs, categories) should test each distinct section.
322291
- **Auth pages (login/register) - SPECIAL ORDER: test NEGATIVE before positive:**
322922
+ ${emailVerificationUnavailable ? ` **Auth pages (login/register) - registration requested but this runner CANNOT receive email:**
322923
+ - Step 1: Test negative flows FIRST while not logged in: on the LOGIN form, submit empty fields and invalid credentials (synthetic values like "wrong@example.com" / "badpassword" via \`browser_type\` \u2014 nothing can be created there); on the REGISTRATION form, empty-submit validation ONLY (a filled synthetic signup could CREATE a stray account on a permissive app). Call \`record_journey\` ONCE for the combined validation flow
322924
+ - Step 2: Attempt the happy path ONCE via REGISTRATION: \`fill_credential_form\` with the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys, submit the signup form ONCE. Never alter the email or create a second account; if signup says the address is already registered, log in with the same keys instead
322925
+ - Step 3: If you are signed in with NO email round-trip, call \`record_journey\` for the happy path and continue exploring as a logged-in user
322926
+ - THE WALL: if any step demands an emailed code/link (verification code, OTP, magic link, "check your email"), STOP immediately \u2014 never type a guessed code, never click Resend \u2014 record only the validation journey and note in \`finish_exploration\` that the authenticated happy path is BLOCKED (cannot receive verification email)` : ` **Auth pages (login/register) - SPECIAL ORDER: test NEGATIVE before positive:**
322292
322927
  - You arrive at the login/register page NOT logged in - use this state to test errors FIRST
322293
322928
  - Step 1: Submit with empty fields \u2192 observe validation errors
322294
322929
  - Step 2: Submit with invalid credentials (wrong password, bad email) \u2192 observe error messages
@@ -322296,7 +322931,7 @@ Every feature you discover should be USED, not just cataloged. The test generato
322296
322931
  - Step 4: NOW do the happy path - log in with valid credentials \u2192 observe redirect to dashboard
322297
322932
  - Step 5: Call \`record_journey\` for the happy path
322298
322933
  - This order avoids needing to log out between tests. After the happy path, you are logged in and ready to explore other features
322299
- - **Login filling**: ALWAYS use \`fill_credential_form\` for the happy-path login \u2014 never \`browser_fill_form\` or \`browser_type\` with the actual email/password (those calls will be blocked). Example: \`fill_credential_form({ fields: [{ field: "Email", credentialKey: "TEST_USER_EMAIL" }, { field: "Password", credentialKey: "TEST_USER_PASSWORD" }] })\`. For sequential fills, use \`fill_credential\` per field. Pure validation flows (empty submit, deliberately wrong password) can use \`browser_type\` with synthetic invalid values like "wrong@example.com" / "badpassword" \u2014 those are not real credentials and won't be blocked.
322934
+ - **Login filling**: ALWAYS use \`fill_credential_form\` for the happy-path login \u2014 never \`browser_fill_form\` or \`browser_type\` with the actual email/password (those calls will be blocked). Example: \`fill_credential_form({ fields: [{ field: "Email", credentialKey: "TEST_USER_EMAIL" }, { field: "Password", credentialKey: "TEST_USER_PASSWORD" }] })\`. For sequential fills, use \`fill_credential\` per field. Pure validation flows (empty submit, deliberately wrong password) can use \`browser_type\` with synthetic invalid values like "wrong@example.com" / "badpassword" \u2014 those are not real credentials and won't be blocked.`}
322300
322935
  **Logging out (when needed for other reasons):**
322301
322936
  - To find the logout button: look in the user/avatar menu (click the profile icon or avatar in the header), check sidebar bottom, check settings page
322302
322937
  - Do NOT try navigating to /logout or /auth/logout directly - these URLs rarely exist. Always find and click the actual logout button in the UI
@@ -322347,25 +322982,38 @@ Every feature you discover should be USED, not just cataloged. The test generato
322347
322982
 
322348
322983
  GOOD (specific, with routes, stable assertions):
322349
322984
  \`\`\`
322350
- record_journey({
322985
+ ${emailVerificationUnavailable ? `record_journey({
322986
+ name: "Contact Form - Validation",
322987
+ route: "/contact",
322988
+ routes: ["/", "/contact"],
322989
+ steps: ["Navigate to /", "Click 'Contact' link", "Submit the empty form", "Observe validation errors"],
322990
+ expectedOutcome: "URL matches /contact. Validation error 'Email is required' is visible.",
322991
+ assertions: [{ type: "url", value: "/contact" }, { type: "error-text", value: "Email is required" }]
322992
+ })` : `record_journey({
322351
322993
  name: "Login - Happy Path",
322352
322994
  route: "/auth/login",
322353
322995
  routes: ["/", "/auth/login", "/dashboard"],
322354
322996
  steps: ["Navigate to /", "Click 'Sign In' link", "Fill email and password with test credentials", "Click 'Log In' button", "Observe redirect to /dashboard"],
322355
322997
  expectedOutcome: "URL matches /dashboard. Sidebar link 'Overview' is visible.",
322356
322998
  assertions: [{ type: "url", value: "/dashboard" }, { type: "nav-link", locator: "getByRole('link', { name: 'Overview' })" }]
322357
- })
322999
+ })`}
322358
323000
  \`\`\`
322359
323001
 
322360
323002
  BAD (vague, missing routes, marketing assertions):
322361
323003
  \`\`\`
322362
- record_journey({
323004
+ ${emailVerificationUnavailable ? `record_journey({
323005
+ name: "Contact test",
323006
+ route: "/contact",
323007
+ routes: ["/contact"],
323008
+ steps: ["Try the contact form"],
323009
+ expectedOutcome: "User sees 'The Best Platform Ever' heading"
323010
+ })` : `record_journey({
322363
323011
  name: "Login test",
322364
323012
  route: "/auth/login",
322365
323013
  routes: ["/auth/login"],
322366
323014
  steps: ["Login to the app"],
322367
323015
  expectedOutcome: "User sees dashboard with 'Welcome to the Best Platform Ever' heading"
322368
- })
323016
+ })`}
322369
323017
  \`\`\`
322370
323018
 
322371
323019
  ## Bug Detection - CRITICAL
@@ -322410,7 +323058,7 @@ Exception: if you need to navigate THROUGH other pages to reach a focus area (e.
322410
323058
  2. Take a \`browser_snapshot\` to understand what's on the page
322411
323059
  3. **HAPPY PATH**: Fill ALL form fields with valid/realistic data, click the primary action button (Submit, Generate, Save, Create, etc.), and observe the result. For AI/async operations, wait for the result using \`browser_wait_for\` before taking a snapshot. Call \`record_journey\` to document this flow
322412
323060
  4. **VALIDATION/ERROR**: Go back to the page. Try submitting with empty required fields. Try invalid data (wrong email format, short password, etc.). Observe ALL validation messages. Fix errors and resubmit successfully. Call \`record_journey\` for the combined validation+recovery flow
322413
- - **Exception for auth pages (login/register)**: Do validation/errors FIRST (step 4 before step 3) while you are not logged in, then happy path last. See the auth-specific instructions above
323061
+ - **Exception for auth pages (login/register)**: ${emailVerificationUnavailable ? "validation FIRST, then ONE registration attempt with the TEST_USER_EMAIL + TEST_USER_PASSWORD credential keys \u2014 if an emailed code/link is demanded, STOP (never guess a code) and note the happy path as BLOCKED (cannot receive verification email). See the auth-specific instructions above" : "Do validation/errors FIRST (step 4 before step 3) while you are not logged in, then happy path last. See the auth-specific instructions above"}
322414
323062
  5. **ADDITIONAL INTERACTIONS**: Click buttons, open dropdowns, test tabs/sections, try search if available. If there are distinct sub-sections (e.g. tabs, accordion panels), test each one
322415
323063
  6. After each interaction, call \`capture_page\` with detailed observations
322416
323064
  7. **ONLY after completing steps 1-6 for ALL focus areas**, call \`finish_exploration\`
@@ -322512,7 +323160,9 @@ ${surfaceIntelligence}
322512
323160
 
322513
323161
  ## App Details
322514
323162
  - Base URL: ${baseUrl}
322515
- ${credentialBlock}${redactedBlock}${inboxBlock}`;
323163
+ ${credentialBlock}${redactedBlock}${inboxBlock}${emailVerificationUnavailable ? `
323164
+
323165
+ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
322516
323166
  }
322517
323167
  async function runExplorePhase(mcpClient, baseUrl, variableContext, existingSiteMap, projectId, existingTestNames, options, config) {
322518
323168
  const logs2 = [];
@@ -322556,6 +323206,7 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
322556
323206
  const perceptionEnricherEnabled = ((0, shared_1.parseBoolFlag)(process.env.DISCOVERY_PERCEPTION_ENRICHER) || process.env.DISCOVERY_PERCEPTION_ENRICHER === "1") && typeof options?.getPerceptionPage === "function";
322557
323207
  const linkFrontierEnabled = (0, survey_link_frontier_js_1.isSurveyLinkFrontierEnabled)() && options?.mode === "survey";
322558
323208
  const linkScope = (0, survey_link_frontier_js_1.deriveLinkScope)(baseUrl);
323209
+ const surveyNavScope = options?.mode === "survey" ? (0, survey_scope_js_1.deriveSurveyPathScope)(baseUrl) : null;
322559
323210
  let finishRedirects = 0;
322560
323211
  let lastLandedPageUrl = null;
322561
323212
  const state2 = (0, exploration_state_js_1.createExplorationState)();
@@ -322584,6 +323235,50 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
322584
323235
  logs2.push(` [link-frontier] ingestion skipped - ${err instanceof Error ? err.message : String(err)}`);
322585
323236
  }
322586
323237
  }
323238
+ const domHarvestEnabled = linkFrontierEnabled && (0, survey_door_harvester_js_1.isDomHarvestEnabled)();
323239
+ const menuExpandEnabled = domHarvestEnabled && (0, survey_door_harvester_js_1.isMenuExpandEnabled)();
323240
+ const menuExpandAttemptedRoutes = /* @__PURE__ */ new Set();
323241
+ async function harvestDomLinksIntoFrontier() {
323242
+ if (!domHarvestEnabled)
323243
+ return;
323244
+ const currentScoped = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(state2.currentRoute);
323245
+ const currentRs = state2.routeState.get(state2.currentRoute);
323246
+ try {
323247
+ const expandHere = menuExpandEnabled && !menuExpandAttemptedRoutes.has(currentScoped);
323248
+ if (expandHere)
323249
+ menuExpandAttemptedRoutes.add(currentScoped);
323250
+ const harvest = await (0, survey_door_harvester_js_1.harvestDomLinks)(mcpClient, {
323251
+ expandMenus: expandHere,
323252
+ log: (m) => logs2.push(m)
323253
+ });
323254
+ if (harvest.pageUrl)
323255
+ lastLandedPageUrl = harvest.pageUrl;
323256
+ if (harvest.links.length > 0) {
323257
+ (0, survey_link_frontier_js_1.ingestSeenLinks)(state2, {
323258
+ links: (0, survey_door_harvester_js_1.toSeenLinks)(harvest.links),
323259
+ scope: linkScope,
323260
+ isEntryOrHub: currentScoped === linkScope.basePath || state2.iteration <= 2,
323261
+ isErrorContext: currentRs?.pageType === "ERROR",
323262
+ pageUrl: harvest.pageUrl ?? lastLandedPageUrl,
323263
+ provenance: "dom",
323264
+ log: (m) => logs2.push(m)
323265
+ });
323266
+ }
323267
+ if (harvest.navigated && harvest.originalUrl) {
323268
+ logs2.push(` [door-harvest] page moved during menu expansion \u2014 restoring ${harvest.originalUrl}`);
323269
+ try {
323270
+ const navResult = await mcpClient.callTool({ name: "browser_navigate", arguments: { url: harvest.originalUrl } });
323271
+ if (navResult && typeof navResult === "object" && navResult.isError === true) {
323272
+ logs2.push(" [door-harvest] restore navigate returned an error \u2014 model may be on a different page");
323273
+ }
323274
+ } catch (navErr) {
323275
+ logs2.push(` [door-harvest] restore failed - ${navErr instanceof Error ? navErr.message : String(navErr)}`);
323276
+ }
323277
+ }
323278
+ } catch (err) {
323279
+ logs2.push(` [door-harvest] skipped - ${err instanceof Error ? err.message : String(err)}`);
323280
+ }
323281
+ }
322587
323282
  const recentExchanges = [];
322588
323283
  const transientDirectives = [];
322589
323284
  const MAX_RECENT_EXCHANGES = 28;
@@ -322612,7 +323307,8 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
322612
323307
  let inboxState = null;
322613
323308
  let inboxReader = null;
322614
323309
  const inboxConfig = (0, verification_inbox_js_1.loadVerificationInboxConfig)();
322615
- if (inboxConfig && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext)) {
323310
+ const registerIntent = (0, inbox_tools_js_1.hasRegisterIntent)(variableContext);
323311
+ if (inboxConfig && registerIntent) {
322616
323312
  inboxState = (0, inbox_tools_js_1.createInboxRunState)(inboxConfig, {
322617
323313
  tagSeed: baseUrl,
322618
323314
  projectScope: projectId,
@@ -322630,8 +323326,19 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
322630
323326
  log: (line) => logs2.push(line)
322631
323327
  });
322632
323328
  logs2.push(`Verification inbox ENABLED for registration \u2014 email ${inboxState.email}`);
322633
- } else if ((0, inbox_tools_js_1.clearThrowawayRegisterSeed)(variableContext)) {
322634
- logs2.push("Register intent set but verification inbox not configured here \u2014 dropped the throwaway TEST_USER_EMAIL seed so the agent registers a fresh account.");
323329
+ } else {
323330
+ if (registerIntent && !inboxConfig) {
323331
+ const gap = (0, verification_inbox_js_1.describeVerificationInboxConfigGap)() ?? "verification inbox unavailable";
323332
+ logs2.push(`Register intent set but the ${gap} on this runner \u2014 keeping the seeded register identity for a one-shot registration attempt.`);
323333
+ } else if ((0, inbox_tools_js_1.clearThrowawayRegisterSeed)(variableContext)) {
323334
+ logs2.push("Stale throwaway TEST_USER_EMAIL seed found (register intent not set) \u2014 dropped it so the agent does not log in with a never-registered account.");
323335
+ }
323336
+ }
323337
+ const emailVerificationUnavailable = registerIntent && !inboxConfig && !(0, credential_tools_js_1.hasUsableLoginIdentity)(variableContext.allTestVariables ?? variableContext.publicTestVariables, {
323338
+ isPlaceholderIdentityValue: inbox_tools_js_1.isThrowawayRegisterSeedEmail
323339
+ });
323340
+ if (emailVerificationUnavailable) {
323341
+ logs2.push("Email verification unavailable \u2014 the agent will ATTEMPT registration once; any flow requiring an emailed code/link will be reported BLOCKED.");
322635
323342
  }
322636
323343
  const inboxPromptBlock = inboxState ? (0, inbox_tools_js_1.buildInboxPromptBlock)({
322637
323344
  email: inboxState.email,
@@ -322669,7 +323376,8 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
322669
323376
  let systemPrompt = config?.systemPrompt ?? buildSystemPrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, {
322670
323377
  ...options,
322671
323378
  canRotateCredentials: !!options?.onCredentialUpdate,
322672
- inboxPrompt: inboxPromptBlock
323379
+ inboxPrompt: inboxPromptBlock,
323380
+ emailVerificationUnavailable
322673
323381
  });
322674
323382
  if (config?.systemPrompt && inboxPromptBlock) {
322675
323383
  systemPrompt += `
@@ -322680,7 +323388,14 @@ ${inboxPromptBlock}`;
322680
323388
  const hasLoginCredentials = Object.keys(browserSecretsForHint).length > 0;
322681
323389
  const aiClient = options?.modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)();
322682
323390
  const isDeepMode = options?.mode === "deep";
322683
- const maxIterations = config?.maxIterationsOverride && config.maxIterationsOverride > 0 ? config.maxIterationsOverride : getMaxIterations(options?.mode ?? "full", options?.incremental);
323391
+ const baseMaxIterations = config?.maxIterationsOverride && config.maxIterationsOverride > 0 ? config.maxIterationsOverride : getMaxIterations(options?.mode ?? "full", options?.incremental);
323392
+ const adaptiveBudgetEnabled = process.env.DISCOVERY_ADAPTIVE_BUDGET === "1" || (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_ADAPTIVE_BUDGET);
323393
+ const adaptiveActive = adaptiveBudgetEnabled && isSurveyMode && linkFrontierEnabled && !options?.incremental && !(config?.maxIterationsOverride && config.maxIterationsOverride > 0);
323394
+ const adaptiveHardCap = adaptiveActive ? Math.max(baseMaxIterations, readPositiveIntEnv2(process.env, "DISCOVERY_ADAPTIVE_BUDGET_HARD_CAP", DEFAULT_ADAPTIVE_BUDGET_HARD_CAP)) : baseMaxIterations;
323395
+ let maxIterations = baseMaxIterations;
323396
+ if (adaptiveActive) {
323397
+ logs2.push(`Adaptive budget: ON (floor ${baseMaxIterations}, hard cap ${adaptiveHardCap})`);
323398
+ }
322684
323399
  const configKeepTail = isDeepMode ? Math.ceil(ctxConfig.keepTail * 1.5) : ctxConfig.keepTail;
322685
323400
  logs2.push(`Context: ${ctxConfig.tier} tier (${Math.round(ctxConfig.contextWindow / 1e3)}k window, emergency at ${Math.round(ctxConfig.emergencyTokenThreshold / 1e3)}k, prune every ${ctxConfig.pruneEveryN || "never"}, keepTail=${configKeepTail})`);
322686
323401
  logs2.push(`Discovery mode: ${useV3 ? "V3 (per-turn rebuild)" : "V2.5 (full transcript + control brief)"}${v3Env ? ` [override: V3=${v3Env}]` : " [auto]"}`);
@@ -322689,7 +323404,8 @@ ${inboxPromptBlock}`;
322689
323404
  const inboxPasswordKey = inboxState?.passwordKey ?? "TEST_USER_PASSWORD";
322690
323405
  const registerHint = ` follow the **Account Registration** steps in the system prompt to create and email-verify a new account (call provision_identity for the email, fill the signup form with that email + the \`${inboxPasswordKey}\` credential, submit, then complete the email verification)`;
322691
323406
  const surveyBreadthHint = linkFrontierEnabled ? " Visit and capture EVERY top-level link on your checklist BEFORE drilling into any section's sub-pages. The state packet's Frontier list tracks discovered-but-unvisited routes - drain it." : "";
322692
- const userMessage = config?.userMessage ?? (isSurveyMode ? registerMode ? `Navigate to ${baseUrl}. BEFORE registering, explore all reachable public auth pages from the logged-out experience (login, register, forgot-password - check menus and drawers too). Capture each with capture_page and record_transition. Do NOT submit the password-reset form. Then create your account:${registerHint}. After your account is verified and you are logged in, take a browser_snapshot and list the navigation links visible in the snapshot before clicking anything - this is your checklist.${surveyBreadthHint} Then visit each page systematically, catalog it with capture_page, and record transitions. After capture_page records a page, reuse the captured state facts; only take a fresh browser_snapshot after navigation, loading, or a visible UI state change. Open tabs, and visit one detail page per list. Map the entire application thoroughly.` : `Navigate to ${baseUrl}.${hasLoginCredentials ? ` BEFORE logging in, explore all reachable public auth pages from the logged-out experience (login, register, forgot-password - check menus and drawers too). Capture each with capture_page and record_transition. Do NOT submit registration or password-reset forms. After mapping public auth pages, log in.${loginHint}` : ""} After${hasLoginCredentials ? " logging in" : " landing"}, take a browser_snapshot and list the navigation links visible in the snapshot before clicking anything - this is your checklist.${surveyBreadthHint} Then visit each page systematically, catalog it with capture_page, and record transitions. After capture_page records a page, reuse the captured state facts; only take a fresh browser_snapshot after navigation, loading, or a visible UI state change. Open tabs, and visit one detail page per list. Map the entire application thoroughly.` : isDeepMode ? `Start by navigating to ${baseUrl}${registerMode ? ` and create your account:${registerHint}.` : hasLoginCredentials ? ` and log in.${loginHint}` : "."} Then go to the focus areas listed in the system prompt. For EACH focus area, you must: (1) test the happy path with valid data and call record_journey, (2) test validation/errors with invalid/empty data and call record_journey, (3) interact with all buttons/dropdowns/tabs on the page. You have ${maxIterations} iterations - use them all. Do NOT call finish_exploration until you have completed BOTH happy path AND validation testing for every focus area.` : `Start by navigating to ${baseUrl}${registerMode ? ` and create your account:${registerHint}.` : hasLoginCredentials ? ` and log in.${loginHint}` : "."} Then explore the application feature by feature. On each page, take a browser_snapshot and call capture_page. When you find forms or interactive features, actually USE them - fill in fields, click buttons, submit forms, and observe what happens. Capture every resulting page. Remember: visiting a page is not enough - interact with it like a real user would.`);
323407
+ const emailWallNote = ' NOTE: this project asked to REGISTER a new account, but this runner CANNOT receive email (verification inbox unavailable). Attempt registration ONCE with the `TEST_USER_EMAIL` + `TEST_USER_PASSWORD` credential keys via `fill_credential_form` (never alter the email or create a second account; if signup says the address is already registered, log in with the same keys instead). If you are signed in without an email round-trip, continue as a normal logged-in user. The moment any step demands an emailed code/link (verification code, OTP, magic link, "check your email"), STOP that flow - never type a guessed code, never click Resend - continue with publicly reachable pages and state in your finish_exploration summary that authenticated coverage is BLOCKED (cannot receive verification email).';
323408
+ const userMessage = config?.userMessage ?? (isSurveyMode ? registerMode ? `Navigate to ${baseUrl}. BEFORE registering, explore all reachable public auth pages from the logged-out experience (login, register, forgot-password - check menus and drawers too). Capture each with capture_page and record_transition. Do NOT submit the password-reset form. Then create your account:${registerHint}. After your account is verified and you are logged in, take a browser_snapshot and list the navigation links visible in the snapshot before clicking anything - this is your checklist.${surveyBreadthHint} Then visit each page systematically, catalog it with capture_page, and record transitions. After capture_page records a page, reuse the captured state facts; only take a fresh browser_snapshot after navigation, loading, or a visible UI state change. Open tabs, and visit one detail page per list. Map the entire application thoroughly.` : emailVerificationUnavailable ? `Navigate to ${baseUrl}.${emailWallNote} BEFORE the registration attempt, explore all reachable public auth pages from the logged-out experience and capture each with capture_page and record_transition (do NOT submit the password-reset form). After the registration attempt resolves (signed in, or blocked at the email wall), take a browser_snapshot and list the navigation links visible in the snapshot before clicking anything - this is your checklist.${surveyBreadthHint} Then visit each reachable page systematically, catalog it with capture_page, and record transitions. After capture_page records a page, reuse the captured state facts; only take a fresh browser_snapshot after navigation, loading, or a visible UI state change. Open tabs, and visit one detail page per list. Map the entire reachable application thoroughly.` : `Navigate to ${baseUrl}.${hasLoginCredentials ? ` BEFORE logging in, explore all reachable public auth pages from the logged-out experience (login, register, forgot-password - check menus and drawers too). Capture each with capture_page and record_transition. Do NOT submit registration or password-reset forms. After mapping public auth pages, log in.${loginHint}` : ""} After${hasLoginCredentials ? " logging in" : " landing"}, take a browser_snapshot and list the navigation links visible in the snapshot before clicking anything - this is your checklist.${surveyBreadthHint} Then visit each page systematically, catalog it with capture_page, and record transitions. After capture_page records a page, reuse the captured state facts; only take a fresh browser_snapshot after navigation, loading, or a visible UI state change. Open tabs, and visit one detail page per list. Map the entire application thoroughly.` : isDeepMode ? emailVerificationUnavailable ? `Start by navigating to ${baseUrl}.${emailWallNote} Make the ONE registration attempt first. Then go to the focus areas listed in the system prompt. If you are signed in, cover EACH focus area fully: (1) happy path with valid data \u2192 record_journey, (2) validation/errors with invalid/empty data \u2192 record_journey, (3) interact with all buttons/dropdowns/tabs. If you were blocked at the email wall, cover what is reachable WITHOUT an account: validation/errors with synthetic invalid/empty data \u2192 record_journey, public interactions, and note account-gated flows as BLOCKED in finish_exploration instead of attempting them. You have ${maxIterations} iterations. Do NOT call finish_exploration until you have covered every reachable focus area.` : `Start by navigating to ${baseUrl}${registerMode ? ` and create your account:${registerHint}.` : hasLoginCredentials ? ` and log in.${loginHint}` : "."} Then go to the focus areas listed in the system prompt. For EACH focus area, you must: (1) test the happy path with valid data and call record_journey, (2) test validation/errors with invalid/empty data and call record_journey, (3) interact with all buttons/dropdowns/tabs on the page. You have ${maxIterations} iterations - use them all. Do NOT call finish_exploration until you have completed BOTH happy path AND validation testing for every focus area.` : `Start by navigating to ${baseUrl}${registerMode ? ` and create your account:${registerHint}.` : emailVerificationUnavailable ? `.${emailWallNote}` : hasLoginCredentials ? ` and log in.${loginHint}` : "."} Then explore the application feature by feature. On each page, take a browser_snapshot and call capture_page. When you find forms or interactive features, actually USE them - fill in fields, click buttons, submit forms, and observe what happens. Capture every resulting page. Remember: visiting a page is not enough - interact with it like a real user would.`);
322693
323409
  let messages = [
322694
323410
  { role: "system", content: systemPrompt },
322695
323411
  { role: "user", content: userMessage }
@@ -322698,6 +323414,7 @@ ${inboxPromptBlock}`;
322698
323414
  let lastVisitedSize = 0;
322699
323415
  let lastSurveyProgressMark = 0;
322700
323416
  let staleIterations = 0;
323417
+ let staleKnownKeysMark = 0;
322701
323418
  let lastCaptureIteration = 0;
322702
323419
  let lastJourneyProgress = 0;
322703
323420
  let lastDeepProgressMark = 0;
@@ -322710,9 +323427,151 @@ ${inboxPromptBlock}`;
322710
323427
  const BRIEF_INTERVAL = 5;
322711
323428
  const stuckDetectorEnabled = process.env.DISCOVERY_STUCK_DETECTOR === "1" || (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_STUCK_DETECTOR);
322712
323429
  const stuckDetector = stuckDetectorEnabled ? new stuck_detector_js_1.StuckDetector({ repetitionReachesRecovery: !isDeepMode }) : null;
322713
- while (iteration < maxIterations && !explorationComplete) {
323430
+ let adaptiveDecisionArmedAt = Math.ceil(maxIterations * ADAPTIVE_BUDGET_DECISION_PCT);
323431
+ let adaptiveRescueUsed = false;
323432
+ let adaptiveComplete = false;
323433
+ let adaptivePendingDiscoveryRoutes = [];
323434
+ let adaptiveLastNewRouteIteration = Number.NEGATIVE_INFINITY;
323435
+ let adaptivePrevVisitedSize = 0;
323436
+ let captureRevision = 0;
323437
+ let captureCreditedThrough = 0;
323438
+ const adaptiveKnownKeys = /* @__PURE__ */ new Set();
323439
+ let discoveryRevision = 0;
323440
+ let discoveryCreditedThrough = 0;
323441
+ let adaptiveLastDiscoveryIteration = Number.NEGATIVE_INFINITY;
323442
+ let adaptiveDiscoveryLeaseUsed = false;
323443
+ const adaptiveDebtRoutes = () => {
323444
+ const isAttemptedLinkEntry = (r) => {
323445
+ const item = state2.frontier.get(r);
323446
+ return item?.source === "link-seen" && item.attempted === true;
323447
+ };
323448
+ const ranked = (0, exploration_state_js_1.getRankedFrontier)(state2, { preferShallow: true }).filter((r) => r !== "/" && !isAttemptedLinkEntry(r));
323449
+ const rankedSet = new Set(ranked);
323450
+ const navigatedUncaptured = [];
323451
+ for (const r of state2.frontier.keys()) {
323452
+ if (r !== "/" && !rankedSet.has(r) && !isAttemptedLinkEntry(r))
323453
+ navigatedUncaptured.push(r);
323454
+ }
323455
+ return ranked.concat(navigatedUncaptured);
323456
+ };
323457
+ const describeRankedRoutes = (routes, max) => routes.slice(0, max).map((r) => {
323458
+ const label = state2.frontier.get(r)?.label;
323459
+ return label ? `${label} (${r})` : r;
323460
+ }).join(", ");
323461
+ const refreshAdaptiveDiscovery = () => {
323462
+ if (state2.visitedRoutes.size > adaptivePrevVisitedSize) {
323463
+ adaptivePrevVisitedSize = state2.visitedRoutes.size;
323464
+ adaptiveLastNewRouteIteration = iteration;
323465
+ captureRevision++;
323466
+ }
323467
+ const debt = adaptiveDebtRoutes();
323468
+ let grew = false;
323469
+ for (const route2 of debt) {
323470
+ const key = state2.frontier.get(route2)?.familyKey ?? (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2);
323471
+ if (!adaptiveKnownKeys.has(key)) {
323472
+ adaptiveKnownKeys.add(key);
323473
+ adaptivePendingDiscoveryRoutes.push(route2);
323474
+ grew = true;
323475
+ }
323476
+ }
323477
+ if (grew) {
323478
+ discoveryRevision++;
323479
+ adaptiveLastDiscoveryIteration = iteration;
323480
+ }
323481
+ if (!adaptiveComplete && adaptiveDecisionArmedAt === Number.POSITIVE_INFINITY && debt.length > 0) {
323482
+ adaptiveDecisionArmedAt = iteration;
323483
+ }
323484
+ };
323485
+ const runBudgetController = (opts = {}) => {
323486
+ const allowExtension = opts.allowExtension ?? true;
323487
+ const remainingList = adaptiveDebtRoutes();
323488
+ const capturing = captureRevision > captureCreditedThrough && iteration - adaptiveLastNewRouteIteration <= ADAPTIVE_BUDGET_PROGRESS_WINDOW;
323489
+ const freshDiscovery = discoveryRevision > discoveryCreditedThrough && iteration - adaptiveLastDiscoveryIteration <= ADAPTIVE_BUDGET_PROGRESS_WINDOW;
323490
+ const decision = computeBudgetDecision({
323491
+ iteration,
323492
+ currentCap: maxIterations,
323493
+ hardCap: adaptiveHardCap,
323494
+ remainingRoutes: remainingList.length,
323495
+ capturing,
323496
+ freshDiscovery,
323497
+ discoveryLeaseUsed: adaptiveDiscoveryLeaseUsed,
323498
+ rescueUsed: adaptiveRescueUsed,
323499
+ allowExtension
323500
+ });
323501
+ if (decision.action === "extend") {
323502
+ maxIterations = decision.newCap;
323503
+ adaptiveRescueUsed = false;
323504
+ discoveryCreditedThrough = discoveryRevision;
323505
+ adaptiveDecisionArmedAt = Math.min(maxIterations, Math.max(iteration + ADAPTIVE_BUDGET_MIN_GAP, Math.ceil(maxIterations * ADAPTIVE_BUDGET_DECISION_PCT)));
323506
+ const remainingSet = new Set(remainingList);
323507
+ const freshFirst = [.../* @__PURE__ */ new Set([
323508
+ ...adaptivePendingDiscoveryRoutes.filter((r) => remainingSet.has(r)),
323509
+ ...remainingList
323510
+ ])];
323511
+ adaptivePendingDiscoveryRoutes = [];
323512
+ if (decision.lease === "discovery") {
323513
+ adaptiveDiscoveryLeaseUsed = true;
323514
+ logs2.push(` [adaptive-budget] discovery lease +${decision.delta} \u2192 ${maxIterations}; ${remainingList.length} newly-found route(s)`);
323515
+ return {
323516
+ directive: `GRANTED: ${decision.delta} more iterations to capture routes you discovered late in the crawl. Reach these through the app's own links/menus, then capture_page + record_transition each: ${describeRankedRoutes(freshFirst, 6)}. If one can't be reached after a deliberate attempt, note it in your summary and move on.`,
323517
+ keepGoing: true
323518
+ };
323519
+ }
323520
+ captureCreditedThrough = captureRevision;
323521
+ logs2.push(` [adaptive-budget] capture lease +${decision.delta} \u2192 ${maxIterations}; ${remainingList.length} route(s) still un-captured`);
323522
+ return {
323523
+ directive: `GRANTED: ${decision.delta} additional iterations (your budget is now ${maxIterations}). Coverage is incomplete. Reach these still-unmapped routes through the app's own links and menus, then capture_page + record_transition each: ${describeRankedRoutes(freshFirst, 6)}. Already-captured pages may be traversed, but do not re-catalog them. If a target redirects, errors, or cannot be located after a deliberate attempt, note it in your summary and move to the next.`,
323524
+ keepGoing: true
323525
+ };
323526
+ }
323527
+ if (decision.action === "rescue") {
323528
+ adaptiveRescueUsed = true;
323529
+ const runwayTarget = Math.min(adaptiveHardCap, iteration + ADAPTIVE_BUDGET_RESCUE_WINDOW);
323530
+ if (runwayTarget > maxIterations)
323531
+ maxIterations = runwayTarget;
323532
+ adaptiveDecisionArmedAt = Math.min(maxIterations, iteration + ADAPTIVE_BUDGET_RESCUE_WINDOW);
323533
+ logs2.push(` [adaptive-budget] stall rescue \u2192 steering to ${remainingList.length} un-captured route(s) (cap ${maxIterations})`);
323534
+ return {
323535
+ directive: `You are revisiting pages without capturing anything new. ${remainingList.length} discovered route(s) are still unmapped: ${describeRankedRoutes(remainingList, 4)}. Navigate to each through the app's links/menus and capture_page it now. If one is genuinely unreachable, note it and move on \u2014 do not keep re-loading pages you have already seen.`,
323536
+ keepGoing: true
323537
+ };
323538
+ }
323539
+ if (decision.action === "finish") {
323540
+ if (decision.reason === "drained") {
323541
+ adaptiveDecisionArmedAt = Number.POSITIVE_INFINITY;
323542
+ logs2.push(" [adaptive-budget] frontier drained \u2014 quiescent (will re-arm on new discovery)");
323543
+ } else {
323544
+ adaptiveComplete = true;
323545
+ adaptiveDecisionArmedAt = Number.POSITIVE_INFINITY;
323546
+ logs2.push(` [adaptive-budget] finishing (${decision.reason}); ${remainingList.length} route(s) left un-captured`);
323547
+ }
323548
+ return { directive: null, keepGoing: false };
323549
+ }
323550
+ adaptiveDecisionArmedAt = Math.min(iteration + 5, maxIterations);
323551
+ return { directive: null, keepGoing: true };
323552
+ };
323553
+ const tryAdaptiveBoundaryExtend = () => {
323554
+ if (!adaptiveActive || adaptiveComplete)
323555
+ return false;
323556
+ refreshAdaptiveDiscovery();
323557
+ const { directive, keepGoing } = runBudgetController();
323558
+ if (keepGoing && maxIterations > iteration) {
323559
+ if (directive) {
323560
+ if (useV3) {
323561
+ transientDirectives.push(directive);
323562
+ } else {
323563
+ messages.push({ role: "user", content: directive });
323564
+ }
323565
+ }
323566
+ return true;
323567
+ }
323568
+ return false;
323569
+ };
323570
+ while (!explorationComplete && (iteration < maxIterations || tryAdaptiveBoundaryExtend())) {
322714
323571
  iteration++;
322715
323572
  (0, exploration_state_js_1.reduceEvent)(state2, { type: "ITERATION_STARTED", iteration });
323573
+ if (adaptiveActive)
323574
+ refreshAdaptiveDiscovery();
322716
323575
  const iterStartedAt = Date.now();
322717
323576
  logs2.push(`
322718
323577
  -- Explore iteration ${iteration}/${maxIterations} --`);
@@ -322756,7 +323615,7 @@ ${inboxPromptBlock}`;
322756
323615
  logs2.push(` Deep mode escalation: forced action after ${deepStaleChecks * 5} stale iterations`);
322757
323616
  } else if (deepStaleChecks >= 3 && !deepStaleNudgeSent) {
322758
323617
  deepStaleNudgeSent = true;
322759
- const nudgeMsg = `You haven't recorded any new journeys or captured new interactions in a while. If you've finished testing all focus areas (both happy path AND validation), call finish_exploration. Otherwise, continue - try submitting forms with invalid/empty data to test validation, or interact with elements you haven't tried yet.`;
323618
+ const nudgeMsg = emailVerificationUnavailable ? `You haven't recorded any new journeys or captured new interactions in a while. If you've finished testing everything reachable in your focus areas, call finish_exploration. Otherwise, continue - try submitting forms with invalid/empty synthetic data to test validation, or interact with elements you haven't tried yet. Do NOT re-attempt registration or login flows that were blocked at the email-verification wall - note them as BLOCKED instead.` : `You haven't recorded any new journeys or captured new interactions in a while. If you've finished testing all focus areas (both happy path AND validation), call finish_exploration. Otherwise, continue - try submitting forms with invalid/empty data to test validation, or interact with elements you haven't tried yet.`;
322760
323619
  if (useV3) {
322761
323620
  transientDirectives.push(nudgeMsg);
322762
323621
  } else {
@@ -322790,14 +323649,39 @@ ${inboxPromptBlock}`;
322790
323649
  }
322791
323650
  logs2.push(` Snapshot-loop nudge: ${itersSinceCapture} iterations without capture_page`);
322792
323651
  }
322793
- if (currentSurveyProgressMark === lastSurveyProgressMark) {
323652
+ if (adaptiveActive && !adaptiveComplete && adaptiveKnownKeys.size > staleKnownKeysMark) {
323653
+ staleKnownKeysMark = adaptiveKnownKeys.size;
323654
+ if (currentSurveyProgressMark === lastSurveyProgressMark && staleIterations > 0) {
323655
+ logs2.push(` Stall check: frontier grew to ${adaptiveKnownKeys.size} identities \u2014 counting as progress`);
323656
+ }
323657
+ staleIterations = 0;
323658
+ } else if (currentSurveyProgressMark === lastSurveyProgressMark) {
322794
323659
  staleIterations++;
322795
323660
  if (staleIterations >= 4) {
322796
- logs2.push(` Survey hard stop: no survey progress in ${staleIterations * 5} iterations, forcing completion`);
322797
- explorationComplete = true;
322798
- summary = `Exploration force-stopped after ${iteration} iterations - no survey progress in ${staleIterations * 5} iterations. Visited ${currentVisitedSize} pages.`;
322799
- break;
322800
- } else if (staleIterations >= 2) {
323661
+ if (adaptiveActive && !adaptiveComplete) {
323662
+ const { directive, keepGoing } = runBudgetController({ allowExtension: false });
323663
+ if (keepGoing) {
323664
+ staleIterations = 2;
323665
+ if (directive) {
323666
+ if (useV3) {
323667
+ transientDirectives.push(directive);
323668
+ } else {
323669
+ messages.push({ role: "user", content: directive });
323670
+ }
323671
+ }
323672
+ } else {
323673
+ logs2.push(` Survey hard stop (adaptive terminal): no survey progress in ${staleIterations * 5} iterations`);
323674
+ explorationComplete = true;
323675
+ summary = `Exploration force-stopped after ${iteration} iterations - no survey progress in ${staleIterations * 5} iterations. Visited ${currentVisitedSize} pages.`;
323676
+ break;
323677
+ }
323678
+ } else {
323679
+ logs2.push(` Survey hard stop: no survey progress in ${staleIterations * 5} iterations, forcing completion`);
323680
+ explorationComplete = true;
323681
+ summary = `Exploration force-stopped after ${iteration} iterations - no survey progress in ${staleIterations * 5} iterations. Visited ${currentVisitedSize} pages.`;
323682
+ break;
323683
+ }
323684
+ } else if (staleIterations >= 2 && !adaptiveActive) {
322801
323685
  const loopMsg = "You appear to be revisiting the same pages without discovering new routes, transitions, or useful auth/navigation progress. Call finish_exploration NOW with a summary of what you found.";
322802
323686
  if (useV3) {
322803
323687
  transientDirectives.push(loopMsg);
@@ -322844,7 +323728,23 @@ ${inboxPromptBlock}`;
322844
323728
  lastJourneyProgress = currentJourneyProgress;
322845
323729
  }
322846
323730
  }
322847
- if (iteration >= Math.floor(maxIterations * 0.9)) {
323731
+ if (adaptiveActive && !adaptiveComplete && iteration >= adaptiveDecisionArmedAt) {
323732
+ const { directive, keepGoing } = runBudgetController();
323733
+ if (directive) {
323734
+ if (useV3) {
323735
+ transientDirectives.push(directive);
323736
+ } else {
323737
+ messages.push({ role: "user", content: directive });
323738
+ }
323739
+ }
323740
+ if (!keepGoing && adaptiveComplete) {
323741
+ explorationComplete = true;
323742
+ summary = summary || `Exploration finished after ${iteration} iterations (adaptive budget: coverage incomplete).`;
323743
+ logs2.push(` [adaptive-budget] terminal at iteration ${iteration}`);
323744
+ break;
323745
+ }
323746
+ }
323747
+ if (!adaptiveActive && iteration >= Math.floor(maxIterations * 0.9)) {
322848
323748
  const budgetTestCount = useV2 ? state2.recordedJourneys.length : localJourneys.length;
322849
323749
  const budgetBlocking = linkFrontierEnabled ? (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2) : [];
322850
323750
  const budgetMsg = isDeepMode ? budgetTestCount === 0 ? "CRITICAL: You have used 90% of your iteration budget and have NOT recorded any journeys yet. You MUST call record_journey NOW for every flow you have tested (happy paths, validation, edge cases). After calling record_journey for all completed flows, call finish_exploration. Do NOT call finish_exploration without first calling record_journey." : `CRITICAL: You have used 90% of your iteration budget. You have recorded ${budgetTestCount} journey(s) so far. If you have completed any additional flows that you haven't called record_journey for, do so NOW. Then call finish_exploration with a summary.` : budgetBlocking.length > 0 ? `CRITICAL: You have used 90% of your iteration budget. These discovered routes were never visited: ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(budgetBlocking, 4)}. Visit ONLY these (capture_page each), then call finish_exploration. Do not explore anything else.` : "CRITICAL: You have used 90% of your iteration budget. Wrap up your current interaction and call finish_exploration with a summary of what you found. If you are mid-flow, complete it quickly then finish.";
@@ -322936,7 +323836,7 @@ ${recent}`);
322936
323836
  if (remainingFocusAreas.length > 0)
322937
323837
  progressLines.push(`### Focus Areas NOT YET Visited: ${remainingFocusAreas.join(", ")} - explore these next!`);
322938
323838
  progressLines.push("");
322939
- progressLines.push("IMPORTANT: For each focus area, you must test BOTH the happy path (valid data) AND validation/error handling (invalid/empty data). If you have only done the happy path, go back and test validation now.");
323839
+ progressLines.push(emailVerificationUnavailable ? "IMPORTANT: For each focus area, test everything reachable in your current auth state: validation/error handling with invalid/empty data, plus the happy path if you are signed in. If a flow was blocked at the email-verification wall, note it as BLOCKED \u2014 do not re-attempt it and never type a guessed code." : "IMPORTANT: For each focus area, you must test BOTH the happy path (valid data) AND validation/error handling (invalid/empty data). If you have only done the happy path, go back and test validation now.");
322940
323840
  }
322941
323841
  progressLines.push("");
322942
323842
  progressLines.push("## NEXT ACTIONS");
@@ -322947,7 +323847,7 @@ ${recent}`);
322947
323847
  });
322948
323848
  if (untestedAreas.length > 0) {
322949
323849
  progressLines.push(`These focus areas still need testing: ${untestedAreas.join(", ")}`);
322950
- progressLines.push("For each: (1) happy path with valid data \u2192 record_journey, (2) validation with invalid/empty data \u2192 record_journey");
323850
+ progressLines.push(emailVerificationUnavailable ? "For each: cover what your current auth state reaches \u2014 validation with invalid/empty data \u2192 record_journey, plus happy path if signed in; note email-wall-blocked flows as BLOCKED instead of re-attempting them" : "For each: (1) happy path with valid data \u2192 record_journey, (2) validation with invalid/empty data \u2192 record_journey");
322951
323851
  } else {
322952
323852
  progressLines.push("All focus areas have tests. Look for additional edge cases or call finish_exploration.");
322953
323853
  }
@@ -323000,7 +323900,8 @@ ${currentSummary}`;
323000
323900
  const brief = (0, exploration_prompt_builder_js_2.buildControlBrief)(state2, {
323001
323901
  mode: options?.mode ?? "full",
323002
323902
  maxIterations,
323003
- focusAreas: options?.focusAreas
323903
+ focusAreas: options?.focusAreas,
323904
+ adaptiveBudget: adaptiveActive
323004
323905
  });
323005
323906
  messages.push({ role: "user", content: brief });
323006
323907
  lastBriefRoute = currentStateRoute;
@@ -323019,7 +323920,8 @@ ${currentSummary}`;
323019
323920
  recentExchanges,
323020
323921
  incremental: options?.incremental,
323021
323922
  userNotes: options?.userNotes,
323022
- linkFrontierEnabled
323923
+ linkFrontierEnabled,
323924
+ adaptiveBudget: adaptiveActive
323023
323925
  });
323024
323926
  transientDirectives.length = 0;
323025
323927
  }
@@ -323228,6 +324130,9 @@ ${currentSummary}`;
323228
324130
  sessionState: normalized.sessionState
323229
324131
  });
323230
324132
  ingestLinksFromText(snapshotText.snapshotText);
324133
+ if (domHarvestEnabled) {
324134
+ await harvestDomLinksIntoFrontier();
324135
+ }
323231
324136
  const isSurvey = options?.mode === "survey";
323232
324137
  const subNavItems = [];
323233
324138
  for (const el of normalized.elements) {
@@ -323736,6 +324641,30 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
323736
324641
  logs2.push(` finish accepted with ${blocking.length} uncovered discovered route(s): ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(blocking, 10)}`);
323737
324642
  }
323738
324643
  }
324644
+ if (adaptiveActive && !adaptiveComplete) {
324645
+ refreshAdaptiveDiscovery();
324646
+ const remainingList = adaptiveDebtRoutes();
324647
+ if (remainingList.length > 0 && iteration < adaptiveHardCap) {
324648
+ let reason = null;
324649
+ if (iteration >= adaptiveDecisionArmedAt) {
324650
+ const { directive, keepGoing } = runBudgetController();
324651
+ if (keepGoing) {
324652
+ reason = directive ?? `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration.`;
324653
+ }
324654
+ } else {
324655
+ reason = `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration.`;
324656
+ }
324657
+ if (reason) {
324658
+ toolResults.push({
324659
+ role: "tool",
324660
+ tool_call_id: toolCall.id,
324661
+ content: JSON.stringify({ acknowledged: false, keepExploring: true, reason })
324662
+ });
324663
+ logs2.push(` [adaptive-budget] finish bounced: ${remainingList.length} route(s) still un-captured`);
324664
+ continue;
324665
+ }
324666
+ }
324667
+ }
323739
324668
  explorationComplete = true;
323740
324669
  summary = String(toolArgs.summary || "");
323741
324670
  (0, exploration_state_js_1.reduceEvent)(state2, { type: "EXPLORATION_FINISHED", summary });
@@ -323793,6 +324722,18 @@ Exploration complete: ${summary}`);
323793
324722
  continue;
323794
324723
  }
323795
324724
  if (toolName === "fill_credential" || toolName === "fill_credential_form") {
324725
+ if (emailVerificationUnavailable) {
324726
+ const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
324727
+ if (codeGuard.blocked) {
324728
+ logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
324729
+ toolResults.push({
324730
+ role: "tool",
324731
+ tool_call_id: toolCall.id,
324732
+ content: codeGuard.message ?? "BLOCKED: cannot receive verification email"
324733
+ });
324734
+ continue;
324735
+ }
324736
+ }
323796
324737
  const fullVarMap = variableContext.allTestVariables ?? variableContext.publicTestVariables;
323797
324738
  const { secrets: dispatchSecrets } = (0, credential_tools_js_1.partitionTestVariables)(fullVarMap);
323798
324739
  try {
@@ -323875,6 +324816,18 @@ Exploration complete: ${summary}`);
323875
324816
  continue;
323876
324817
  }
323877
324818
  }
324819
+ if (emailVerificationUnavailable) {
324820
+ const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
324821
+ if (codeGuard.blocked) {
324822
+ logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
324823
+ toolResults.push({
324824
+ role: "tool",
324825
+ tool_call_id: toolCall.id,
324826
+ content: codeGuard.message ?? "BLOCKED: cannot receive verification email"
324827
+ });
324828
+ continue;
324829
+ }
324830
+ }
323878
324831
  if (inboxState?.provisioned && toolCallLooksLikeSignupSubmit(toolName, toolArgs)) {
323879
324832
  accountAlreadyExistsRecoveryExpiresAt = iteration + 2;
323880
324833
  }
@@ -324118,6 +325071,11 @@ Exploration complete: ${summary}`);
324118
325071
  logs2.push(` \u26A0 Out of scope: ${detectedRoute} (focus: ${navFocusAreas.join(", ")})`);
324119
325072
  toolResults.__outOfScopeRedirect = `You navigated to ${detectedRoute} which is OUTSIDE your focus areas (${navFocusAreas.join(", ")}). This is a different feature. You may observe the URL for assertion purposes (e.g. "verify redirect to ${detectedRoute}"), but do NOT interact with this page - do not fill forms, click buttons, or call capture_page here. Navigate back to your focus areas or call finish_exploration if you are done.`;
324120
325073
  }
325074
+ } else if (surveyNavScope) {
325075
+ if (!(0, survey_scope_js_1.routeWithinSurveyBasePath)(detectedRoute, surveyNavScope) && !(0, survey_scope_js_1.hasAuthFlowSegment)(detectedRoute)) {
325076
+ logs2.push(` \u26A0 Out of survey scope: ${detectedRoute} (entry scope: ${surveyNavScope.basePath})`);
325077
+ toolResults.__outOfScopeRedirect = `You navigated to ${detectedRoute}, which is OUTSIDE this survey's entry scope (\`${surveyNavScope.basePath}\`). This is a SCOPED survey \u2014 that page is not part of the audited flow: do NOT catalog it (no capture_page), do not follow its links, and do not create a featureGroup for it. Navigate back to a page under \`${surveyNavScope.basePath}\` and continue mapping the in-scope flow. If you have already covered the in-scope pages, call finish_exploration.`;
325078
+ }
324121
325079
  }
324122
325080
  }
324123
325081
  }
@@ -324413,6 +325371,7 @@ Fatal error: ${msg}`);
324413
325371
  const status = fatalErr?.status;
324414
325372
  const lower = msg.toLowerCase();
324415
325373
  const truncatedReason = status === 429 || lower.includes("rate limit") || lower.includes("too many requests") || lower.includes("quota") ? "rate_limit" : status === 408 || lower.includes("timeout") || lower.includes("timed out") || lower.includes("etimedout") ? "timeout" : "ai_error";
325374
+ const truncatedTransient = (0, ai_retry_js_1.isTransientRateError)(fatalErr);
324416
325375
  if (useV2) {
324417
325376
  try {
324418
325377
  (0, exploration_state_js_1.flushUnflushedCommands)(state2, variableContext.allTestVariables ?? variableContext.publicTestVariables);
@@ -324457,7 +325416,8 @@ Fatal error: ${msg}`);
324457
325416
  collectedTransitions: state2.collectedTransitions,
324458
325417
  iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0,
324459
325418
  truncated: true,
324460
- truncatedReason
325419
+ truncatedReason,
325420
+ truncatedTransient
324461
325421
  };
324462
325422
  }
324463
325423
  return {
@@ -324477,7 +325437,8 @@ Fatal error: ${msg}`);
324477
325437
  collectedTransitions,
324478
325438
  iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0,
324479
325439
  truncated: true,
324480
- truncatedReason
325440
+ truncatedReason,
325441
+ truncatedTransient
324481
325442
  };
324482
325443
  } finally {
324483
325444
  if (inboxReader) {
@@ -324506,9 +325467,12 @@ var require_discover_feature_explorer = __commonJS({
324506
325467
  "../runner-core/dist/services/discover-feature-explorer.js"(exports2) {
324507
325468
  "use strict";
324508
325469
  Object.defineProperty(exports2, "__esModule", { value: true });
325470
+ exports2.buildFeatureExplorePrompt = buildFeatureExplorePrompt;
325471
+ exports2.buildIncrementalExplorePrompt = buildIncrementalExplorePrompt;
324509
325472
  exports2.runFeatureExplorePhase = runFeatureExplorePhase;
324510
325473
  var discover_explorer_js_1 = require_discover_explorer();
324511
325474
  var exploration_parser_js_1 = require_exploration_parser();
325475
+ var credential_tools_js_1 = require_credential_tools();
324512
325476
  var verification_inbox_js_1 = require_verification_inbox();
324513
325477
  var inbox_tools_js_1 = require_inbox_tools();
324514
325478
  function buildFeatureExplorerConfig(focusAreas, incremental, otherRoutes) {
@@ -324573,7 +325537,8 @@ var require_discover_feature_explorer = __commonJS({
324573
325537
  const browserCredentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
324574
325538
  const hasCredentials = Object.keys(browserCredentials).length > 0;
324575
325539
  const registerMode = !!((0, verification_inbox_js_1.loadVerificationInboxConfig)() && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext));
324576
- const credentialLines = Object.entries(browserCredentials).map(([k, v]) => `- **${k}**: \`${v}\``).join("\n");
325540
+ const emailVerificationUnavailable = !registerMode && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext) && !(0, credential_tools_js_1.hasUsableLoginIdentity)(browserCredentials, { isPlaceholderIdentityValue: inbox_tools_js_1.isThrowawayRegisterSeedEmail });
325541
+ const credentialLines = Object.entries(browserCredentials).filter(([k]) => k !== "DISCOVERY_SHOULD_REGISTER").map(([k, v]) => `- **${k}**: \`${v}\``).join("\n");
324577
325542
  const otherFeaturesBlock = featureOpts.otherFeatures && featureOpts.otherFeatures.length > 0 ? `
324578
325543
  ## Other Features (DO NOT TEST)
324579
325544
  These are separate features. Do NOT navigate to their pages, do NOT interact with them, do NOT call record_journey for them:
@@ -324684,6 +325649,22 @@ ${transitionBlock}
324684
325649
  ${(() => {
324685
325650
  const isAuthFeature = featureOpts.focusAreas.some((fa) => /\/(auth|login|sign-?in|register|signup)\b/i.test(fa));
324686
325651
  if (isAuthFeature && hasCredentials) {
325652
+ if (emailVerificationUnavailable) {
325653
+ return `## Login/Registration IS Your Test Assignment (email verification UNAVAILABLE)
325654
+ Your assigned pages ARE the login/registration pages. This project registers its own account, but this runner CANNOT receive email \u2014 registration may still work if the app does not require email verification.
325655
+
325656
+ Follow this order:
325657
+ 1. Navigate to your first assigned auth page (e.g., the login page)
325658
+ 2. Take a \`browser_snapshot\` to see what's there
325659
+ 3. **Test VALIDATION FIRST** while you are NOT logged in - on the LOGIN form: empty fields, invalid email, wrong password (synthetic values like "wrong@example.com" / "badpassword"); on the REGISTRATION form: empty-submit validation ONLY (a filled synthetic signup could create a stray account)
325660
+ 4. Call \`record_journey\` for the validation flow
325661
+ 5. **THEN attempt the happy path ONCE via REGISTRATION**: fill the signup form with \`fill_credential_form\` using the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys and submit it ONCE. Never alter the email, never create a second account. If signup says the address is ALREADY registered (a previous run created it), log in with the same credential keys instead
325662
+ 6. **THE WALL**: if any step demands an emailed code/link (verification code, OTP, magic link, "check your email"), STOP immediately - never type a guessed code, never click Resend - and treat the happy path as BLOCKED
325663
+ 7. If you are signed in with NO email round-trip, call \`record_journey\` for the happy path
325664
+ 8. Call \`capture_page\` on each auth page AFTER interacting (this saves the successful commands for test generation)
325665
+
325666
+ When done, call \`finish_exploration\` - if you hit the wall, note that the authenticated happy path is BLOCKED (cannot receive verification email).`;
325667
+ }
324687
325668
  return `## Login/Registration IS Your Test Assignment
324688
325669
  Your assigned pages ARE the login/registration pages. Do NOT log in as a pass-through - the auth pages are what you are testing.
324689
325670
 
@@ -324738,7 +325719,7 @@ ${otherFeaturesBlock}
324738
325719
  4. Take \`browser_snapshot\` of the result page
324739
325720
  - If the page shows a loading skeleton or spinner after navigation, wait for the real content to appear before calling \`capture_page\` or \`record_journey\`
324740
325721
  5. Call \`record_journey\` with:
324741
- - All routes visited in \`routes\` array (e.g., ["/auth/login", "/dashboard"])
325722
+ - All routes visited in \`routes\` array (e.g., ${emailVerificationUnavailable ? '["/", "/contact"]' : '["/auth/login", "/dashboard"]'})
324742
325723
  - Specific steps describing what you did
324743
325724
  - expectedOutcome with the EXACT result you observed
324744
325725
  - assertions with STABLE structural elements from the result page
@@ -324750,11 +325731,15 @@ ${otherFeaturesBlock}
324750
325731
  4. Fix the errors and resubmit successfully \u2192 observe recovery
324751
325732
  5. Call \`record_journey\` ONCE for this entire combined flow
324752
325733
 
324753
- **Auth pages (login/register) - test NEGATIVE before positive:**
325734
+ ${emailVerificationUnavailable ? `**Auth pages (login/register) - this runner CANNOT receive email:**
325735
+ - Test negative flows FIRST: on the LOGIN form empty fields \u2192 invalid synthetic values; on the REGISTRATION form empty-submit only (never submit a filled synthetic signup) \u2192 call record_journey for the validation flow
325736
+ - THEN attempt the happy path ONCE via registration with the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys (\`fill_credential_form\`); if the address is already registered, log in with the same keys
325737
+ - If any step demands an emailed code/link, STOP - never type a guessed code - and note in finish_exploration that the authenticated happy path is BLOCKED (cannot receive verification email)
325738
+ - If you are signed in without an email round-trip, call record_journey for the happy path` : `**Auth pages (login/register) - test NEGATIVE before positive:**
324754
325739
  - You arrive at the page NOT logged in - test errors FIRST while in this state
324755
325740
  - Empty fields \u2192 invalid data \u2192 call record_journey
324756
325741
  - THEN do the happy path - valid credentials \u2192 call record_journey
324757
- - This order avoids needing to log out between journeys
325742
+ - This order avoids needing to log out between journeys`}
324758
325743
 
324759
325744
  ### For CRUD Features - Combine into a Single Journey:
324760
325745
  If the feature has Create/View/Edit/Delete operations across pages:
@@ -324779,7 +325764,7 @@ When you have thoroughly explored all the feature pages and tested the key flows
324779
325764
  - Pages you visited and what you found there
324780
325765
 
324781
325766
  ## Test Data Rules
324782
- ${registerMode ? `- **Account credentials**: do NOT invent an email or password \u2014 register and log in with the email returned by \`provision_identity\` and the \`TEST_USER_PASSWORD\` credential (filled via \`fill_credential\`). See the Account Registration section.` : `- **LOGIN** credential fields: use \`process.env.TEST_USER_EMAIL!\` and \`process.env.TEST_USER_PASSWORD!\`
325767
+ ${registerMode ? `- **Account credentials**: do NOT invent an email or password \u2014 register and log in with the email returned by \`provision_identity\` and the \`TEST_USER_PASSWORD\` credential (filled via \`fill_credential\`). See the Account Registration section.` : emailVerificationUnavailable ? `- **Account credentials**: this project registers its own account but this runner CANNOT receive email. Register/log in ONLY with the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys (\`fill_credential\` / \`fill_credential_form\`) \u2014 never invent a different email, never create a second account. If a step demands an emailed verification code or link, STOP that flow (never guess a code) and mark it BLOCKED.` : `- **LOGIN** credential fields: use \`process.env.TEST_USER_EMAIL!\` and \`process.env.TEST_USER_PASSWORD!\`
324783
325768
  - **REGISTRATION/SIGNUP** email fields: MUST be unique every run \u2192 \`testuser-\${Date.now()}@example.com\`
324784
325769
  - Passwords for registration: \`TestPass123!\` (meets typical strength rules)`}
324785
325770
  - **Other form fields**: use realistic static values (Jane Smith, 555-0100, https://example.com)
@@ -324801,7 +325786,17 @@ When calling \`record_journey\`:
324801
325786
 
324802
325787
  **Example record_journey with good assertions:**
324803
325788
  \`\`\`
324804
- record_journey({
325789
+ ${emailVerificationUnavailable ? `record_journey({
325790
+ name: "Login Form - Validation",
325791
+ route: "/auth/login",
325792
+ routes: ["/", "/auth/login"],
325793
+ steps: ["Navigate to /", "Click 'Sign In'", "Submit the empty form", "Observe validation errors"],
325794
+ expectedOutcome: "URL matches /auth/login. Validation error 'Email is required' is visible.",
325795
+ assertions: [
325796
+ { type: "url", value: "/auth/login" },
325797
+ { type: "error-text", value: "Email is required" }
325798
+ ]
325799
+ })` : `record_journey({
324805
325800
  name: "Login - Happy Path",
324806
325801
  route: "/auth/login",
324807
325802
  routes: ["/", "/auth/login", "/dashboard"],
@@ -324812,7 +325807,7 @@ record_journey({
324812
325807
  { type: "nav-link", locator: "getByRole('link', { name: 'Dashboard' })" },
324813
325808
  { type: "heading", locator: "getByRole('heading', { name: 'Dashboard' })" }
324814
325809
  ]
324815
- })
325810
+ })`}
324816
325811
  \`\`\`
324817
325812
  BAD assertions (do NOT use): \`{ type: "heading", locator: "getByRole('heading', { name: 'Welcome back, John!' })" }\` - user-specific text
324818
325813
 
@@ -324848,13 +325843,16 @@ Do NOT re-propose a test with the same name or identical steps as any item above
324848
325843
  ${hasCredentials ? `
324849
325844
  ## Credentials (browser interaction ONLY - NEVER include in record_journey steps)
324850
325845
  ${credentialLines}
325846
+ ` : ""}${emailVerificationUnavailable ? `
325847
+ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}
324851
325848
  ` : ""}`;
324852
325849
  }
324853
325850
  function buildIncrementalExplorePrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, featureOpts) {
324854
325851
  const browserCredentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
324855
325852
  const hasCredentials = Object.keys(browserCredentials).length > 0;
324856
325853
  const registerMode = !!((0, verification_inbox_js_1.loadVerificationInboxConfig)() && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext));
324857
- const credentialLines = Object.entries(browserCredentials).map(([k, v]) => `- **${k}**: \`${v}\``).join("\n");
325854
+ const emailVerificationUnavailable = !registerMode && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext) && !(0, credential_tools_js_1.hasUsableLoginIdentity)(browserCredentials, { isPlaceholderIdentityValue: inbox_tools_js_1.isThrowawayRegisterSeedEmail });
325855
+ const credentialLines = Object.entries(browserCredentials).filter(([k]) => k !== "DISCOVERY_SHOULD_REGISTER").map(([k, v]) => `- **${k}**: \`${v}\``).join("\n");
324858
325856
  const existingTestsBlock = featureOpts.existingTestDetails && featureOpts.existingTestDetails.length > 0 ? featureOpts.existingTestDetails.map((t, i) => {
324859
325857
  const stepsStr = t.steps.length > 0 ? `
324860
325858
  Steps: ${t.steps.join(" \u2192 ")}` : "";
@@ -324926,7 +325924,7 @@ ${featureOpts.appBrief}
324926
325924
  ${featureOpts.focusAreas.map((fa) => `- ${fa}`).join("\n")}
324927
325925
  ${pageElementsBlock}${otherFeaturesBlock}
324928
325926
  ## Process
324929
- 1. ${registerMode ? "Create your account by following the Account Registration section below, then navigate to the feature pages" : isAuthFeature && hasCredentials ? "Navigate to the auth pages - test validation/error scenarios NOT already covered" : hasCredentials ? "Log in using credentials below, then navigate to the feature pages" : "Navigate to the feature pages"}
325927
+ 1. ${registerMode ? "Create your account by following the Account Registration section below, then navigate to the feature pages" : emailVerificationUnavailable ? "This runner CANNOT receive email. Attempt registration ONCE with the `TEST_USER_EMAIL` + `TEST_USER_PASSWORD` credential keys via `fill_credential_form` (if the address is already registered, log in with the same keys), then navigate to the feature pages. If any step demands an emailed code/link, STOP that flow \u2014 never type a guessed code \u2014 cover only unauthenticated gaps and note blocked authenticated scenarios in finish_exploration" : isAuthFeature && hasCredentials ? "Navigate to the auth pages - test validation/error scenarios NOT already covered" : hasCredentials ? "Log in using credentials below, then navigate to the feature pages" : "Navigate to the feature pages"}
324930
325928
  2. Take \`browser_snapshot\` on each page
324931
325929
  3. Compare what you see with the existing tests - what flows are NOT tested?
324932
325930
  4. For each gap you find, interact with the page to verify the flow works, then call \`record_journey\`
@@ -324950,11 +325948,14 @@ ${featureOpts.priorInsight}
324950
325948
  ${hasCredentials ? `
324951
325949
  ## Credentials (browser interaction ONLY)
324952
325950
  ${credentialLines}
325951
+ ` : ""}${emailVerificationUnavailable ? `
325952
+ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}
324953
325953
  ` : ""}`;
324954
325954
  }
324955
325955
  async function runFeatureExplorePhase(mcpClient, baseUrl, variableContext, existingSiteMap, projectId, existingTestNames, featureOpts) {
324956
325956
  const inboxConfig = (0, verification_inbox_js_1.loadVerificationInboxConfig)();
324957
- const registerMode = !!(inboxConfig && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext));
325957
+ const registerIntent = (0, inbox_tools_js_1.hasRegisterIntent)(variableContext);
325958
+ const registerMode = !!(inboxConfig && registerIntent);
324958
325959
  let inboxPasswordKey = "TEST_USER_PASSWORD";
324959
325960
  if (registerMode && inboxConfig) {
324960
325961
  const inboxIdentity = (0, inbox_tools_js_1.createInboxRunState)(inboxConfig, { tagSeed: baseUrl, projectScope: projectId });
@@ -324965,6 +325966,9 @@ ${credentialLines}
324965
325966
  passwordKey: inboxPasswordKey
324966
325967
  });
324967
325968
  }
325969
+ const emailVerificationUnavailable = registerIntent && !inboxConfig && !(0, credential_tools_js_1.hasUsableLoginIdentity)(variableContext.allTestVariables ?? variableContext.publicTestVariables, {
325970
+ isPlaceholderIdentityValue: inbox_tools_js_1.isThrowawayRegisterSeedEmail
325971
+ });
324968
325972
  const isIncremental = featureOpts.incremental && existingTestNames.length > 0;
324969
325973
  const systemPrompt = isIncremental ? buildIncrementalExplorePrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, featureOpts) : buildFeatureExplorePrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, featureOpts);
324970
325974
  const hasCredentials = Object.keys(variableContext.allTestVariables ?? variableContext.publicTestVariables).length > 0;
@@ -324975,6 +325979,8 @@ ${credentialLines}
324975
325979
  const focusHint = featureOpts.userNotes ? ` The user asked you to focus on: "${featureOpts.userNotes}".` : "";
324976
325980
  if (registerMode) {
324977
325981
  userMessage = `Navigate to ${baseUrl} and create your account:${registerHint}. Then review the ${existingTestNames.length} existing tests and navigate to: ${featureOpts.focusAreas.join(", ")}. Look for gaps - scenarios, pages, or error cases not yet covered.${focusHint} If all meaningful scenarios are covered, call finish_exploration with "No additional tests needed."`;
325982
+ } else if (emailVerificationUnavailable) {
325983
+ userMessage = `Navigate to ${baseUrl}. NOTE: this project registers its own account but this runner CANNOT receive email. Attempt registration ONCE with the TEST_USER_EMAIL + TEST_USER_PASSWORD credential keys via fill_credential_form (if the address is already registered, log in with the same keys; never alter the email or invent another). If any step demands an emailed code/link, STOP that flow \u2014 never type a guessed code. Review the ${existingTestNames.length} existing tests, then look for gaps reachable in your auth state on: ${featureOpts.focusAreas.join(", ")}.${focusHint} Note any email-wall-blocked scenarios as BLOCKED in finish_exploration.`;
324978
325984
  } else if (isAuthFeature) {
324979
325985
  userMessage = `Navigate to ${baseUrl}. Review the ${existingTestNames.length} existing tests listed above, then navigate to the auth pages: ${featureOpts.focusAreas.join(", ")}. Look for untested scenarios - error cases, edge cases, flows not yet covered.${focusHint} If all meaningful scenarios are covered, call finish_exploration with "No additional tests needed."`;
324980
325986
  } else if (hasCredentials) {
@@ -324986,6 +325992,8 @@ ${credentialLines}
324986
325992
  let navigationInstruction;
324987
325993
  if (registerMode) {
324988
325994
  navigationInstruction = `Navigate to ${baseUrl} and create your account:${registerHint}. Then go to your assigned feature pages: ${featureOpts.focusAreas.join(", ")}.`;
325995
+ } else if (emailVerificationUnavailable) {
325996
+ navigationInstruction = `Navigate to ${baseUrl}. NOTE: this project registers its own account but this runner CANNOT receive email. Attempt registration ONCE with the TEST_USER_EMAIL + TEST_USER_PASSWORD credential keys via fill_credential_form (if the address is already registered, log in with the same keys; never alter the email or create a second account). If you are signed in without an email round-trip, test your assigned feature pages fully: ${featureOpts.focusAreas.join(", ")}. If any step demands an emailed code/link, STOP that flow \u2014 never type a guessed code \u2014 test what is reachable without authentication instead and note blocked authenticated flows in finish_exploration.`;
324989
325997
  } else if (isAuthFeature) {
324990
325998
  navigationInstruction = `Navigate to ${baseUrl}. Your test assignment is the authentication pages: ${featureOpts.focusAreas.join(", ")}. Navigate to the login/register page and test it - start with validation (empty/wrong fields) while NOT logged in, then test the happy path.`;
324991
325999
  } else if (hasCredentials) {
@@ -325218,8 +326226,14 @@ var require_auth_capture_run = __commonJS({
325218
326226
  exports2.filterLoginCaptureCredentials = filterLoginCaptureCredentials;
325219
326227
  exports2.buildLoginCaptureCredentials = buildLoginCaptureCredentials;
325220
326228
  exports2.getLoginCaptureMaxIterations = getLoginCaptureMaxIterations;
326229
+ exports2.getCaptureRateLimitMaxRetries = getCaptureRateLimitMaxRetries;
326230
+ exports2.getCaptureRateLimitMaxDelayMs = getCaptureRateLimitMaxDelayMs;
326231
+ exports2.getAuthCaptureTimeoutMs = getAuthCaptureTimeoutMs;
326232
+ exports2.getAuthCaptureRunRetryMaxAttempts = getAuthCaptureRunRetryMaxAttempts;
326233
+ exports2.classifyCaptureMiss = classifyCaptureMiss;
325221
326234
  exports2.harvestAuthProofLive = harvestAuthProofLive;
325222
326235
  exports2.findCredentialLiteralLeaks = findCredentialLiteralLeaks;
326236
+ exports2.runLoginCaptureRunWithOutcome = runLoginCaptureRunWithOutcome;
325223
326237
  exports2.runLoginCaptureRun = runLoginCaptureRun;
325224
326238
  exports2.getRegisterCaptureMaxIterations = getRegisterCaptureMaxIterations;
325225
326239
  exports2.runRegisterCaptureRun = runRegisterCaptureRun;
@@ -325232,6 +326246,7 @@ var require_auth_capture_run = __commonJS({
325232
326246
  var playwright_native_js_1 = require_playwright_native();
325233
326247
  var scrub_credentials_js_1 = require_scrub_credentials();
325234
326248
  var credential_tools_js_1 = require_credential_tools();
326249
+ var ai_retry_js_1 = require_ai_retry();
325235
326250
  var LOGIN_CAPTURE_CONTROL_VARIABLES = /* @__PURE__ */ new Set([
325236
326251
  "BASE_URL",
325237
326252
  "DISCOVERY_SHOULD_REGISTER",
@@ -325265,6 +326280,36 @@ var require_auth_capture_run = __commonJS({
325265
326280
  }
325266
326281
  var CAPTURE_RATE_LIMIT_MAX_RETRIES = 2;
325267
326282
  var CAPTURE_RATE_LIMIT_MAX_DELAY_MS = 8e3;
326283
+ function readNonNegativeIntEnv(env, name, fallback) {
326284
+ const raw = env[name]?.trim();
326285
+ if (raw === void 0 || raw === "" || !/^\d+$/.test(raw))
326286
+ return fallback;
326287
+ const parsed = Number(raw);
326288
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback;
326289
+ }
326290
+ function getCaptureRateLimitMaxRetries(env = process.env) {
326291
+ return readNonNegativeIntEnv(env, "AUTH_CAPTURE_RATE_LIMIT_MAX_RETRIES", CAPTURE_RATE_LIMIT_MAX_RETRIES);
326292
+ }
326293
+ function getCaptureRateLimitMaxDelayMs(env = process.env) {
326294
+ return readPositiveIntEnv2(env, "AUTH_CAPTURE_RATE_LIMIT_MAX_DELAY_MS", CAPTURE_RATE_LIMIT_MAX_DELAY_MS);
326295
+ }
326296
+ var DEFAULT_AUTH_CAPTURE_TIMEOUT_MS = 4 * 60 * 1e3;
326297
+ function getAuthCaptureTimeoutMs(env = process.env) {
326298
+ return readPositiveIntEnv2(env, "AUTH_CAPTURE_TIMEOUT_MS", DEFAULT_AUTH_CAPTURE_TIMEOUT_MS);
326299
+ }
326300
+ var DEFAULT_AUTH_CAPTURE_RUN_RETRY_MAX_ATTEMPTS = 2;
326301
+ function getAuthCaptureRunRetryMaxAttempts(env = process.env) {
326302
+ return readPositiveIntEnv2(env, "AUTH_CAPTURE_RUN_RETRY_MAX_ATTEMPTS", DEFAULT_AUTH_CAPTURE_RUN_RETRY_MAX_ATTEMPTS);
326303
+ }
326304
+ function classifyCaptureMiss(explore, fallback) {
326305
+ if (!explore.truncated)
326306
+ return fallback;
326307
+ if (explore.truncatedTransient)
326308
+ return "rate_limited";
326309
+ if (explore.truncatedReason === "timeout")
326310
+ return "timeout";
326311
+ return "error";
326312
+ }
325268
326313
  function buildLoginCaptureSystemPrompt(baseUrl, availableKeys, maxIterations = getLoginCaptureMaxIterations()) {
325269
326314
  const keyLines = availableKeys.map((k) => `- \`${k}\``).join("\n");
325270
326315
  return `You are a login specialist. Your SINGLE objective: navigate to the app, log in with the provided test credentials, and confirm you are authenticated. Then stop.
@@ -325288,6 +326333,7 @@ ${keyLines || "- (none provided)"}
325288
326333
  ## Hard rules
325289
326334
  - Do NOT submit registration, password-reset, or any other forms. Login only.
325290
326335
  - Do NOT fill validation / negative values. Happy-path login only.
326336
+ - If login demands an emailed verification code, one-time password (OTP), or magic link, STOP \u2014 you have no inbox access and NEVER type a guessed code (any code you type is fabricated). Call \`capture_page\` on the verification wall noting it as the blocker, then \`finish_exploration\`.
325291
326337
  - Use RELATIVE routes in capture_page (e.g. \`/login\`, \`/dashboard\`), never absolute URLs.
325292
326338
  - If login fails after 2 attempts, call \`capture_page\` noting the failure and call \`finish_exploration\`.
325293
326339
  - You have a hard limit of ${maxIterations} iterations. Finish well before then.`;
@@ -325458,17 +326504,17 @@ ${keyLines || "- (none provided)"}
325458
326504
  }
325459
326505
  return false;
325460
326506
  }
325461
- async function runLoginCaptureRun(args) {
326507
+ async function runLoginCaptureRunWithOutcome(args) {
325462
326508
  const { loginBrowser, baseUrl, projectId, model, onAudit, logs: logs2 } = args;
325463
326509
  const credentials = filterLoginCaptureCredentials(args.credentials);
325464
326510
  if ((0, agentic_cli_constants_js_1.getAgenticEngine)(model) !== "native") {
325465
326511
  logs2(` auth-capture skipped: engine for ${model} is not native (V1 is native-engine-only)`);
325466
- return null;
326512
+ return { recipe: null, outcome: "skipped" };
325467
326513
  }
325468
326514
  const { secrets } = (0, credential_tools_js_1.partitionTestVariables)(credentials);
325469
326515
  if (Object.keys(secrets).length === 0) {
325470
326516
  logs2(" auth-capture skipped: no secret credentials available to log in with");
325471
- return null;
326517
+ return { recipe: null, outcome: "skipped" };
325472
326518
  }
325473
326519
  const auditRemap = onAudit ? (info) => {
325474
326520
  try {
@@ -325489,7 +326535,7 @@ ${keyLines || "- (none provided)"}
325489
326535
  const mcpClient = handle.mcpClient;
325490
326536
  if (!mcpClient) {
325491
326537
  logs2(" auth-capture aborted: instrumented context has no MCP client");
325492
- return null;
326538
+ return { recipe: null, outcome: "skipped" };
325493
326539
  }
325494
326540
  const availableKeys = Object.keys(credentials);
325495
326541
  const maxIterations = getLoginCaptureMaxIterations();
@@ -325499,8 +326545,8 @@ ${keyLines || "- (none provided)"}
325499
326545
  enableRecordJourney: false,
325500
326546
  // login-only — no feature journeys to record
325501
326547
  maxIterationsOverride: maxIterations,
325502
- rateLimitMaxRetries: CAPTURE_RATE_LIMIT_MAX_RETRIES,
325503
- rateLimitMaxDelayMs: CAPTURE_RATE_LIMIT_MAX_DELAY_MS
326548
+ rateLimitMaxRetries: getCaptureRateLimitMaxRetries(),
326549
+ rateLimitMaxDelayMs: getCaptureRateLimitMaxDelayMs()
325504
326550
  };
325505
326551
  const exploreResult = await (0, discover_explorer_js_1.runExplorePhase)(mcpClient, baseUrl, {
325506
326552
  publicTestVariables: credentials,
@@ -325515,6 +326561,7 @@ ${keyLines || "- (none provided)"}
325515
326561
  onProgress: void 0
325516
326562
  }, loginConfig);
325517
326563
  logs2(` auth-capture explore: ${exploreResult.pagesDiscovered} pages, ${exploreResult.collectedPages.length} captured`);
326564
+ const exploreMiss = (fallback) => classifyCaptureMiss(exploreResult, fallback);
325518
326565
  const loginObservedOrigins = exploreResult.observedOrigins ?? [];
325519
326566
  try {
325520
326567
  args.onObservedOrigins?.(loginObservedOrigins);
@@ -325530,7 +326577,7 @@ ${keyLines || "- (none provided)"}
325530
326577
  const login = (0, discover_generator_js_1.getLoginCommands)(loginSiteMap.pages, credentials);
325531
326578
  if (!login) {
325532
326579
  logs2(" \u26A0 auth unverified (login run produced no derivable login sequence) \u2014 falling back to model-authored login");
325533
- return null;
326580
+ return { recipe: null, outcome: exploreMiss("no_login") };
325534
326581
  }
325535
326582
  const scrubbedLogin = {
325536
326583
  ...login,
@@ -325540,19 +326587,19 @@ ${keyLines || "- (none provided)"}
325540
326587
  const authProof = harvestAuthProofLive(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route, credentials, logs2);
325541
326588
  if (!authProof) {
325542
326589
  logs2(" \u26A0 auth unverified (no durable signed-in proof on the authenticated page) \u2014 falling back to model-authored login");
325543
- return null;
326590
+ return { recipe: null, outcome: exploreMiss("verify_failed") };
325544
326591
  }
325545
326592
  const candidate = { ...(0, auth_recipe_js_1.buildAuthRecipe)(scrubbedLogin, authProof, (/* @__PURE__ */ new Date()).toISOString()), appOrigin: loginRedirectAppOrigin };
325546
326593
  const rendered = (0, auth_recipe_js_1.renderAuthFn)(candidate);
325547
326594
  const leaks = findCredentialLiteralLeaks(rendered, credentials);
325548
326595
  if (leaks.length > 0) {
325549
326596
  logs2(` \u26A0 auth recipe rejected: rendered __vqaLogin contains literal credential value(s) for ${leaks.join(", ")} \u2014 falling back to model-authored login`);
325550
- return null;
326597
+ return { recipe: null, outcome: "credential_leak" };
325551
326598
  }
325552
326599
  const planLeaks = findCredentialLiteralLeaks(JSON.stringify(candidate.replayPlan ?? []), credentials);
325553
326600
  if (planLeaks.length > 0) {
325554
326601
  logs2(` \u26A0 auth recipe rejected: structured replayPlan contains literal credential value(s) for ${planLeaks.join(", ")} \u2014 falling back to model-authored login`);
325555
- return null;
326602
+ return { recipe: null, outcome: "credential_leak" };
325556
326603
  }
325557
326604
  try {
325558
326605
  await handle.close();
@@ -325562,14 +326609,14 @@ ${keyLines || "- (none provided)"}
325562
326609
  const ok = await verifyAuthRecipe(candidate, baseUrl, credentials, logs2);
325563
326610
  if (!ok) {
325564
326611
  logs2(" \u26A0 auth unverified (live-verify replay failed \u2014 fleet-config drift) \u2014 falling back to model-authored login");
325565
- return null;
326612
+ return { recipe: null, outcome: exploreMiss("verify_failed") };
325566
326613
  }
325567
326614
  const proofDesc = authProof.kind === "role" ? `role:${authProof.role}${authProof.name ? `(${authProof.name})` : ""}` : authProof.kind === "testid" ? `testid:${authProof.testId}` : `url:${authProof.route}`;
325568
326615
  logs2(` \u2713 auth recipe verified (hash ${candidate.recipeHash}, proof ${proofDesc}, ${(candidate.replayPlan ?? []).length} replay steps) \u2014 stamping into @needs-auth tests`);
325569
- return candidate;
326616
+ return { recipe: candidate, outcome: "captured" };
325570
326617
  } catch (err) {
325571
326618
  logs2(` \u26A0 auth-capture run failed: ${err instanceof Error ? err.message : String(err)} \u2014 falling back to model-authored login`);
325572
- return null;
326619
+ return { recipe: null, outcome: (0, ai_retry_js_1.isTransientRateError)(err) ? "rate_limited" : "error" };
325573
326620
  } finally {
325574
326621
  if (handle) {
325575
326622
  try {
@@ -325579,6 +326626,9 @@ ${keyLines || "- (none provided)"}
325579
326626
  }
325580
326627
  }
325581
326628
  }
326629
+ async function runLoginCaptureRun(args) {
326630
+ return (await runLoginCaptureRunWithOutcome(args)).recipe;
326631
+ }
325582
326632
  var DEFAULT_MAX_REGISTER_CAPTURE_ITERATIONS = 18;
325583
326633
  function getRegisterCaptureMaxIterations(env = process.env) {
325584
326634
  return readPositiveIntEnv2(env, "AUTH_CAPTURE_REGISTER_MAX_ITERATIONS", DEFAULT_MAX_REGISTER_CAPTURE_ITERATIONS);
@@ -325650,8 +326700,8 @@ ${keyLines || "- (none provided)"}
325650
326700
  userMessage: buildRegisterCaptureUserMessage(baseUrl),
325651
326701
  enableRecordJourney: false,
325652
326702
  maxIterationsOverride: maxIterations,
325653
- rateLimitMaxRetries: CAPTURE_RATE_LIMIT_MAX_RETRIES,
325654
- rateLimitMaxDelayMs: CAPTURE_RATE_LIMIT_MAX_DELAY_MS,
326703
+ rateLimitMaxRetries: getCaptureRateLimitMaxRetries(),
326704
+ rateLimitMaxDelayMs: getCaptureRateLimitMaxDelayMs(),
325655
326705
  // Register + poll the PROJECT-SCOPED address so it matches the one the
325656
326706
  // standalone authored tests log in with (see #922).
325657
326707
  inboxTagOverride: args.inboxTag
@@ -332383,6 +333433,8 @@ var require_discover_agent = __commonJS({
332383
333433
  Object.defineProperty(exports2, "__esModule", { value: true });
332384
333434
  exports2.runMobileDiscoveryOnRunner = exports2.buildInMemorySiteMap = void 0;
332385
333435
  exports2.runCanvasPreflight = runCanvasPreflight;
333436
+ exports2.runCaptureAttemptLoop = runCaptureAttemptLoop;
333437
+ exports2.isCaptureRunRetryEnabled = isCaptureRunRetryEnabled;
332386
333438
  exports2.getFatalExploreError = getFatalExploreError;
332387
333439
  exports2.runSiteDiscoveryOnRunner = runSiteDiscoveryOnRunner2;
332388
333440
  exports2.runFeatureDiscoveryOnRunner = runFeatureDiscoveryOnRunner2;
@@ -332631,8 +333683,56 @@ Respond with ONLY valid JSON:
332631
333683
  }
332632
333684
  return merged;
332633
333685
  }
332634
- var AUTH_CAPTURE_TIMEOUT_MS = 4 * 60 * 1e3;
332635
- function maybeStartLoginCaptureRun(ctx, callbackOptions, onAICall, pushLog, onObservedOrigins) {
333686
+ function computeCaptureRetryBackoffMs(attempt) {
333687
+ const capped = Math.min(3e3 * Math.pow(2, attempt - 1), 15e3);
333688
+ const jitter = 0.5 + Math.random() * 0.5;
333689
+ return Math.round(capped * jitter);
333690
+ }
333691
+ async function runCaptureAttemptLoop(args) {
333692
+ const { runCapture, maxAttempts, deadline, minRetryBudgetMs, now, sleep, backoffMs, log: log2, onAttempt } = args;
333693
+ let attemptsMade = 0;
333694
+ let lastOutcome = "error";
333695
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
333696
+ if (now() >= deadline) {
333697
+ lastOutcome = "timeout";
333698
+ break;
333699
+ }
333700
+ attemptsMade = attempt;
333701
+ onAttempt?.(attempt);
333702
+ const { recipe, outcome } = await runCapture(attempt);
333703
+ lastOutcome = outcome;
333704
+ if (recipe)
333705
+ return { recipe, outcome, attempts: attemptsMade };
333706
+ if (attempt >= maxAttempts || outcome !== "rate_limited")
333707
+ break;
333708
+ const remaining = deadline - now();
333709
+ if (remaining < minRetryBudgetMs) {
333710
+ log2(`auth-prologue: transient rate-limit miss, but only ${Math.round(remaining / 1e3)}s of the capture wall remain \u2014 not retrying`);
333711
+ break;
333712
+ }
333713
+ const backoff = Math.min(backoffMs(attempt), remaining - minRetryBudgetMs);
333714
+ if (backoff > 0) {
333715
+ log2(`auth-prologue: login-capture hit a transient rate limit (attempt ${attempt}/${maxAttempts}) \u2014 retrying in ${Math.round(backoff / 1e3)}s`);
333716
+ await sleep(backoff);
333717
+ }
333718
+ if (deadline - now() < minRetryBudgetMs) {
333719
+ log2("auth-prologue: capture wall nearly exhausted after backoff \u2014 not retrying");
333720
+ break;
333721
+ }
333722
+ }
333723
+ return { recipe: null, outcome: lastOutcome, attempts: attemptsMade };
333724
+ }
333725
+ function logAuthGenerationStats(logs2, tests, authRecipe) {
333726
+ const needsAuth = tests.filter((t) => t.tags.includes("@needs-auth"));
333727
+ if (needsAuth.length === 0)
333728
+ return;
333729
+ const recipeStamped = needsAuth.filter((t) => /VALIDATE_QA_AUTH/.test(t.playwrightCode ?? "")).length;
333730
+ logs2.push(`[auth-capture] needsAuthTests=${needsAuth.length} recipeStamped=${recipeStamped} inlineLogin=${needsAuth.length - recipeStamped} recipeAvailable=${authRecipe ? "yes" : "no"}`);
333731
+ }
333732
+ function isCaptureRunRetryEnabled(env = process.env) {
333733
+ return (0, shared_1.parseBoolFlag)(env.AUTH_CAPTURE_RUN_RETRY, true);
333734
+ }
333735
+ function maybeStartLoginCaptureRun(ctx, callbackOptions, onAICall, pushLog, onObservedOrigins, onOutcome) {
332636
333736
  if (!(0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) && ctx.mode !== "capture")
332637
333737
  return null;
332638
333738
  if (ctx.authRecipe) {
@@ -332657,25 +333757,44 @@ Respond with ONLY valid JSON:
332657
333757
  return null;
332658
333758
  }
332659
333759
  const credentials = (0, auth_capture_run_js_1.buildLoginCaptureCredentials)(ctx.variableContext);
333760
+ const runStart = Date.now();
333761
+ const timeoutMs = (0, auth_capture_run_js_1.getAuthCaptureTimeoutMs)();
333762
+ const deadline = runStart + timeoutMs;
333763
+ const retryEnabled = isCaptureRunRetryEnabled();
333764
+ const maxAttempts = retryEnabled ? (0, auth_capture_run_js_1.getAuthCaptureRunRetryMaxAttempts)() : 1;
333765
+ const MIN_RETRY_BUDGET_MS = 45e3;
333766
+ let attemptsMade = 0;
332660
333767
  const run2 = (async () => {
332661
333768
  let lease = null;
332662
333769
  try {
332663
333770
  lease = await acquire();
332664
- const recipe = await (0, auth_capture_run_js_1.runLoginCaptureRun)({
332665
- loginBrowser: lease.browser,
332666
- baseUrl: ctx.baseUrl,
332667
- credentials,
332668
- projectId: ctx.projectId,
332669
- model,
332670
- existingSiteMap: ctx.existingSiteMap,
332671
- onAudit: onAICall,
332672
- onObservedOrigins,
332673
- logs: pushLog
333771
+ const leased = lease;
333772
+ return await runCaptureAttemptLoop({
333773
+ maxAttempts,
333774
+ deadline,
333775
+ minRetryBudgetMs: MIN_RETRY_BUDGET_MS,
333776
+ now: Date.now,
333777
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
333778
+ backoffMs: computeCaptureRetryBackoffMs,
333779
+ log: pushLog,
333780
+ onAttempt: (n) => {
333781
+ attemptsMade = n;
333782
+ },
333783
+ runCapture: () => (0, auth_capture_run_js_1.runLoginCaptureRunWithOutcome)({
333784
+ loginBrowser: leased.browser,
333785
+ baseUrl: ctx.baseUrl,
333786
+ credentials,
333787
+ projectId: ctx.projectId,
333788
+ model,
333789
+ existingSiteMap: ctx.existingSiteMap,
333790
+ onAudit: onAICall,
333791
+ onObservedOrigins,
333792
+ logs: pushLog
333793
+ })
332674
333794
  });
332675
- return recipe;
332676
333795
  } catch (err) {
332677
333796
  pushLog(`auth-prologue: login-capture run threw (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
332678
- return null;
333797
+ return { recipe: null, outcome: "error", attempts: attemptsMade };
332679
333798
  } finally {
332680
333799
  if (lease) {
332681
333800
  try {
@@ -332688,16 +333807,19 @@ Respond with ONLY valid JSON:
332688
333807
  let timer;
332689
333808
  const timeout = new Promise((resolve2) => {
332690
333809
  timer = setTimeout(() => {
332691
- pushLog(`auth-prologue: login-capture run exceeded ${AUTH_CAPTURE_TIMEOUT_MS / 1e3}s \u2014 proceeding without a recipe`);
332692
- resolve2(null);
332693
- }, AUTH_CAPTURE_TIMEOUT_MS);
333810
+ pushLog(`auth-prologue: login-capture run exceeded ${Math.round(timeoutMs / 1e3)}s \u2014 proceeding without a recipe`);
333811
+ resolve2({ recipe: null, outcome: "timeout", attempts: attemptsMade });
333812
+ }, Math.max(0, deadline - Date.now()));
332694
333813
  timer.unref?.();
332695
333814
  });
332696
- const timed = Promise.race([run2, timeout]).finally(() => {
333815
+ return Promise.race([run2, timeout]).finally(() => {
332697
333816
  if (timer)
332698
333817
  clearTimeout(timer);
332699
- });
332700
- return timed.catch(() => null);
333818
+ }).then((res) => {
333819
+ pushLog(`[auth-capture] outcome=${res.outcome} attempts=${res.attempts} durationMs=${Date.now() - runStart} mode=${ctx.mode ?? "discovery"}`);
333820
+ onOutcome?.(res.outcome);
333821
+ return res.recipe;
333822
+ }).catch(() => null);
332701
333823
  }
332702
333824
  function getFatalExploreError(exploreResult) {
332703
333825
  const prefix = "Fatal error:";
@@ -332712,12 +333834,15 @@ Respond with ONLY valid JSON:
332712
333834
  logs2.push(`Base URL: ${ctx.baseUrl}`);
332713
333835
  onLog?.("starting", logs2.join("\n"));
332714
333836
  const loginObservedOrigins = [];
333837
+ let captureOutcome;
332715
333838
  const loginRun = maybeStartLoginCaptureRun(ctx, callbackOptions, onAICall, (line) => {
332716
333839
  logs2.push(line);
332717
333840
  onLog?.("progress", logs2.join("\n"));
332718
333841
  }, (origins) => {
332719
333842
  for (const origin of origins)
332720
333843
  loginObservedOrigins.push(origin);
333844
+ }, (outcome) => {
333845
+ captureOutcome = outcome;
332721
333846
  });
332722
333847
  const authRecipe = loginRun ? await loginRun : null;
332723
333848
  const duration = (Date.now() - startTime) / 1e3;
@@ -332742,7 +333867,7 @@ Respond with ONLY valid JSON:
332742
333867
  },
332743
333868
  duration,
332744
333869
  mode: "capture",
332745
- ...authRecipe ? {} : { error: "Login capture did not produce a verified recipe" }
333870
+ ...authRecipe ? {} : { error: `Login capture did not produce a verified recipe${captureOutcome ? ` (${captureOutcome})` : ""}` }
332746
333871
  };
332747
333872
  }
332748
333873
  const mode = ctx.mode;
@@ -332922,14 +334047,18 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
332922
334047
  const updatedSiteMap = (0, sitemap_builder_js_1.buildInMemorySiteMap)(ctx.projectId, ctx.baseUrl, exploreResult.collectedPages, exploreResult.collectedTransitions, ctx.existingSiteMap);
332923
334048
  if (updatedSiteMap.pages.length === 0) {
332924
334049
  logs2.push("No site map data available - skipping plan and generate phases");
332925
- if (loginRunP)
332926
- await loginRunP;
334050
+ const earlyAuthRecipe = loginRunP ? await loginRunP : null;
332927
334051
  const duration2 = (Date.now() - startTime) / 1e3;
332928
334052
  const fatalExploreError = getFatalExploreError(exploreResult);
332929
334053
  return {
332930
334054
  collectedPages: exploreResult.collectedPages,
332931
334055
  collectedTransitions: exploreResult.collectedTransitions,
332932
334056
  tests: [],
334057
+ // Persist only a FRESHLY-captured recipe — never re-persist a reused cached one
334058
+ // (ctx.authRecipe set) or a late warm run could overwrite/re-verify a newer recipe.
334059
+ authRecipe: ctx.authRecipe ? void 0 : earlyAuthRecipe,
334060
+ observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
334061
+ resolvedAppOrigin: earlyAuthRecipe?.appOrigin ?? void 0,
332933
334062
  healthObservations: healthObserver?.getSummary(),
332934
334063
  logs: logs2.join("\n"),
332935
334064
  screenshots,
@@ -333017,12 +334146,6 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
333017
334146
  logs2.push(surveyHealth.totalIssues > 0 ? `
333018
334147
  Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.length} console errors, ${surveyHealth.failedRequests.length} failed requests` : "\nHealth observer: no issues detected");
333019
334148
  }
333020
- const duration2 = (Date.now() - startTime) / 1e3;
333021
- const featureCount = featureGroups?.length || finalPlan.testSuites.length;
333022
- const summary2 = `Survey complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elementsFound} elements, ${featureCount} features identified in ${duration2.toFixed(1)}s`;
333023
- logs2.push(`
333024
- === Survey Complete (${duration2.toFixed(1)}s) ===`);
333025
- logs2.push(summary2);
333026
334149
  let mergedWarning = planResult.warning;
333027
334150
  if (exploreResult.truncated) {
333028
334151
  const reasonLabel = exploreResult.truncatedReason === "rate_limit" ? "AI rate-limit" : exploreResult.truncatedReason === "timeout" ? "AI timeout" : "AI error";
@@ -333043,6 +334166,12 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
333043
334166
  if (surveyAuthRecipe) {
333044
334167
  logs2.push(`Auth-prologue: captured + verified login recipe ${surveyAuthRecipe.recipeHash} during survey \u2014 persisting for reuse on later runs`);
333045
334168
  }
334169
+ const duration2 = (Date.now() - startTime) / 1e3;
334170
+ const featureCount = featureGroups?.length || finalPlan.testSuites.length;
334171
+ const summary2 = `Survey complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elementsFound} elements, ${featureCount} features identified in ${duration2.toFixed(1)}s`;
334172
+ logs2.push(`
334173
+ === Survey Complete (${duration2.toFixed(1)}s) ===`);
334174
+ logs2.push(summary2);
333046
334175
  const plannerFailedError = planResult.plannerFailed ? `Survey planning failed (${planResult.plannerFailureKind ?? "unknown"}): ${planResult.fallbackReason ?? "planner did not produce a usable plan"}. Explore evidence was saved; re-run the survey to retry planning.` : void 0;
333047
334176
  return {
333048
334177
  observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
@@ -333105,6 +334234,7 @@ Generation complete: ${generateResult.tests.length} tests generated (${generateE
333105
334234
  const tags = t.tags.join(", ");
333106
334235
  logs2.push(` ${i + 1}. ${t.name} [${tags}]${t.verified ? " \u2713live" : ""}`);
333107
334236
  });
334237
+ logAuthGenerationStats(logs2, generateResult.tests, authRecipe);
333108
334238
  onLog?.("generate", logs2.join("\n"));
333109
334239
  const postGenClose = await handle.close();
333110
334240
  handle = null;
@@ -333474,8 +334604,7 @@ Feature explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResul
333474
334604
  }
333475
334605
  if (!isPlanReuseResume && updatedSiteMap.pages.length === 0) {
333476
334606
  logs2.push("No site map data - skipping plan and generate phases");
333477
- if (loginRunP)
333478
- await loginRunP;
334607
+ const earlyAuthRecipe = loginRunP ? await loginRunP : null;
333479
334608
  const earlyClose = await handle.close();
333480
334609
  handle = null;
333481
334610
  discoveryApiCalls = earlyClose.apiCalls;
@@ -333486,6 +334615,11 @@ Feature explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResul
333486
334615
  collectedPages: exploreResult.collectedPages,
333487
334616
  collectedTransitions: exploreResult.collectedTransitions,
333488
334617
  tests: [],
334618
+ // Persist only a FRESHLY-captured recipe — never re-persist a reused cached one
334619
+ // (ctx.authRecipe set) or a late warm run could overwrite/re-verify a newer recipe.
334620
+ authRecipe: ctx.authRecipe ? void 0 : earlyAuthRecipe,
334621
+ observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
334622
+ resolvedAppOrigin: earlyAuthRecipe?.appOrigin ?? void 0,
333489
334623
  logs: logs2.join("\n"),
333490
334624
  screenshots,
333491
334625
  apiCalls: discoveryApiCalls.length > 0 ? discoveryApiCalls : void 0,
@@ -333626,6 +334760,7 @@ Generation complete: ${generateResult.tests.length} tests (${generateElapsed}s e
333626
334760
  const tags = t.tags.join(", ");
333627
334761
  logs2.push(` ${i + 1}. ${t.name} [${tags}]${t.verified ? " \u2713live" : ""}`);
333628
334762
  });
334763
+ logAuthGenerationStats(logs2, generateResult.tests, authRecipe);
333629
334764
  onLog?.("generate", logs2.join("\n"));
333630
334765
  const postGenClose = await handle.close();
333631
334766
  handle = null;
@@ -346386,7 +347521,6 @@ var require_repo_analysis_runner = __commonJS({
346386
347521
  const softTestTarget = intEnv("REPO_ANALYSIS_SOFT_TEST_TARGET", 20, 1, 50);
346387
347522
  const maxEndpointsPerFeature = intEnv("REPO_ANALYSIS_MAX_ENDPOINTS_PER_FEATURE", 80, 1, 400);
346388
347523
  const maxFacts = intEnv("REPO_ANALYSIS_MAX_FACTS", 40, 1, 120);
346389
- const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
346390
347524
  const provider = "anthropic";
346391
347525
  const model = agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL;
346392
347526
  log2(`=== Repo Analysis Runner (Claude CLI, no browser${isMobileAnalysis ? ", mobile\u2192Appium" : ", web\u2192Playwright"}) ===`);
@@ -348090,7 +349224,7 @@ var require_package4 = __commonJS({
348090
349224
  "package.json"(exports2, module2) {
348091
349225
  module2.exports = {
348092
349226
  name: "@validate.qa/runner",
348093
- version: "1.0.52",
349227
+ version: "1.0.54",
348094
349228
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
348095
349229
  bin: {
348096
349230
  "validate-runner": "dist/cli.js"