@jevitate/cli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -4003,7 +4003,7 @@ function handleIntersectionResults(result, left, right) {
4003
4003
  const unrecKeys = /* @__PURE__ */ new Map();
4004
4004
  let unrecIssue;
4005
4005
  const keyIssues = /* @__PURE__ */ new Map();
4006
- const collect6 = (iss, side2) => {
4006
+ const collect7 = (iss, side2) => {
4007
4007
  let keys;
4008
4008
  if (iss.code === "unrecognized_keys" && !iss.path?.length) {
4009
4009
  unrecIssue ?? (unrecIssue = iss);
@@ -4024,11 +4024,11 @@ function handleIntersectionResults(result, left, right) {
4024
4024
  return true;
4025
4025
  };
4026
4026
  for (const iss of left.issues) {
4027
- if (!collect6(iss, "l"))
4027
+ if (!collect7(iss, "l"))
4028
4028
  result.issues.push(iss);
4029
4029
  }
4030
4030
  for (const iss of right.issues) {
4031
- if (!collect6(iss, "r"))
4031
+ if (!collect7(iss, "r"))
4032
4032
  result.issues.push(iss);
4033
4033
  }
4034
4034
  const bothKeys = [...unrecKeys].filter(([, f]) => f.l && f.r).map(([k]) => k);
@@ -26829,6 +26829,43 @@ var clock = {
26829
26829
  sleep: (ms) => new Promise((resolve14) => current.setTimeout(() => resolve14(), ms))
26830
26830
  };
26831
26831
 
26832
+ // ../domain/dist/external-scheme.js
26833
+ var IN_BROWSER_SCHEMES = /* @__PURE__ */ new Set([
26834
+ "http",
26835
+ "https",
26836
+ "ws",
26837
+ "wss",
26838
+ "data",
26839
+ "blob",
26840
+ "about",
26841
+ "file",
26842
+ "javascript",
26843
+ "filesystem",
26844
+ "chrome",
26845
+ "chrome-extension",
26846
+ "chrome-error",
26847
+ "chrome-untrusted",
26848
+ "devtools",
26849
+ "edge",
26850
+ "view-source",
26851
+ "moz-extension",
26852
+ "resource"
26853
+ ]);
26854
+ var SCHEME = /^([a-z][a-z0-9+.-]*):/i;
26855
+ function urlSchemeOf(url2) {
26856
+ const m = SCHEME.exec(url2.trim());
26857
+ return m?.[1]?.toLowerCase();
26858
+ }
26859
+ function externalSchemeOf(url2) {
26860
+ const scheme = urlSchemeOf(url2);
26861
+ if (scheme === void 0 || IN_BROWSER_SCHEMES.has(scheme))
26862
+ return void 0;
26863
+ return scheme;
26864
+ }
26865
+ function isExternalSchemeUrl(url2) {
26866
+ return externalSchemeOf(url2) !== void 0;
26867
+ }
26868
+
26832
26869
  // ../daemon/dist/profile-manager.js
26833
26870
  var ProfileManager = class {
26834
26871
  rootDir;
@@ -34378,10 +34415,8 @@ async function evaluateAssertionOnce(actor, a) {
34378
34415
  const page = actor.ability(BrowseTheWebToken).session.page;
34379
34416
  return page.url().includes(a.text);
34380
34417
  }
34381
- case "textIncludes": {
34382
- const text2 = await actor.asks(TextOf.target(descriptorToTarget(a.target)));
34383
- return text2 !== null && textIncludesCI(text2, a.text);
34384
- }
34418
+ case "textIncludes":
34419
+ return textIncludesOnAnyMatch(actor, a.target, a.text);
34385
34420
  case "count": {
34386
34421
  const n3 = await actor.asks(CountOf.target(descriptorToTarget(a.target)));
34387
34422
  return (a.min === void 0 || n3 >= a.min) && (a.max === void 0 || n3 <= a.max);
@@ -34390,6 +34425,29 @@ async function evaluateAssertionOnce(actor, a) {
34390
34425
  return await actor.asks(ValueOf.target(descriptorToTarget(a.target))) === a.value;
34391
34426
  }
34392
34427
  }
34428
+ var MAX_TEXT_MATCHES = 50;
34429
+ async function textIncludesOnAnyMatch(actor, target, text2) {
34430
+ const t = descriptorToTarget(target);
34431
+ const locator = t.resolve(actor.ability(BrowseTheWebToken).session.page);
34432
+ const n3 = await locator.count();
34433
+ if (n3 === 0)
34434
+ return false;
34435
+ if (n3 === 1) {
34436
+ if (!await locator.isVisible().catch(() => false))
34437
+ return false;
34438
+ const read = await actor.asks(TextOf.target(t));
34439
+ return read !== null && textIncludesCI(read, text2);
34440
+ }
34441
+ for (let i = 0; i < Math.min(n3, MAX_TEXT_MATCHES); i++) {
34442
+ const el = locator.nth(i);
34443
+ if (!await el.isVisible().catch(() => false))
34444
+ continue;
34445
+ const read = await el.innerText({ timeout: 1e3 }).catch(() => null);
34446
+ if (read !== null && textIncludesCI(read, text2))
34447
+ return true;
34448
+ }
34449
+ return false;
34450
+ }
34393
34451
  async function readAssertionText(actor, a) {
34394
34452
  if (a.kind === "textIncludes")
34395
34453
  return actor.asks(TextOf.target(descriptorToTarget(a.target))).catch(() => null);
@@ -35895,6 +35953,68 @@ function srOnlyLabelOf(node2) {
35895
35953
  return { none: true };
35896
35954
  }
35897
35955
 
35956
+ // ../explore/dist/composer-evidence.js
35957
+ var MESSAGE_FIELD = /\b(reply|message|ask|chat|inquiry|prompt|say|talk|conversation)\b/i;
35958
+ var MESSAGE_INPUT_TYPES = /* @__PURE__ */ new Set(["", "text", "search"]);
35959
+ var SUBMIT_NAME = /\b(send|submit|reply|ask|post)\b|[→➤➔↑]|^\s*(go|ok)\s*$/i;
35960
+ var PAIRED_SUBMIT_DISTANCE = 6;
35961
+ function composerEvidence(node2, args) {
35962
+ const el = node2;
35963
+ const norm5 = (s3) => s3 === null ? "" : s3.replace(/\s+/g, " ").trim();
35964
+ const parentOf = (a) => a.parentElement ?? (a.parentNode instanceof ShadowRoot ? a.parentNode.host : null);
35965
+ const rendered = (a) => a.getClientRects().length > 0 || a.offsetParent !== null;
35966
+ const roots = [document];
35967
+ const own2 = el.getRootNode();
35968
+ if (own2 instanceof ShadowRoot)
35969
+ roots.push(own2);
35970
+ const all = (sel) => roots.flatMap((r) => Array.from(r.querySelectorAll(sel)));
35971
+ if (all('[role="log"]').some(rendered))
35972
+ return true;
35973
+ for (const live of all('[aria-live="polite"], [aria-live="assertive"]')) {
35974
+ const role = norm5(live.getAttribute("role")).toLowerCase();
35975
+ if (role === "status" || role === "alert" || role === "timer" || role === "progressbar")
35976
+ continue;
35977
+ if (rendered(live) && live.children.length >= 2)
35978
+ return true;
35979
+ }
35980
+ const MESSAGE_ITEM = /(^|[\s_-])(message|msg|bubble|chat-?message|chat-?bubble)s?($|[\s_-])/i;
35981
+ const itemLike = (a) => MESSAGE_ITEM.test(a.getAttribute("class") ?? "") || Array.from(a.attributes).some((at3) => at3.name.startsWith("data-") && MESSAGE_ITEM.test(`${at3.name.slice(5)} ${at3.value}`));
35982
+ const parents = /* @__PURE__ */ new Map();
35983
+ for (const item of all("[class], [data-role], [data-type], [data-message-id], [data-message-author-role]")) {
35984
+ if (item === el || item.contains(el) || !itemLike(item) || item.parentElement === null || !rendered(item))
35985
+ continue;
35986
+ const n3 = (parents.get(item.parentElement) ?? 0) + 1;
35987
+ if (n3 >= 2)
35988
+ return true;
35989
+ parents.set(item.parentElement, n3);
35990
+ }
35991
+ const submit2 = new RegExp(args.submit, "i");
35992
+ const up4 = (a) => {
35993
+ const out = [];
35994
+ for (let cur = a; cur !== null; cur = parentOf(cur))
35995
+ out.push(cur);
35996
+ return out;
35997
+ };
35998
+ const fieldPath = up4(el);
35999
+ for (const b2 of all('button, [role="button"], input[type="submit"], input[type="button"]')) {
36000
+ if (b2 === el || !rendered(b2))
36001
+ continue;
36002
+ const name = norm5(b2.getAttribute("aria-label")) || norm5(b2.textContent) || norm5(b2.value ?? null) || norm5(b2.getAttribute("title"));
36003
+ if (!submit2.test(name))
36004
+ continue;
36005
+ const bp = up4(b2);
36006
+ for (let i = 0; i < fieldPath.length && i <= args.distance; i++) {
36007
+ const j = bp.indexOf(fieldPath[i]);
36008
+ if (j >= 0) {
36009
+ if (i + j <= args.distance)
36010
+ return true;
36011
+ break;
36012
+ }
36013
+ }
36014
+ }
36015
+ return false;
36016
+ }
36017
+
35898
36018
  // ../explore/dist/temporal-value.js
35899
36019
  var TEMPORAL_FORMATS = {
35900
36020
  time: "HH:MM (24-hour, e.g. 08:00 or 17:30)",
@@ -36034,6 +36154,24 @@ function inWireFormat(field3, value) {
36034
36154
  }
36035
36155
 
36036
36156
  // ../explore/dist/snapshot.js
36157
+ var NAVIGATION_RETRIES = 3;
36158
+ var NAVIGATION_WAIT_MS = 15e3;
36159
+ var PageNavigatingError = class extends Error {
36160
+ url;
36161
+ attempts;
36162
+ name = "PageNavigatingError";
36163
+ constructor(url2, attempts, cause) {
36164
+ super(`the page kept navigating: its controls could not be read in ${attempts} attempts (last at ${redactUrl(url2)})`, { cause });
36165
+ this.url = url2;
36166
+ this.attempts = attempts;
36167
+ }
36168
+ };
36169
+ function isNavigationInterruption(e) {
36170
+ const message2 = e instanceof Error ? e.message : String(e);
36171
+ if (/Target page, context or browser has been closed|Target closed/i.test(message2))
36172
+ return false;
36173
+ return /Execution context was destroyed|Cannot find context with specified id/.test(message2);
36174
+ }
36037
36175
  var SNAPSHOT_BUDGET_MS = 3e4;
36038
36176
  var LIST_OPTION_CAP = 25;
36039
36177
  var LIST_OPTION_ROLES = /* @__PURE__ */ new Set(["option", "menuitem", "menuitemradio", "menuitemcheckbox", "treeitem"]);
@@ -36360,6 +36498,25 @@ function readCheapNames(els) {
36360
36498
  });
36361
36499
  }
36362
36500
  async function snapshot(page, opts) {
36501
+ const retries = opts?.navigationRetries ?? NAVIGATION_RETRIES;
36502
+ const deadline = clock.now() + (opts?.navigationWaitMs ?? NAVIGATION_WAIT_MS);
36503
+ const awaitNavigation = opts?.awaitNavigation ?? (async (remainingMs) => {
36504
+ await page.waitForLoadState("load", { timeout: Math.max(1, remainingMs) }).catch(() => void 0);
36505
+ });
36506
+ for (let attempt2 = 1; ; attempt2 += 1) {
36507
+ try {
36508
+ return await readSnapshot(page, opts);
36509
+ } catch (e) {
36510
+ if (!isNavigationInterruption(e))
36511
+ throw e;
36512
+ const remaining = deadline - clock.now();
36513
+ if (attempt2 > retries || remaining <= 0 || page.isClosed())
36514
+ throw new PageNavigatingError(page.url(), attempt2, e);
36515
+ await awaitNavigation(remaining);
36516
+ }
36517
+ }
36518
+ }
36519
+ async function readSnapshot(page, opts) {
36363
36520
  const maxCandidates = opts?.maxCandidates ?? DEFAULT_BOUNDS.maxCandidates;
36364
36521
  const url2 = page.url();
36365
36522
  const viewport = page.viewportSize();
@@ -36420,6 +36577,8 @@ async function snapshot(page, opts) {
36420
36577
  continue;
36421
36578
  const value = raw.valueBearing && !isSecretField(raw.inputType, raw.autocomplete) ? await handle.evaluate(readControlValue) : null;
36422
36579
  const facts = { ...raw, value };
36580
+ const textEntry = raw.tag === "textarea" || raw.tag === "input" && MESSAGE_INPUT_TYPES.has(raw.inputType ?? "") || raw.role === "textbox";
36581
+ const conversational = textEntry && MESSAGE_FIELD.test(raw.name) ? await handle.evaluate(composerEvidence, { submit: SUBMIT_NAME.source, distance: PAIRED_SUBMIT_DISTANCE }).catch(() => false) : null;
36423
36582
  const computed = await computeDescriptor(page, handle, { primaryOnly: true });
36424
36583
  const control = {
36425
36584
  index: controls.length,
@@ -36449,11 +36608,14 @@ async function snapshot(page, opts) {
36449
36608
  step: facts.step,
36450
36609
  landmark: facts.landmark,
36451
36610
  ...facts.richText ? { richText: true } : {},
36611
+ ...conversational === null ? {} : { conversational },
36452
36612
  ...facts.clippedOffscreen ? { clippedOffscreen: true } : {}
36453
36613
  };
36454
36614
  controls.push(redactControl(control, opts?.secrets ?? []));
36455
36615
  keptFacts.push(facts);
36456
- } catch {
36616
+ } catch (e) {
36617
+ if (isNavigationInterruption(e))
36618
+ throw e;
36457
36619
  } finally {
36458
36620
  await handle.dispose().catch(() => void 0);
36459
36621
  }
@@ -36560,15 +36722,179 @@ var FirstPartyOrigins = class {
36560
36722
  }
36561
36723
  };
36562
36724
 
36725
+ // ../explore/dist/rpc-status.js
36726
+ var RPC_CONTENT = /^\s*application\/(?:grpc-web(?:-text)?(?:\+[\w.-]+)?|connect\+[\w.-]+)\s*(?:;|$)/i;
36727
+ var GRPC_CODE_NAMES = [
36728
+ "ok",
36729
+ "canceled",
36730
+ "unknown",
36731
+ "invalid_argument",
36732
+ "deadline_exceeded",
36733
+ "not_found",
36734
+ "already_exists",
36735
+ "permission_denied",
36736
+ "resource_exhausted",
36737
+ "failed_precondition",
36738
+ "aborted",
36739
+ "out_of_range",
36740
+ "unimplemented",
36741
+ "internal",
36742
+ "unavailable",
36743
+ "data_loss",
36744
+ "unauthenticated"
36745
+ ];
36746
+ var HTTP_EQUIVALENT = [200, 499, 500, 400, 504, 404, 409, 403, 429, 400, 409, 400, 501, 500, 503, 500, 401];
36747
+ var MAX_RPC_BODY_BYTES = 8 * 1024 * 1024;
36748
+ function statusOf2(protocol, code) {
36749
+ if (!Number.isInteger(code) || code < 0)
36750
+ return null;
36751
+ return { protocol, code, name: GRPC_CODE_NAMES[code] ?? `code_${code}` };
36752
+ }
36753
+ function rpcHttpEquivalent(code) {
36754
+ return HTTP_EQUIVALENT[code] ?? 500;
36755
+ }
36756
+ function effectiveStatus(r) {
36757
+ if (r.status === null)
36758
+ return null;
36759
+ if (r.rpcStatus === void 0 || r.rpcStatus.code === 0)
36760
+ return r.status;
36761
+ return r.status >= 200 && r.status < 300 ? rpcHttpEquivalent(r.rpcStatus.code) : r.status;
36762
+ }
36763
+ function describeStatusWithRpc(r) {
36764
+ if (r.status === null)
36765
+ return "no response";
36766
+ if (r.rpcStatus === void 0 || r.rpcStatus.code === 0)
36767
+ return String(r.status);
36768
+ return `${r.status} (grpc-status ${r.rpcStatus.code} ${r.rpcStatus.name})`;
36769
+ }
36770
+ function protocolOf(contentType) {
36771
+ if (!RPC_CONTENT.test(contentType))
36772
+ return null;
36773
+ return /connect\+/i.test(contentType) ? "connect" : "grpc-web";
36774
+ }
36775
+ function rpcStatusFromHeaders(headers, contentType = headers["content-type"] ?? "") {
36776
+ const raw = headers["grpc-status"];
36777
+ if (raw === void 0 || !/^\s*\d+\s*$/.test(raw))
36778
+ return null;
36779
+ return statusOf2(/connect\+/i.test(contentType) ? "connect" : "grpc-web", Number(raw.trim()));
36780
+ }
36781
+ function decodeGrpcWebText(body) {
36782
+ const text2 = Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString("latin1").replace(/\s+/g, "");
36783
+ const chunks = text2.match(/[^=]+={0,2}|={1,2}/g) ?? [];
36784
+ return Buffer.concat(chunks.map((c) => Buffer.from(c, "base64")));
36785
+ }
36786
+ function lastFrameWith(bytes, bit) {
36787
+ let found = null;
36788
+ let at3 = 0;
36789
+ while (at3 + 5 <= bytes.length) {
36790
+ const flag = bytes[at3] ?? 0;
36791
+ const len = ((bytes[at3 + 1] ?? 0) << 24 >>> 0) + ((bytes[at3 + 2] ?? 0) << 16) + ((bytes[at3 + 3] ?? 0) << 8) + (bytes[at3 + 4] ?? 0);
36792
+ const end = at3 + 5 + len;
36793
+ if (end > bytes.length)
36794
+ break;
36795
+ if ((flag & bit) !== 0)
36796
+ found = bytes.subarray(at3 + 5, end);
36797
+ at3 = end;
36798
+ }
36799
+ return found;
36800
+ }
36801
+ function parseGrpcWebTrailer(body, contentType) {
36802
+ const bytes = /grpc-web-text/i.test(contentType) ? decodeGrpcWebText(body) : body;
36803
+ const trailer = lastFrameWith(bytes, 128);
36804
+ if (trailer === null)
36805
+ return null;
36806
+ for (const line of Buffer.from(trailer.buffer, trailer.byteOffset, trailer.byteLength).toString("latin1").split(/\r?\n/)) {
36807
+ const m = /^\s*grpc-status\s*:\s*(\d+)\s*$/i.exec(line);
36808
+ if (m !== null)
36809
+ return Number(m[1]);
36810
+ }
36811
+ return null;
36812
+ }
36813
+ function parseConnectUnaryError(body) {
36814
+ try {
36815
+ const parsed = JSON.parse(body);
36816
+ const code = parsed !== null && typeof parsed === "object" && "code" in parsed ? parsed.code : null;
36817
+ return typeof code === "string" && /^[a-z_]+$/.test(code) ? code : null;
36818
+ } catch {
36819
+ return null;
36820
+ }
36821
+ }
36822
+ function parseConnectEndStream(body) {
36823
+ const end = lastFrameWith(body, 2);
36824
+ if (end === null)
36825
+ return null;
36826
+ try {
36827
+ const parsed = JSON.parse(Buffer.from(end.buffer, end.byteOffset, end.byteLength).toString("utf8"));
36828
+ if (parsed === null || typeof parsed !== "object")
36829
+ return null;
36830
+ const error62 = parsed.error;
36831
+ if (error62 === void 0 || error62 === null)
36832
+ return 0;
36833
+ const name = typeof error62 === "object" ? error62.code : void 0;
36834
+ if (typeof name !== "string")
36835
+ return 2;
36836
+ const code = GRPC_CODE_NAMES.indexOf(name);
36837
+ return code <= 0 ? 2 : code;
36838
+ } catch {
36839
+ return null;
36840
+ }
36841
+ }
36842
+ function rpcStatusFromBody(body, contentType) {
36843
+ const protocol = protocolOf(contentType);
36844
+ if (protocol === null)
36845
+ return null;
36846
+ const code = protocol === "connect" ? parseConnectEndStream(body) : parseGrpcWebTrailer(body, contentType);
36847
+ return code === null ? null : statusOf2(protocol, code);
36848
+ }
36849
+ var reads = /* @__PURE__ */ new WeakMap();
36850
+ function rpcStatusOfResponse(response) {
36851
+ const known = reads.get(response);
36852
+ if (known !== void 0)
36853
+ return known;
36854
+ const read = (async () => {
36855
+ const headers = response.headers();
36856
+ const contentType = headers["content-type"] ?? "";
36857
+ const fromHeader = rpcStatusFromHeaders(headers, contentType);
36858
+ if (fromHeader !== null)
36859
+ return fromHeader;
36860
+ if (protocolOf(contentType) === null)
36861
+ return null;
36862
+ const declared = Number(headers["content-length"] ?? "0");
36863
+ if (Number.isFinite(declared) && declared > MAX_RPC_BODY_BYTES)
36864
+ return null;
36865
+ const body = await response.body();
36866
+ return body.length > MAX_RPC_BODY_BYTES ? null : rpcStatusFromBody(body, contentType);
36867
+ })().catch(() => null);
36868
+ reads.set(response, read);
36869
+ return read;
36870
+ }
36871
+
36563
36872
  // ../explore/dist/http-5xx.js
36564
- function http5xxSignalOf(response, firstParty, headers, method) {
36565
- const status = response.status();
36873
+ function http5xxSignalOf(response, firstParty, headers, method, rpc) {
36874
+ const httpStatus = response.status();
36875
+ const status = effectiveStatus({ status: httpStatus, ...rpc === void 0 || rpc === null ? {} : { rpcStatus: rpc } }) ?? httpStatus;
36566
36876
  if (status < 500)
36567
36877
  return null;
36568
36878
  if (firstParty !== void 0 && firstParty.thirdParty(response.url(), headers) !== null)
36569
36879
  return null;
36570
36880
  const url2 = redactUrl(response.url());
36571
- return { kind: "http-5xx", detail: `${status} ${url2}`, url: url2, status, ...method === void 0 ? {} : { method: method.toUpperCase() } };
36881
+ const rpcNote = status === httpStatus || rpc === void 0 || rpc === null ? "" : ` (HTTP ${httpStatus}, grpc-status ${rpc.code} ${rpc.name})`;
36882
+ return { kind: "http-5xx", detail: `${status} ${url2}${rpcNote}`, url: url2, status, ...method === void 0 ? {} : { method: method.toUpperCase() } };
36883
+ }
36884
+ async function rpc5xxSignalOf(response, firstParty, headers, method) {
36885
+ if (response.status() >= 500)
36886
+ return null;
36887
+ let rpcCandidate = false;
36888
+ try {
36889
+ const h = response.headers();
36890
+ rpcCandidate = RPC_CONTENT.test(h["content-type"] ?? "") || h["grpc-status"] !== void 0;
36891
+ } catch {
36892
+ return null;
36893
+ }
36894
+ if (!rpcCandidate)
36895
+ return null;
36896
+ const rpc = await rpcStatusOfResponse(response);
36897
+ return rpc === null || rpc.code === 0 ? null : http5xxSignalOf(response, firstParty, headers, method, rpc);
36572
36898
  }
36573
36899
  function requestHeadersOf(r) {
36574
36900
  try {
@@ -36594,15 +36920,24 @@ var Http5xxOracle = class {
36594
36920
  });
36595
36921
  page.on("response", (response) => {
36596
36922
  const request = response.request();
36923
+ const isDocument = request.isNavigationRequest() && request.frame() === page.mainFrame();
36924
+ const pageUrl = redactUrl(page.url());
36925
+ const record2 = (signal2) => {
36926
+ this.#observed.push({
36927
+ signal: signal2,
36928
+ method: request.method().toUpperCase(),
36929
+ pageUrl: isDocument ? signal2.url : pageUrl,
36930
+ startedAt: this.#started.get(request) ?? this.#now()
36931
+ });
36932
+ };
36597
36933
  const signal = http5xxSignalOf(response, this.#firstParty, requestHeadersOf(request));
36598
- if (signal === null)
36934
+ if (signal !== null) {
36935
+ record2(signal);
36599
36936
  return;
36600
- const isDocument = request.isNavigationRequest() && request.frame() === page.mainFrame();
36601
- this.#observed.push({
36602
- signal,
36603
- method: request.method().toUpperCase(),
36604
- pageUrl: isDocument ? signal.url : redactUrl(page.url()),
36605
- startedAt: this.#started.get(request) ?? this.#now()
36937
+ }
36938
+ void rpc5xxSignalOf(response, this.#firstParty, requestHeadersOf(request)).then((s3) => {
36939
+ if (s3 !== null)
36940
+ record2(s3);
36606
36941
  });
36607
36942
  });
36608
36943
  }
@@ -36764,15 +37099,26 @@ var PageSignalCollector = class {
36764
37099
  responseSeen.add(response.request());
36765
37100
  noteResponse(response.status(), redactUrl(response.url()));
36766
37101
  const request = response.request();
36767
- const signal = http5xxSignalOf(response, firstParty, firstParty === void 0 ? void 0 : requestHeadersOf(request), methodOf(request));
36768
- if (signal === null)
37102
+ const headers = firstParty === void 0 ? void 0 : requestHeadersOf(request);
37103
+ const push = (signal2) => {
37104
+ const started = requestStarted.get(request);
37105
+ if (started !== void 0)
37106
+ this.startedAt.set(signal2, started);
37107
+ this.buffer.push(signal2);
37108
+ };
37109
+ const signal = http5xxSignalOf(response, firstParty, headers, methodOf(request));
37110
+ if (signal !== null) {
37111
+ push(signal);
36769
37112
  return;
36770
- const started = requestStarted.get(request);
36771
- if (started !== void 0)
36772
- this.startedAt.set(signal, started);
36773
- this.buffer.push(signal);
37113
+ }
37114
+ void rpc5xxSignalOf(response, firstParty, headers, methodOf(request)).then((s3) => {
37115
+ if (s3 !== null)
37116
+ push(s3);
37117
+ });
36774
37118
  });
36775
37119
  page.on("requestfailed", (request) => {
37120
+ if (isExternalSchemeUrl(request.url()))
37121
+ return;
36776
37122
  const errorText = request.failure()?.errorText ?? "request failed";
36777
37123
  if (errorText === ERR_ABORTED) {
36778
37124
  if (responseSeen.has(request))
@@ -36997,6 +37343,7 @@ function readPageTiming() {
36997
37343
  }
36998
37344
  async function measurePageTiming(page, window2) {
36999
37345
  const apiPrefixes = window2.apiPrefixes ?? [];
37346
+ const waitedMs2 = Math.max(0, window2.waitedMs ?? 0);
37000
37347
  const side2 = await page.evaluate(readPageTiming).catch(() => ({ docId: null, nav: null, lcp: null }));
37001
37348
  const now2 = window2.settleEndedAt;
37002
37349
  const all = [
@@ -37010,7 +37357,8 @@ async function measurePageTiming(page, window2) {
37010
37357
  route: normalizeRoute(redactUrl(page.url())),
37011
37358
  kind,
37012
37359
  ...newDocument && side2.nav !== null ? { navigation: side2.nav } : {},
37013
- ...window2.actionAt !== null ? { settleMs: Math.max(0, window2.settleEndedAt - window2.actionAt) } : {},
37360
+ ...window2.actionAt !== null ? { settleMs: Math.max(0, window2.settleEndedAt - window2.actionAt - waitedMs2) } : {},
37361
+ ...window2.actionAt !== null && waitedMs2 > 0 ? { waitedMs: waitedMs2 } : {},
37014
37362
  settled: window2.settle.settled,
37015
37363
  requests: {
37016
37364
  count: window2.completed.length,
@@ -37183,7 +37531,22 @@ function hangRoute(url2) {
37183
37531
  return normalizeRoute(redactUrl(url2));
37184
37532
  }
37185
37533
 
37534
+ // ../explore/dist/clock-bound.js
37535
+ async function clockBounded(work, ms, onTimeout) {
37536
+ let timer;
37537
+ const bound = new Promise((resolve14) => {
37538
+ timer = clock.setTimeout(() => resolve14(onTimeout), Math.max(1, ms));
37539
+ });
37540
+ try {
37541
+ return await Promise.race([work, bound]);
37542
+ } finally {
37543
+ if (timer !== void 0)
37544
+ clock.clearTimeout(timer);
37545
+ }
37546
+ }
37547
+
37186
37548
  // ../explore/dist/page-monitor.js
37549
+ var RPC_TRAILER_READ_MS = 2e3;
37187
37550
  var INTERACTIVE_SELECTOR2 = "a[href],button,input:not([type=hidden]),select,textarea,[role=button],[role=link],[role=textbox],[role=checkbox],[role=combobox],[contenteditable=true]";
37188
37551
  function hasEnabledControl(selector) {
37189
37552
  for (const el of Array.from(document.querySelectorAll(selector))) {
@@ -37352,6 +37715,21 @@ var RequestCapture = class {
37352
37715
  return this.#truncated;
37353
37716
  }
37354
37717
  };
37718
+ var ACTION_CAUSED_MS = 1500;
37719
+ var isRead = (r) => ["GET", "HEAD"].includes(r.method.toUpperCase());
37720
+ var RECURRING_WINDOW_MS = 3e4;
37721
+ var RECURRING_MIN_COMPLETIONS = 2;
37722
+ function isRecurringRead(r, recent) {
37723
+ const method = r.method.toUpperCase();
37724
+ if (!isRead(r))
37725
+ return false;
37726
+ const path2 = pathOf2(r.url);
37727
+ let n3 = 0;
37728
+ for (const c of recent)
37729
+ if (c.method.toUpperCase() === method && pathOf2(c.url) === path2 && ++n3 >= RECURRING_MIN_COMPLETIONS)
37730
+ return true;
37731
+ return false;
37732
+ }
37355
37733
  function pathOf2(url2) {
37356
37734
  try {
37357
37735
  return new URL(url2).pathname;
@@ -37375,6 +37753,9 @@ var PageMonitor = class {
37375
37753
  #wakers = /* @__PURE__ */ new Set();
37376
37754
  #statuses = /* @__PURE__ */ new WeakMap();
37377
37755
  #contentTypes = /* @__PURE__ */ new WeakMap();
37756
+ /** #378: the RPC-status read of a gRPC-web / Connect response (its end waits for it). */
37757
+ #rpcReads = /* @__PURE__ */ new WeakMap();
37758
+ #rpcStatuses = /* @__PURE__ */ new WeakMap();
37378
37759
  #now;
37379
37760
  #lastNetworkActivity;
37380
37761
  /** The last main-frame navigation (activity no request filter can discount). */
@@ -37392,6 +37773,8 @@ var PageMonitor = class {
37392
37773
  this.#lastNetworkActivity = now2();
37393
37774
  this.#lastNavigation = now2();
37394
37775
  page.on("request", (r) => {
37776
+ if (isExternalSchemeUrl(r.url()))
37777
+ return;
37395
37778
  const startedAt = this.#now();
37396
37779
  if (r.isNavigationRequest() && r.frame() === page.mainFrame())
37397
37780
  this.#documentNavStartedAt = startedAt;
@@ -37445,6 +37828,7 @@ var PageMonitor = class {
37445
37828
  const abortedAfterResponse = failed && status !== null;
37446
37829
  const endedAt = this.#now();
37447
37830
  const contentType = this.#contentTypes.get(r) ?? null;
37831
+ const rpcStatus = this.#rpcStatuses.get(r);
37448
37832
  this.#completed.push({
37449
37833
  ...started,
37450
37834
  status,
@@ -37466,7 +37850,8 @@ var PageMonitor = class {
37466
37850
  resourceType: started.resourceType,
37467
37851
  contentType,
37468
37852
  startedAt: started.startedAt,
37469
- ...started.requestContentType === void 0 ? {} : { requestContentType: started.requestContentType }
37853
+ ...started.requestContentType === void 0 ? {} : { requestContentType: started.requestContentType },
37854
+ ...rpcStatus === void 0 ? {} : { rpcStatus }
37470
37855
  });
37471
37856
  }
37472
37857
  } else {
@@ -37476,13 +37861,28 @@ var PageMonitor = class {
37476
37861
  if (!ignored)
37477
37862
  this.#touch();
37478
37863
  };
37479
- page.on("requestfinished", (r) => end(r, false));
37480
- page.on("requestfailed", (r) => end(r, true));
37864
+ const finish = (r, failed) => {
37865
+ const read = this.#rpcReads.get(r);
37866
+ if (read === void 0) {
37867
+ end(r, failed);
37868
+ return;
37869
+ }
37870
+ this.#rpcReads.delete(r);
37871
+ void clockBounded(read, RPC_TRAILER_READ_MS, null).then((s3) => {
37872
+ if (s3 !== null)
37873
+ this.#rpcStatuses.set(r, s3);
37874
+ end(r, failed);
37875
+ });
37876
+ };
37877
+ page.on("requestfinished", (r) => finish(r, false));
37878
+ page.on("requestfailed", (r) => finish(r, true));
37481
37879
  page.on("response", (res) => {
37482
37880
  this.#statuses.set(res.request(), res.status());
37483
37881
  const type = res.headers()["content-type"] ?? "";
37484
37882
  if (type !== "")
37485
37883
  this.#contentTypes.set(res.request(), type);
37884
+ if (RPC_CONTENT.test(type) || res.headers()["grpc-status"] !== void 0)
37885
+ this.#rpcReads.set(res.request(), rpcStatusOfResponse(res));
37486
37886
  if (STREAMING_CONTENT.test(type)) {
37487
37887
  this.#background.set(res.request(), "stream");
37488
37888
  this.#touch();
@@ -37541,6 +37941,20 @@ var PageMonitor = class {
37541
37941
  if (this.#ignore(r.url()))
37542
37942
  this.#background.set(r, "ignored");
37543
37943
  }
37944
+ /**
37945
+ * #383: pending work minus the page's own polling — an in-flight read (GET/HEAD) whose method and
37946
+ * path already completed {@link RECURRING_MIN_COMPLETIONS} times in the last
37947
+ * {@link RECURRING_WINDOW_MS} is a periodic poll (a balance, a notification count), not the app
37948
+ * working on what the run did; so is a read the page started on its own more than
37949
+ * {@link ACTION_CAUSED_MS} after the run's last action. A write always counts. What a quiet `wait`
37950
+ * asks (#241).
37951
+ */
37952
+ pendingWork() {
37953
+ const now2 = this.#now();
37954
+ const recent = this.#completed.filter((c) => c.endedAt >= now2 - RECURRING_WINDOW_MS);
37955
+ const since = this.#lastActionAt;
37956
+ return this.pending().filter((r) => !isRecurringRead(r, recent) && !(isRead(r) && since !== null && r.startedAt > since + ACTION_CAUSED_MS));
37957
+ }
37544
37958
  /** Requests that are pending WORK: long-lived connections and background requests excluded. */
37545
37959
  pending() {
37546
37960
  return [...this.#inflight.entries()].filter(([r, info]) => !STREAM_TYPES.has(info.resourceType) && !this.#background.has(r)).map(([, info]) => info);
@@ -37614,15 +38028,40 @@ var PageMonitor = class {
37614
38028
  // ---- the timing window: from one perception to the next (owner ruling 6) ----
37615
38029
  #windowStart = null;
37616
38030
  #actionAt = null;
38031
+ /** #383: the run's most recent action, kept across windows (what "caused by the run" is measured from). */
38032
+ #lastActionAt = null;
37617
38033
  #lastDocId = null;
38034
+ /** #368: time spent since the action inside a wait the run chose (a reply wait, a job wait). */
38035
+ #waitedMs = 0;
38036
+ #waitDepth = 0;
37618
38037
  /** Called by `act()` when it dispatches a page-changing action: the start of a transition. */
37619
38038
  markAction() {
37620
38039
  this.#actionAt = this.#now();
38040
+ this.#lastActionAt = this.#actionAt;
38041
+ this.#waitedMs = 0;
38042
+ }
38043
+ /**
38044
+ * #368: runs an EXPLICIT wait the run chose (a chat reply wait `--reply-wait-ms`, a job wait
38045
+ * `--job-wait-ms`, a `wait` decision) and books its duration as waiting, not as the page's render:
38046
+ * the transition's `settleMs` excludes it, so a 30s reply wait never reads as a 30s render (and
38047
+ * never feeds the host-health render trend). Nested waits count once.
38048
+ */
38049
+ async explicitWait(wait) {
38050
+ const started = this.#now();
38051
+ this.#waitDepth += 1;
38052
+ try {
38053
+ return await wait();
38054
+ } finally {
38055
+ this.#waitDepth -= 1;
38056
+ if (this.#waitDepth === 0)
38057
+ this.#waitedMs += Math.max(0, this.#now() - started);
38058
+ }
37621
38059
  }
37622
38060
  /** The open timing window (since the previous perception, or since the monitor started). */
37623
38061
  window() {
37624
38062
  const start = this.#windowStart ?? 0;
37625
- return { start, actionAt: this.#actionAt !== null && this.#actionAt >= start ? this.#actionAt : null, lastDocId: this.#lastDocId };
38063
+ const actionAt = this.#actionAt !== null && this.#actionAt >= start ? this.#actionAt : null;
38064
+ return { start, actionAt, lastDocId: this.#lastDocId, waitedMs: actionAt === null ? 0 : this.#waitedMs };
37626
38065
  }
37627
38066
  /**
37628
38067
  * Closes the window at `at` (the perception just read the page) and forgets requests that ended
@@ -37634,6 +38073,7 @@ var PageMonitor = class {
37634
38073
  closeWindow(at3, docId) {
37635
38074
  this.#windowStart = at3;
37636
38075
  this.#actionAt = null;
38076
+ this.#waitedMs = 0;
37637
38077
  if (docId !== null)
37638
38078
  this.#lastDocId = docId;
37639
38079
  const keepFrom = at3 - RECENT_REQUESTS_MS;
@@ -37766,18 +38206,267 @@ function monitorFor(page) {
37766
38206
  return m;
37767
38207
  }
37768
38208
 
37769
- // ../explore/dist/clock-bound.js
37770
- async function clockBounded(work, ms, onTimeout) {
37771
- let timer;
37772
- const bound = new Promise((resolve14) => {
37773
- timer = clock.setTimeout(() => resolve14(onTimeout), Math.max(1, ms));
37774
- });
37775
- try {
37776
- return await Promise.race([work, bound]);
37777
- } finally {
37778
- if (timer !== void 0)
37779
- clock.clearTimeout(timer);
38209
+ // ../explore/dist/status.js
38210
+ var EMPTY_STATUS = { alerts: [], notices: [], invalid: [] };
38211
+ var STATUS_CHARS = 200;
38212
+ var STATUS_ITEMS = 5;
38213
+ function readStatusInPage(limits) {
38214
+ const norm5 = (s3) => (s3 ?? "").replace(/\s+/g, " ").trim();
38215
+ const cut3 = (s3) => s3.length > limits.chars ? `${s3.slice(0, limits.chars)}\u2026` : s3;
38216
+ const shown = (el) => {
38217
+ const h = el;
38218
+ if (h.closest("[hidden],[aria-hidden=true]") !== null)
38219
+ return false;
38220
+ const st = window.getComputedStyle(h);
38221
+ const r = h.getBoundingClientRect();
38222
+ return st.display !== "none" && st.visibility !== "hidden" && r.width > 0 && r.height > 0;
38223
+ };
38224
+ const w = window;
38225
+ if (w.__jevInvalid === void 0) {
38226
+ const seen = /* @__PURE__ */ new WeakSet();
38227
+ w.__jevInvalid = seen;
38228
+ document.addEventListener("invalid", (e) => {
38229
+ if (e.target instanceof Element)
38230
+ seen.add(e.target);
38231
+ }, true);
38232
+ }
38233
+ const refused = w.__jevInvalid;
38234
+ const alerts = [];
38235
+ const notices = [];
38236
+ const live = document.querySelectorAll("[role=alert],[role=status],[aria-live=polite],[aria-live=assertive],output");
38237
+ for (const el of Array.from(live)) {
38238
+ if (!shown(el))
38239
+ continue;
38240
+ const text2 = cut3(norm5(el.innerText));
38241
+ if (text2 === "")
38242
+ continue;
38243
+ if ([...alerts, ...notices].some((a) => a.includes(text2) || text2.includes(a)))
38244
+ continue;
38245
+ const assertive = el.getAttribute("role") === "alert" || el.getAttribute("aria-live") === "assertive";
38246
+ const into = assertive ? alerts : notices;
38247
+ if (into.length < limits.items)
38248
+ into.push(text2);
38249
+ }
38250
+ const labelOf2 = (el) => {
38251
+ const aria = norm5(el.getAttribute("aria-label"));
38252
+ if (aria !== "")
38253
+ return aria;
38254
+ const labels = el.labels;
38255
+ if (labels && labels.length > 0)
38256
+ return norm5(labels[0].textContent);
38257
+ const owner = el.closest("label");
38258
+ if (owner !== null)
38259
+ return norm5(owner.textContent);
38260
+ return norm5(el.getAttribute("placeholder")) || norm5(el.getAttribute("name")) || el.tagName.toLowerCase();
38261
+ };
38262
+ const describedBy = (el) => {
38263
+ const ids = `${el.getAttribute("aria-errormessage") ?? ""} ${el.getAttribute("aria-describedby") ?? ""}`.trim();
38264
+ if (ids === "")
38265
+ return "";
38266
+ return norm5(ids.split(/\s+/).map((id) => document.getElementById(id)).filter((n3) => n3 !== null && shown(n3)).map((n3) => n3.innerText).join(" "));
38267
+ };
38268
+ const invalid = [];
38269
+ const fields = document.querySelectorAll("input:not([type=hidden]),select,textarea,[aria-invalid=true]");
38270
+ for (const el of Array.from(fields)) {
38271
+ if (!shown(el))
38272
+ continue;
38273
+ const f = el;
38274
+ const ariaInvalid = norm5(el.getAttribute("aria-invalid")).toLowerCase() === "true";
38275
+ const nativeInvalid = typeof f.checkValidity === "function" && f.validity !== void 0 && !f.validity.valid;
38276
+ const reached2 = refused.has(el) || typeof f.value === "string" && f.value !== "";
38277
+ if (!ariaInvalid && !(nativeInvalid && reached2))
38278
+ continue;
38279
+ const message2 = norm5(nativeInvalid ? f.validationMessage : "") || describedBy(el) || "marked invalid";
38280
+ invalid.push({ name: cut3(labelOf2(el)), message: cut3(message2) });
38281
+ if (invalid.length >= limits.items)
38282
+ break;
38283
+ }
38284
+ return { alerts, notices, invalid };
38285
+ }
38286
+ async function readPageStatus(page) {
38287
+ return page.evaluate(readStatusInPage, { chars: STATUS_CHARS, items: STATUS_ITEMS }).catch(() => EMPTY_STATUS);
38288
+ }
38289
+ function statusDelta(before, now2) {
38290
+ const alerts = now2.alerts.filter((a) => !before.alerts.includes(a));
38291
+ const notices = now2.notices.filter((a) => !before.notices.includes(a));
38292
+ const invalid = now2.invalid.filter((f) => !before.invalid.some((b2) => b2.name === f.name && b2.message === f.message));
38293
+ return { alerts, notices, invalid };
38294
+ }
38295
+ function describeStatus(s3) {
38296
+ return [
38297
+ ...s3.alerts.map((a) => `alert "${a}"`),
38298
+ ...s3.notices.map((a) => `status "${a}"`),
38299
+ ...s3.invalid.map((f) => `invalid field "${f.name}": "${f.message}"`)
38300
+ ].join("; ");
38301
+ }
38302
+ function isEmptyStatus(s3) {
38303
+ return s3.alerts.length === 0 && s3.notices.length === 0 && s3.invalid.length === 0;
38304
+ }
38305
+ var IN_PROGRESS_WORDS = "simulating|processing|running|generating|drafting|fetching|rendering|publishing|analy[sz]ing|computing|calculating|loading|preparing|working|thinking|uploading|importing|exporting|syncing|saving|submitting|creating|building|training|indexing|queued|in progress";
38306
+ function inProgressInPage(words3) {
38307
+ const norm5 = (s3) => (s3 ?? "").replace(/\s+/g, " ").trim();
38308
+ const cut3 = (s3) => s3.length > 80 ? `${s3.slice(0, 80)}\u2026` : s3;
38309
+ const shown = (el) => {
38310
+ const h = el;
38311
+ if (h.closest("[hidden],[aria-hidden=true]") !== null)
38312
+ return false;
38313
+ const st = window.getComputedStyle(h);
38314
+ const r = h.getBoundingClientRect();
38315
+ return st.display !== "none" && st.visibility !== "hidden" && r.width > 0 && r.height > 0;
38316
+ };
38317
+ const loose = new RegExp(`\\b(?:${words3})\\b`, "i");
38318
+ const trailing = new RegExp(`^\\W*(?:${words3})\\b[^.!?]{0,60}(?:\u2026|\\.\\.\\.)\\s*$`, "i");
38319
+ const job = /\b(?:is|are)\s+(?:still\s+|already\s+|currently\s+|now\s+)?(?:running|processing|in progress|being (?:processed|generated|simulated|prepared))\b|\bstill (?:running|working|processing)\b|^\W*in progress\W*$/i;
38320
+ for (const el of Array.from(document.querySelectorAll('[aria-busy="true"]'))) {
38321
+ if (shown(el))
38322
+ return `aria-busy region${norm5(el.innerText) === "" ? "" : ` "${cut3(norm5(el.innerText))}"`}`;
38323
+ }
38324
+ const live = document.querySelectorAll("[role=status],[aria-live=polite],[aria-live=assertive],[role=progressbar],output");
38325
+ for (const el of Array.from(live)) {
38326
+ if (!shown(el))
38327
+ continue;
38328
+ const text2 = norm5(el.innerText || el.getAttribute("aria-valuetext") || el.getAttribute("aria-label"));
38329
+ if (text2 !== "" && loose.test(text2))
38330
+ return `status "${cut3(text2)}"`;
38331
+ if (text2 !== "" && /^\W*[a-z]{3,}ing\b[^.!?]{0,60}(?:…|\.\.\.)\s*$/i.test(text2))
38332
+ return `status "${cut3(text2)}"`;
38333
+ }
38334
+ const body = document.body ? document.body.innerText : "";
38335
+ for (const raw of body.split(/\n+/)) {
38336
+ const line = norm5(raw);
38337
+ if (line === "" || line.length > 200)
38338
+ continue;
38339
+ if (trailing.test(line) || job.test(line))
38340
+ return `status text "${cut3(line)}"`;
38341
+ }
38342
+ return null;
38343
+ }
38344
+ async function readInProgressStatus(page) {
38345
+ return page.evaluate(inProgressInPage, IN_PROGRESS_WORDS).catch(() => null);
38346
+ }
38347
+ async function busyOverlay(page) {
38348
+ const busy = await page.evaluate(visibleBusyIndicator).catch(() => null);
38349
+ return busy ?? await readInProgressStatus(page);
38350
+ }
38351
+ function workAcknowledgedInPage(words3) {
38352
+ const norm5 = (s3) => (s3 ?? "").replace(/\s+/g, " ").trim();
38353
+ const shown = (el) => {
38354
+ const h = el;
38355
+ if (h.closest("[hidden],[aria-hidden=true]") !== null)
38356
+ return false;
38357
+ const st = window.getComputedStyle(h);
38358
+ const r = h.getBoundingClientRect();
38359
+ return st.display !== "none" && st.visibility !== "hidden" && r.width > 0 && r.height > 0;
38360
+ };
38361
+ const label2 = (el) => el instanceof HTMLInputElement ? norm5(el.value) : norm5(el.getAttribute("aria-label") ?? el.innerText ?? el.textContent);
38362
+ const disabled = (el) => el.disabled === true || el.getAttribute("aria-disabled") === "true";
38363
+ const controls = Array.from(document.querySelectorAll("button,[role=button],input[type=button],input[type=submit]")).filter(shown);
38364
+ const progress = new RegExp(`^\\W*(?:${words3})\\b[^.!?]{0,60}(?:\u2026|\\.\\.\\.)?\\s*$`, "i");
38365
+ let cancel2 = null;
38366
+ let busyControl = null;
38367
+ for (const el of controls) {
38368
+ const n3 = label2(el);
38369
+ if (cancel2 === null && !disabled(el) && /^\W*(?:cancel|stop|abort)\b/i.test(n3))
38370
+ cancel2 = `an enabled "${n3.slice(0, 40)}" control`;
38371
+ if (busyControl === null && disabled(el) && n3 !== "" && progress.test(n3))
38372
+ busyControl = `a disabled "${n3.slice(0, 40)}" control`;
38373
+ }
38374
+ let bar = null;
38375
+ for (const el of Array.from(document.querySelectorAll("[role=progressbar][aria-valuenow],progress[value]"))) {
38376
+ if (!shown(el))
38377
+ continue;
38378
+ const now2 = Number(el.getAttribute("aria-valuenow") ?? el.value);
38379
+ const max = Number(el.getAttribute("aria-valuemax") ?? el.max) || 100;
38380
+ if (Number.isFinite(now2) && now2 < max) {
38381
+ bar = `a progress bar at ${Math.round(now2 / max * 100)}%`;
38382
+ break;
38383
+ }
37780
38384
  }
38385
+ return { busyControl, cancel: cancel2, bar };
38386
+ }
38387
+ async function readWorkingStatus(page) {
38388
+ const ack = await page.evaluate(workAcknowledgedInPage, IN_PROGRESS_WORDS).catch(() => null);
38389
+ if (ack === null)
38390
+ return null;
38391
+ if (ack.busyControl !== null)
38392
+ return ack.cancel === null ? ack.busyControl : `${ack.busyControl} and ${ack.cancel}`;
38393
+ const extra = ack.cancel ?? ack.bar;
38394
+ if (extra === null)
38395
+ return null;
38396
+ const status = await readInProgressStatus(page);
38397
+ return status === null ? null : `${status} with ${extra}`;
38398
+ }
38399
+ var MAX_DOCUMENTED_WAIT_MS = 30 * 6e4;
38400
+ var NUMBER_WORDS = {
38401
+ a: 1,
38402
+ an: 1,
38403
+ one: 1,
38404
+ two: 2,
38405
+ three: 3,
38406
+ four: 4,
38407
+ five: 5,
38408
+ six: 6,
38409
+ seven: 7,
38410
+ eight: 8,
38411
+ nine: 9,
38412
+ ten: 10,
38413
+ fifteen: 15,
38414
+ twenty: 20,
38415
+ thirty: 30,
38416
+ forty: 40,
38417
+ sixty: 60,
38418
+ ninety: 90,
38419
+ "a couple of": 3,
38420
+ "a couple": 3,
38421
+ "a few": 5,
38422
+ few: 5,
38423
+ several: 10
38424
+ };
38425
+ var UNIT_MS = [
38426
+ [/^(?:seconds?|secs?|s)$/i, 1e3],
38427
+ [/^(?:minutes?|mins?|m)$/i, 6e4],
38428
+ [/^(?:hours?|hrs?|h)$/i, 36e5]
38429
+ ];
38430
+ var WAIT_TALK = /\b(?:take|takes|taking|took|wait|waiting|ready|done|finish|finishes|complete|completes|usually|typically|normally|should|may|might|can|up to|less than|under|within|about|around|roughly|approximately|expect)\b/i;
38431
+ var DURATION = /\b(\d+(?:\.\d+)?|an?|one|two|three|four|five|six|seven|eight|nine|ten|fifteen|twenty|thirty|forty|sixty|ninety|a couple(?: of)?|a few|few|several)(?:\s*(?:-|–|to)\s*(\d+(?:\.\d+)?))?\s*(seconds?|secs?|minutes?|mins?|hours?|hrs?)\b/gi;
38432
+ function documentedWaitMs(line) {
38433
+ if (!WAIT_TALK.test(line) || /\bago\b/i.test(line))
38434
+ return null;
38435
+ let longest = null;
38436
+ for (const m of line.matchAll(DURATION)) {
38437
+ const [, first, upper, unit] = m;
38438
+ const per = UNIT_MS.find(([re]) => re.test(unit ?? ""))?.[1];
38439
+ if (per === void 0 || first === void 0)
38440
+ continue;
38441
+ const n3 = Number(upper ?? first) || NUMBER_WORDS[first.toLowerCase()] || NaN;
38442
+ if (!Number.isFinite(n3) || n3 <= 0)
38443
+ continue;
38444
+ longest = Math.max(longest ?? 0, n3 * per);
38445
+ }
38446
+ return longest === null ? null : Math.min(longest, MAX_DOCUMENTED_WAIT_MS);
38447
+ }
38448
+ function durationLinesInPage() {
38449
+ const body = document.body ? document.body.innerText : "";
38450
+ const out = [];
38451
+ for (const raw of body.split(/\n+/)) {
38452
+ const line = raw.replace(/\s+/g, " ").trim();
38453
+ if (line === "" || line.length > 300)
38454
+ continue;
38455
+ if (/\b(?:seconds?|secs?|minutes?|mins?|hours?|hrs?)\b/i.test(line))
38456
+ out.push(line);
38457
+ if (out.length >= 10)
38458
+ break;
38459
+ }
38460
+ return out;
38461
+ }
38462
+ async function readDocumentedWait(page) {
38463
+ const lines = await page.evaluate(durationLinesInPage).catch(() => []);
38464
+ for (const line of lines) {
38465
+ const ms = documentedWaitMs(line);
38466
+ if (ms !== null)
38467
+ return { text: line.length > 120 ? `${line.slice(0, 120)}\u2026` : line, ms };
38468
+ }
38469
+ return null;
37781
38470
  }
37782
38471
 
37783
38472
  // ../explore/dist/perceive.js
@@ -37836,7 +38525,8 @@ async function perceive(page, opts = {}) {
37836
38525
  const requestBoundMs = opts.requestBoundMs ?? ceiling / 2;
37837
38526
  const monitor = monitorFor(page);
37838
38527
  monitor.configure(opts.settleConfig);
37839
- const ignoreNoProgress = textMatcher(opts.hangConfig?.ignoreNoProgress);
38528
+ const ignoreDeclared = textMatcher(opts.hangConfig?.ignoreNoProgress);
38529
+ const ignoreNoProgress = (desc) => ignoreDeclared(desc) || ignoreDeclared(/"(.*)"/.exec(desc)?.[1] ?? desc);
37840
38530
  let responsive = await probeResponsive(page, hangProbeMs);
37841
38531
  if (!responsive && documentPending(monitor.pending())) {
37842
38532
  const deadline = clock.now() + ceiling;
@@ -37947,10 +38637,44 @@ async function perceive(page, opts = {}) {
37947
38637
  actionAt: win.actionAt,
37948
38638
  settle: settle2,
37949
38639
  settleEndedAt,
38640
+ waitedMs: win.waitedMs,
37950
38641
  ...opts.timingConfig?.apiPrefixes === void 0 ? {} : { apiPrefixes: opts.timingConfig.apiPrefixes }
37951
38642
  });
37952
38643
  monitor.closeWindow(settleEndedAt, docId);
37953
- const snap = await snapshot(page, snapOpts);
38644
+ const read = () => snapshot(page, {
38645
+ ...snapOpts,
38646
+ navigationWaitMs: Math.max(1, ceiling),
38647
+ awaitNavigation: async (remainingMs) => {
38648
+ await monitor.waitSettled({ quietMs, ceilingMs: Math.max(1, remainingMs) });
38649
+ }
38650
+ });
38651
+ let snap;
38652
+ try {
38653
+ snap = await read();
38654
+ if (snap.controls.length === 0 && stuckBusy === null) {
38655
+ const left = () => ceiling - (clock.now() - started);
38656
+ if (left() > 0 && await busyOverlay(page) !== null) {
38657
+ while (left() > 0 && !page.isClosed() && await busyOverlay(page) !== null)
38658
+ await clock.sleep(Math.min(250, Math.max(1, left())));
38659
+ if (left() > 0 && !page.isClosed()) {
38660
+ settle2 = await monitor.waitSettled({ quietMs, ceilingMs: Math.max(1, left()) });
38661
+ snap = await read();
38662
+ }
38663
+ }
38664
+ }
38665
+ } catch (e) {
38666
+ if (!(e instanceof PageNavigatingError))
38667
+ throw e;
38668
+ const url2 = redactUrl(page.url());
38669
+ return {
38670
+ rendered: false,
38671
+ snapshot: { url: page.url(), controls: [], truncated: false, signature: contentHash({ url: url2, navigating: true }) },
38672
+ settle: settle2,
38673
+ timing,
38674
+ hang: null,
38675
+ reason: `${e.message} within ${ceiling}ms`
38676
+ };
38677
+ }
37954
38678
  const evidence = pendingEvidence(settle2.pending, settleEndedAt);
37955
38679
  const kind = classifyHang({
37956
38680
  responsive: true,
@@ -38013,9 +38737,8 @@ function affordedOp(c) {
38013
38737
  return "type";
38014
38738
  return "click";
38015
38739
  }
38016
- var MESSAGE_INPUT_TYPES = /* @__PURE__ */ new Set(["", "text", "search"]);
38017
38740
  function sendable(c) {
38018
- if (!c.enabled || !MESSAGE_FIELD.test(c.name))
38741
+ if (!c.enabled || !MESSAGE_FIELD.test(c.name) || c.conversational !== true)
38019
38742
  return false;
38020
38743
  if (c.tag === "textarea")
38021
38744
  return true;
@@ -38023,7 +38746,6 @@ function sendable(c) {
38023
38746
  return MESSAGE_INPUT_TYPES.has(c.inputType ?? "");
38024
38747
  return c.role === "textbox";
38025
38748
  }
38026
- var MESSAGE_FIELD = /\b(reply|message|ask|chat|inquiry|prompt|say|talk|conversation)\b/i;
38027
38749
  var TARGET_FREE_ACTIONS = [
38028
38750
  { op: "wait", description: "wait for the page to finish updating" },
38029
38751
  { op: "scroll_down", description: "scroll down to reveal more of the page" },
@@ -38418,7 +39140,6 @@ var SEND_REQUEST_SLACK_MS = 5e3;
38418
39140
  var REPLY_QUIET_MS = 1e3;
38419
39141
  var MIN_REPLY_CHARS = 12;
38420
39142
  var REPLY_KEEP_CHARS = 1500;
38421
- var SUBMIT_NAME = /\b(send|submit|reply|ask|post)\b|[→➤➔↑]|^\s*(go|ok)\s*$/i;
38422
39143
  var BUSY_TEXT = /^(thinking|typing|loading|generating|sending|working|searching|preparing|processing|analy[sz]ing|drafting|please wait|one moment)\b.{0,60}$/i;
38423
39144
  var PENDING_STATUS = /\b(waiting for|confirmation pending|pending confirmation|may take (a few |several )?(seconds|minutes|a moment)|in progress|still working|hang tight|please wait)\b/i;
38424
39145
  var TRAILING_STATUS = /^.{0,60}(…|\.\.\.)$/;
@@ -38562,6 +39283,9 @@ async function readPageHeadings(page, secrets = []) {
38562
39283
  return { heading: redactPageText(h.heading, secrets), title: redactPageText(h.title, secrets) };
38563
39284
  }
38564
39285
  async function waitForReply(page, opts) {
39286
+ return monitorFor(page).explicitWait(() => listenForReply(page, opts));
39287
+ }
39288
+ async function listenForReply(page, opts) {
38565
39289
  const idleMs = opts.timeoutMs ?? REPLY_WAIT_MS;
38566
39290
  const ceilingMs = Math.max(idleMs, opts.ceilingMs ?? REPLY_CEILING_MS);
38567
39291
  const quietMs = opts.quietMs ?? REPLY_QUIET_MS;
@@ -38584,7 +39308,7 @@ async function waitForReply(page, opts) {
38584
39308
  });
38585
39309
  for (; ; ) {
38586
39310
  if (remaining() <= 0)
38587
- return result(false, "ceiling");
39311
+ return result(false, clock.now() - lastActivity >= idleMs ? "idle" : "ceiling");
38588
39312
  const text2 = await readPageText(page, secrets);
38589
39313
  const fresh = newTurnText(opts.baseline, text2, sent);
38590
39314
  const t = clock.now();
@@ -38622,6 +39346,9 @@ async function waitForReply(page, opts) {
38622
39346
  }
38623
39347
  }
38624
39348
  async function waitForChange(page, timeoutMs) {
39349
+ return monitorFor(page).explicitWait(() => watchForChange(page, timeoutMs));
39350
+ }
39351
+ async function watchForChange(page, timeoutMs) {
38625
39352
  const before = await readPageText(page);
38626
39353
  const changed = page.waitForFunction((b2) => (document.body ? document.body.innerText : "") !== b2, before, {
38627
39354
  timeout: Math.max(1, timeoutMs),
@@ -38633,7 +39360,7 @@ async function waitForChange(page, timeoutMs) {
38633
39360
  return clockBounded(changed, Math.max(1, timeoutMs), false);
38634
39361
  }
38635
39362
  async function stillBusy(page) {
38636
- if (monitorFor(page).pending().length > 0)
39363
+ if (monitorFor(page).pendingWork().length > 0)
38637
39364
  return true;
38638
39365
  return await page.evaluate(visibleBusyIndicator).catch(() => null) !== null;
38639
39366
  }
@@ -38946,28 +39673,55 @@ function exactLiterals(goal) {
38946
39673
  const re = new RegExp(String.raw`\bexactly\s*[:=]?\s*(?:${quoted}|(?!["“'‘\u0060])(\S.{0,499}?)(?=\.(?:\s|$)|[\n;]|\s*$))`, "giu");
38947
39674
  return [...goal.matchAll(re)].map((m) => (m[1] ?? m[2] ?? "").trim()).filter((v) => v !== "");
38948
39675
  }
39676
+ var INSTRUCTION_VERB_LIST = "add create make set enter type fill write test check verify ensure confirm click open go navigate visit connect disconnect save submit send ask select choose change edit update delete remove upload try run start finish complete configure enable disable sign log invite import export publish";
39677
+ var VALUE_SPAN_MARKER = String.raw`\b(?:(?:an?|the|some|one)\s+)?(?:(?:short|brief|quick|simple|one-line|one-sentence|few-word)\s+)?(?:description|summary|explanation|bio|blurb|overview|pitch)\s+(?:of|about|for)\b|\b(?:describing|saying|explaining|mentioning|stating|about|that\s+says)\b`;
39678
+ var VALUE_SPAN = String.raw`\s*[:,]?\s*((?:\([^()\n]{0,300}\)|[^,;:.()\n])+?)(?=\s*(?:[,;:\n]|\.(?:\s|$))|\s+(?:and\s+)?then\s|\s+and\s+(?:${INSTRUCTION_VERB_LIST.replace(/ /g, "|")})\b|\s*$)`;
39679
+ function goalValueSpans(goal) {
39680
+ const spans = [];
39681
+ const cut3 = (re, s3) => s3.replace(re, (m, ...g) => {
39682
+ const span = typeof g[0] === "string" ? g[0] : "";
39683
+ if (span.trim() === "")
39684
+ return m;
39685
+ spans.push(span.trim());
39686
+ const at3 = m.lastIndexOf(span);
39687
+ return `${m.slice(0, at3)} ; ${m.slice(at3 + span.length)}`;
39688
+ });
39689
+ let prose = cut3(/["“'‘\u0060]([^"”'’\u0060\n]{1,200})["”'’\u0060]/gu, goal);
39690
+ for (const literal2 of exactLiterals(goal)) {
39691
+ spans.push(literal2);
39692
+ prose = prose.split(literal2).join(" ; ");
39693
+ }
39694
+ prose = cut3(new RegExp(String.raw`(?:${VALUE_SPAN_MARKER})${VALUE_SPAN}`, "giu"), prose);
39695
+ prose = cut3(/\(([^()\n]{1,300})\)/gu, prose);
39696
+ return { spans, prose };
39697
+ }
38949
39698
  function echoesGoal(value, goal) {
38950
39699
  const v = words2(value);
38951
39700
  const g = words2(goal);
38952
39701
  if (v.length === 0 || g.length === 0)
38953
39702
  return null;
38954
39703
  const flat = v.join(" ");
38955
- const goalFlat = ` ${g.join(" ")} `;
38956
39704
  if (flat === g.join(" "))
38957
39705
  return "it is the goal text";
38958
- if (quotedSegments(goal).some((q) => words2(q).join(" ") === flat))
39706
+ const { spans, prose } = goalValueSpans(goal);
39707
+ if (spans.some((q) => words2(q).join(" ") === flat))
38959
39708
  return null;
39709
+ const valueWords = new Set(spans.flatMap(words2));
39710
+ const instructionWords = new Set(words2(prose).filter((w) => !valueWords.has(w) && !ECHO_STOP_WORDS.has(w)));
39711
+ const goalFlat = ` ${g.join(" ")} `;
38960
39712
  for (let i = 0; i < v.length; i++) {
38961
39713
  let j = i;
38962
39714
  while (j < v.length && goalFlat.includes(` ${v.slice(i, j + 1).join(" ")} `))
38963
39715
  j++;
38964
- const run = v.slice(i, j).join(" ");
38965
- if (j - i >= 5 && run.length >= GOAL_ECHO_RUN_CHARS)
39716
+ const runWords = v.slice(i, j);
39717
+ const run = runWords.join(" ");
39718
+ const instructional = new Set(runWords.filter((w) => instructionWords.has(w))).size >= 2;
39719
+ if (j - i >= 5 && run.length >= GOAL_ECHO_RUN_CHARS && instructional)
38966
39720
  return `it copies the goal ("${run.slice(0, 60)}")`;
38967
39721
  }
38968
39722
  const vs = new Set(v.filter((w) => !ECHO_STOP_WORDS.has(w)));
38969
39723
  if (vs.size >= ECHO_MIN_WORDS) {
38970
- const gs = new Set(g);
39724
+ const gs = instructionWords;
38971
39725
  const overlap = [...vs].filter((w) => gs.has(w)).length / vs.size;
38972
39726
  if (overlap > GOAL_ECHO_OVERLAP)
38973
39727
  return `it restates the goal (${Math.round(overlap * 100)}% of its words are the goal's)`;
@@ -38976,7 +39730,7 @@ function echoesGoal(value, goal) {
38976
39730
  }
38977
39731
  var INSTRUCTION_MIN_WORDS = 4;
38978
39732
  var INSTRUCTION_COVERAGE = 0.8;
38979
- var INSTRUCTION_VERBS = new Set("add create make set enter type fill write test check verify ensure confirm click open go navigate visit connect disconnect save submit send ask select choose change edit update delete remove upload try run start finish complete configure enable disable sign log invite import export publish".split(" "));
39733
+ var INSTRUCTION_VERBS = new Set(INSTRUCTION_VERB_LIST.split(" "));
38980
39734
  var CLAUSE_OPENERS = /* @__PURE__ */ new Set(["and", "then", "also", "please", "next", "finally", "first"]);
38981
39735
  var VALUE_MARKERS = /* @__PURE__ */ new Set([":", "named", "called", "titled", "saying", "says", "reading", "reads", "text", "message", "content", "description", "title", "label", "value", "as"]);
38982
39736
  function copiesGoalInstruction(value, goal) {
@@ -38984,9 +39738,10 @@ function copiesGoalInstruction(value, goal) {
38984
39738
  if (v.length < INSTRUCTION_MIN_WORDS)
38985
39739
  return null;
38986
39740
  const flatValue = v.join(" ");
38987
- if (quotedSegments(goal).some((q) => ` ${words2(q).join(" ")} `.includes(` ${flatValue} `)))
39741
+ const { spans, prose: instructionProse } = goalValueSpans(goal);
39742
+ if (spans.some((q) => ` ${words2(q).join(" ")} `.includes(` ${flatValue} `)))
38988
39743
  return null;
38989
- const prose = goal.replace(/["“'‘\u0060]([^"”'’\u0060\n]{1,200})["”'’\u0060]/gu, " ; ").toLowerCase().match(/[\p{L}\p{N}$]+(?:['’][\p{L}]+)?|:/gu) ?? [];
39744
+ const prose = instructionProse.toLowerCase().match(/[\p{L}\p{N}$]+(?:['’][\p{L}]+)?|:/gu) ?? [];
38990
39745
  const runAt = (i) => {
38991
39746
  let best = { start: -1, len: 0 };
38992
39747
  for (let s3 = 0; s3 < prose.length; s3++) {
@@ -39055,6 +39810,13 @@ var FieldValueLog = class _FieldValueLog {
39055
39810
  #used = /* @__PURE__ */ new Map();
39056
39811
  /** What the latest submit sent, per field — undone by a reload (#184). */
39057
39812
  #lastBatch = /* @__PURE__ */ new Map();
39813
+ /**
39814
+ * #366: the values submitted into each field's CURRENT instance — forgotten when the field left
39815
+ * the page and came back empty (a confirm dialog closed and reopened): a new instance holds none.
39816
+ */
39817
+ #live = /* @__PURE__ */ new Map();
39818
+ /** #366: fields with live values that were absent from the latest page view. */
39819
+ #gone = /* @__PURE__ */ new Set();
39058
39820
  static key(label2) {
39059
39821
  return bareLabel(label2).toLowerCase();
39060
39822
  }
@@ -39068,27 +39830,54 @@ var FieldValueLog = class _FieldValueLog {
39068
39830
  this.#lastBatch.clear();
39069
39831
  for (const [k, v] of this.#pending) {
39070
39832
  this.#lastBatch.set(k, v);
39071
- const list = this.#used.get(k) ?? [];
39072
- if (!list.some((u) => sameValue(u, v)))
39073
- list.push(v);
39074
- this.#used.set(k, list);
39833
+ for (const memory of [this.#used, this.#live]) {
39834
+ const list = memory.get(k) ?? [];
39835
+ if (!list.some((u) => sameValue(u, v)))
39836
+ list.push(v);
39837
+ memory.set(k, list);
39838
+ }
39839
+ this.#gone.delete(k);
39075
39840
  }
39076
39841
  this.#pending.clear();
39077
39842
  }
39843
+ /**
39844
+ * #366: the text fields on the page now (label and current value). A field with submitted values
39845
+ * that left the page and is back EMPTY is a new instance (a dialog reopened, a form remounted):
39846
+ * its live values are forgotten, so the same value may be typed into it again. `used` keeps them.
39847
+ */
39848
+ observe(fields) {
39849
+ const present2 = /* @__PURE__ */ new Map();
39850
+ for (const f of fields) {
39851
+ const k = _FieldValueLog.key(f.label);
39852
+ present2.set(k, (present2.get(k) ?? false) || (f.value ?? "").trim() === "");
39853
+ }
39854
+ for (const k of [...this.#live.keys()]) {
39855
+ const empty = present2.get(k);
39856
+ if (empty === void 0)
39857
+ this.#gone.add(k);
39858
+ else if (this.#gone.has(k)) {
39859
+ this.#gone.delete(k);
39860
+ if (empty)
39861
+ this.#live.delete(k);
39862
+ }
39863
+ }
39864
+ }
39078
39865
  /**
39079
39866
  * The page was reloaded (#184): the last submit did not take, so what it sent is a retry, not a
39080
39867
  * used item — retyping the same value is allowed again.
39081
39868
  */
39082
39869
  reloaded() {
39083
- for (const [k, list] of this.#used) {
39084
- const last = this.#lastBatch.get(k);
39085
- if (last === void 0)
39086
- continue;
39087
- const rest = list.filter((u) => !sameValue(u, last));
39088
- if (rest.length === 0)
39089
- this.#used.delete(k);
39090
- else
39091
- this.#used.set(k, rest);
39870
+ for (const memory of [this.#used, this.#live]) {
39871
+ for (const [k, list] of memory) {
39872
+ const last = this.#lastBatch.get(k);
39873
+ if (last === void 0)
39874
+ continue;
39875
+ const rest = list.filter((u) => !sameValue(u, last));
39876
+ if (rest.length === 0)
39877
+ memory.delete(k);
39878
+ else
39879
+ memory.set(k, rest);
39880
+ }
39092
39881
  }
39093
39882
  this.#lastBatch.clear();
39094
39883
  this.#pending.clear();
@@ -39097,6 +39886,10 @@ var FieldValueLog = class _FieldValueLog {
39097
39886
  used(label2) {
39098
39887
  return this.#used.get(_FieldValueLog.key(label2)) ?? [];
39099
39888
  }
39889
+ /** #366: the values already submitted into `label`'s current instance (see `observe`), oldest first. */
39890
+ liveUsed(label2) {
39891
+ return this.#live.get(_FieldValueLog.key(label2)) ?? [];
39892
+ }
39100
39893
  };
39101
39894
  var VALUE_END = String.raw`(?=\s*(?:[,;\n]|\s\.|\.(?:\s|$)|\s+(?:and|then)\s)|\s*$)`;
39102
39895
  var WORD_VALUE_END = String.raw`(?=\s*(?:[,;\n]|\s\.|\.(?:\s|$)|\s+(?:and|then|to|in|into|on|for|with)\s)|\s*$)`;
@@ -39189,6 +39982,12 @@ function typedPassages(goal) {
39189
39982
  return [...goal.matchAll(re)].map((m) => (m[1] ?? "").trim()).filter((p) => /\n/.test(p) || words2(p).length >= PASSAGE_MIN_WORDS);
39190
39983
  }
39191
39984
  var PASSAGE_MIN_WORDS = 6;
39985
+ function promptAsksFor(value, texts) {
39986
+ const verb = String.raw`\b(?:type|enter|write|input)\s+(?:in\s+)?(?:the\s+(?:word|text|phrase|name|code)\s+)?`;
39987
+ const quoted = new RegExp(String.raw`${verb}["“'‘\u0060]([^"”'’\u0060\n]{1,80})["”'’\u0060]`, "giu");
39988
+ const bare = new RegExp(String.raw`${verb}([^\s"“”'‘’\u0060]{1,80}?)[.,:;!]?(?=\s+(?:to|below|here|in|into|and|if|for|then)\b|[^\S\n]*(?:\n|$)|[.,:;!]\s)`, "giu");
39989
+ return texts.some((t) => [...t.matchAll(quoted), ...t.matchAll(bare)].some((m) => sameValue(m[1] ?? "", value)));
39990
+ }
39192
39991
  var CONTEXT_CEILING = 4e3;
39193
39992
  var FillHelper = class {
39194
39993
  gen;
@@ -39232,6 +40031,17 @@ var FillHelper = class {
39232
40031
  };
39233
40032
  const several = goalListsSeveral(input2.goal);
39234
40033
  const isUsed = (v) => used.some((u) => sameValue(u, v));
40034
+ const live = req.liveUsed === void 0 ? used : req.liveUsed.map((u) => redactContext(u, secrets).slice(0, 200));
40035
+ const repeats = (v) => {
40036
+ if (!isUsed(v))
40037
+ return false;
40038
+ if (promptAsksFor(v, [input2.fieldLabel, redactContext(req.prompt ?? "", secrets)]))
40039
+ return false;
40040
+ if (live.some((u) => sameValue(u, v)))
40041
+ return true;
40042
+ const stated = field3 === void 0 ? [] : valuesStatedInGoal(input2.goal, input2.fieldLabel, field3);
40043
+ return stated.some((s3) => sameValue(s3, v)) && stated.some((s3) => !isUsed(s3));
40044
+ };
39235
40045
  if (field3 !== void 0) {
39236
40046
  const stated = valuesStatedInGoal(input2.goal, input2.fieldLabel, field3);
39237
40047
  const pick3 = several ? stated.find((v) => !isUsed(v)) : stated.length === 1 ? stated[0] : void 0;
@@ -39252,7 +40062,7 @@ var FillHelper = class {
39252
40062
  };
39253
40063
  const text2 = generated !== null && identity !== null && checkFieldValue(generated, field3, input2.fieldLabel, input2.goal) === null && !copied(generated) ? uniqueIdentity(generated, identity, this.#identityToken) : generated;
39254
40064
  if (field3 !== void 0 && text2 !== null) {
39255
- const rejected = checkFieldValue(text2, field3, input2.fieldLabel, input2.goal) ?? (several && isUsed(text2) ? `repeats ${JSON.stringify(text2.trim().slice(0, 80))}, already submitted into this field \u2014 the goal lists several items: use the next one` : null);
40065
+ const rejected = checkFieldValue(text2, field3, input2.fieldLabel, input2.goal) ?? (several && repeats(text2) ? `repeats ${JSON.stringify(text2.trim().slice(0, 80))}, already submitted into this field \u2014 the goal lists several items: use the next one` : null);
39256
40066
  if (rejected !== null)
39257
40067
  return { text: null, rejected, source: "model" };
39258
40068
  const value = field3.tag === "input" ? inWireFormat(field3, text2.trim()) : text2;
@@ -41199,6 +42009,26 @@ var SaveProgress = class {
41199
42009
 
41200
42010
  // ../explore/dist/side-effects.js
41201
42011
  import { createHash as createHash3 } from "node:crypto";
42012
+ var BOOKKEEPING_RPC = /^(?:(?:Record|Track|Log|Report|Send|Emit|Capture|Ingest)\w*(?:Event|Events|Analytics|Telemetry|Metric|Metrics|Impression|Impressions|PageView|PageViews)|Mark\w*(?:Read|Seen|Viewed|Opened)|Ping|Heartbeat|KeepAlive)$/;
42013
+ var BOOKKEEPING_SEGMENT = /^(?:analytics|telemetry|beacons?|metrics|rum)$/i;
42014
+ var READ_MARKER_SEGMENT = /^(?:mark[-_]?(?:as[-_]?)?)?(?:read|seen|viewed)$/i;
42015
+ function isBookkeepingRequest(r) {
42016
+ if (r.resourceType === "ping")
42017
+ return true;
42018
+ const rpc = rpcMethodOf(r.path);
42019
+ if (rpc !== null)
42020
+ return BOOKKEEPING_RPC.test(rpc.method);
42021
+ const segments3 = pathOf4(r.path).split("/").filter((x) => x !== "");
42022
+ if (segments3.some((x) => BOOKKEEPING_SEGMENT.test(x)))
42023
+ return true;
42024
+ const last = segments3[segments3.length - 1];
42025
+ return last !== void 0 && segments3.length > 1 && READ_MARKER_SEGMENT.test(last);
42026
+ }
42027
+ function screenState(controls, visibleText) {
42028
+ const offered = JSON.stringify(controls.map((c) => [c.role, c.name, c.enabled]));
42029
+ return digest(`${offered}
42030
+ ${visibleText.replace(/\s+/g, " ").replace(/\d+/g, "#").trim()}`);
42031
+ }
41202
42032
  function requestSignature(method, path2) {
41203
42033
  return `${method.toUpperCase()} ${urlTemplate(path2)}`;
41204
42034
  }
@@ -41235,9 +42065,12 @@ var SideEffectGuard = class {
41235
42065
  this.#firstParty = opts.firstParty ?? new FirstPartyOrigins(this.#origins);
41236
42066
  this.#ignored = opts.ignoreRequests ?? (() => false);
41237
42067
  }
41238
- /** Is this request the app's own (#274/#284): first-party and not declared background? */
41239
- #ours(url2) {
41240
- return this.#firstParty.thirdParty(url2) === null && !this.#ignored(url2);
42068
+ /**
42069
+ * Is this request the app's own side effect (#274/#284/#374): first-party, not declared
42070
+ * background, and not bookkeeping (analytics, a beacon, a read marker)?
42071
+ */
42072
+ #ours(r) {
42073
+ return this.#firstParty.thirdParty(r.url) === null && !this.#ignored(r.url) && !isBookkeepingRequest({ path: pathOf4(r.url), ...r.resourceType === void 0 ? {} : { resourceType: r.resourceType } });
41241
42074
  }
41242
42075
  /** How a request is named (#194): path on an allowed origin, else origin + path. */
41243
42076
  #name(url2) {
@@ -41252,8 +42085,9 @@ var SideEffectGuard = class {
41252
42085
  /**
41253
42086
  * A click on `id` (a bare string = an element with no known context) is about to be dispatched:
41254
42087
  * watch what it sends. `label` only names it in reasons and drives the sign-in / back rules.
42088
+ * `state` (#380) is its region's state as it is clicked (`screenState`), when the caller read it.
41255
42089
  */
41256
- beginClick(id, label2, route, at3) {
42090
+ beginClick(id, label2, route, at3, state) {
41257
42091
  const identity = identityOf(id);
41258
42092
  this.#closeOpen();
41259
42093
  if (BACK_NAME.test(label2)) {
@@ -41261,14 +42095,19 @@ var SideEffectGuard = class {
41261
42095
  if (f.route === route)
41262
42096
  this.#fired.delete(k);
41263
42097
  }
41264
- this.#open = { key: recordKey(route, identity), element: identity.element, label: label2, route, at: at3, capture: this.#monitor.startCapture(), values: this.#valuesKey() };
42098
+ this.#open = { key: recordKey(route, identity), element: identity.element, label: label2, route, at: at3, capture: this.#monitor.startCapture(), values: this.#valuesKey(), ...state === void 0 ? {} : { before: state } };
41265
42099
  }
41266
42100
  /**
41267
42101
  * The click's window closes (the next perception has settled the page): the writes it fired are
41268
- * the ones that finished since, plus those still in flight that started after it.
42102
+ * the ones that finished since, plus those still in flight that started after it. `state` (#380)
42103
+ * is the clicked control's region state the window closed on, when the caller read it.
41269
42104
  */
41270
- settle() {
41271
- this.#closeOpen();
42105
+ settle(state) {
42106
+ this.#closeOpen(state);
42107
+ }
42108
+ /** Whether a click's window is open (the caller reads the region state for `settle` only then). */
42109
+ clickOpen() {
42110
+ return this.#open !== null;
41272
42111
  }
41273
42112
  /**
41274
42113
  * The most recently closed click (#130a): did it send ANY request (of any method), by the time its
@@ -41277,7 +42116,7 @@ var SideEffectGuard = class {
41277
42116
  lastClick() {
41278
42117
  return this.#lastClick;
41279
42118
  }
41280
- #closeOpen() {
42119
+ #closeOpen(after) {
41281
42120
  const o = this.#open;
41282
42121
  if (o === null)
41283
42122
  return;
@@ -41286,18 +42125,25 @@ var SideEffectGuard = class {
41286
42125
  const requests = o.capture.requests();
41287
42126
  const unfinished = this.#monitor.unfinished();
41288
42127
  const inflightAny = unfinished.some((r) => r.startedAt >= o.at);
41289
- const done = requests.filter((r) => this.#write(r) && this.#ours(r.url)).map((r) => ({
41290
- method: r.method.toUpperCase(),
41291
- path: this.#name(r.url),
41292
- status: r.status,
41293
- rejected: r.status !== null && r.status >= 400 || r.status === null && r.failed
41294
- }));
41295
- const inflight = unfinished.filter((r) => r.startedAt >= o.at && this.#write({ ...r, path: pathOf4(r.url) }) && this.#ours(r.url));
42128
+ const done = requests.filter((r) => this.#write(r) && this.#ours(r)).map((r) => {
42129
+ const status = effectiveStatus(r);
42130
+ return { method: r.method.toUpperCase(), path: this.#name(r.url), status, rejected: status !== null && status >= 400 || status === null && r.failed };
42131
+ });
42132
+ const inflight = unfinished.filter((r) => r.startedAt >= o.at && this.#write({ ...r, path: pathOf4(r.url) }) && this.#ours(r));
41296
42133
  const pending = inflight.map((r) => ({ method: r.method.toUpperCase(), path: this.#name(r.url), status: null, rejected: false }));
41297
42134
  this.#lastClick = { requestSent: requests.length > 0 || inflightAny, writes: [...done, ...pending] };
41298
42135
  if (done.length + pending.length === 0)
41299
42136
  return;
41300
- this.#fired.set(o.key, { label: o.label, route: o.route, element: o.element, writes: [...done, ...pending], inflight, values: o.values });
42137
+ this.#fired.set(o.key, {
42138
+ label: o.label,
42139
+ route: o.route,
42140
+ element: o.element,
42141
+ writes: [...done, ...pending],
42142
+ inflight,
42143
+ values: o.values,
42144
+ ...o.before === void 0 ? {} : { before: o.before },
42145
+ ...after === void 0 ? {} : { after }
42146
+ });
41301
42147
  }
41302
42148
  /** Writes fired by this run's clicks that are still in flight now. */
41303
42149
  inflight() {
@@ -41336,7 +42182,10 @@ var SideEffectGuard = class {
41336
42182
  }
41337
42183
  /**
41338
42184
  * May `key` be clicked on `route` now? `page` is what the page shows: its control names and alerts
41339
- * (a visible retry affordance, or an error alert, re-allows it).
42185
+ * (a visible retry affordance, or an error alert, re-allows it). `state` (#380) is the control's region state
42186
+ * now (`screenState`): one that differs from both the state the earlier click was made in and the
42187
+ * one its window closed on has moved on, and re-allows a click whose writes all finished. Omit it
42188
+ * (a paid / destructive control) and only the rules above apply.
41340
42189
  */
41341
42190
  check(id, route, page) {
41342
42191
  const identity = identityOf(id);
@@ -41367,11 +42216,15 @@ var SideEffectGuard = class {
41367
42216
  return { refuse: false };
41368
42217
  if (f.values !== this.#valuesKey())
41369
42218
  return { refuse: false };
42219
+ const known = page.state !== void 0 && f.before !== void 0 && f.after !== void 0;
42220
+ if (known && page.state !== f.before && page.state !== f.after && f.writes.every((w) => w.status !== null))
42221
+ return { refuse: false };
42222
+ const unchanged = known ? " (its part of the page has not moved on since)" : "";
41370
42223
  const retyped = this.#values.size > 0 ? " (the inputs hold the same values as when it was sent \u2014 nothing new would be submitted)" : "";
41371
42224
  return {
41372
42225
  refuse: true,
41373
42226
  inflight: false,
41374
- reason: `repeated side effect refused: "${f.label}" already sent ${what} on this page and the page does not offer a retry \u2014 clicking it again would repeat that action${retyped}`
42227
+ reason: `repeated side effect refused: "${f.label}" already sent ${what} on this page and the page does not offer a retry \u2014 clicking it again would repeat that action${retyped}${unchanged}`
41375
42228
  };
41376
42229
  }
41377
42230
  };
@@ -41458,7 +42311,7 @@ var SideEffectLog = class {
41458
42311
  continue;
41459
42312
  const m = this.#owner(r.startedAt);
41460
42313
  if (m !== void 0)
41461
- push(m, r.method, r.url, r.status, r.startedAt ?? m.at);
42314
+ push(m, r.method, r.url, effectiveStatus(r), r.startedAt ?? m.at);
41462
42315
  }
41463
42316
  for (const r of monitor.unfinished()) {
41464
42317
  const path2 = pathOf4(r.url);
@@ -42608,265 +43461,6 @@ function demoOverlayFor(enabled, secrets) {
42608
43461
  return enabled === true ? new DemoOverlay(secrets) : null;
42609
43462
  }
42610
43463
 
42611
- // ../explore/dist/status.js
42612
- var EMPTY_STATUS = { alerts: [], notices: [], invalid: [] };
42613
- var STATUS_CHARS = 200;
42614
- var STATUS_ITEMS = 5;
42615
- function readStatusInPage(limits) {
42616
- const norm5 = (s3) => (s3 ?? "").replace(/\s+/g, " ").trim();
42617
- const cut3 = (s3) => s3.length > limits.chars ? `${s3.slice(0, limits.chars)}\u2026` : s3;
42618
- const shown = (el) => {
42619
- const h = el;
42620
- if (h.closest("[hidden],[aria-hidden=true]") !== null)
42621
- return false;
42622
- const st = window.getComputedStyle(h);
42623
- const r = h.getBoundingClientRect();
42624
- return st.display !== "none" && st.visibility !== "hidden" && r.width > 0 && r.height > 0;
42625
- };
42626
- const w = window;
42627
- if (w.__jevInvalid === void 0) {
42628
- const seen = /* @__PURE__ */ new WeakSet();
42629
- w.__jevInvalid = seen;
42630
- document.addEventListener("invalid", (e) => {
42631
- if (e.target instanceof Element)
42632
- seen.add(e.target);
42633
- }, true);
42634
- }
42635
- const refused = w.__jevInvalid;
42636
- const alerts = [];
42637
- const notices = [];
42638
- const live = document.querySelectorAll("[role=alert],[role=status],[aria-live=polite],[aria-live=assertive],output");
42639
- for (const el of Array.from(live)) {
42640
- if (!shown(el))
42641
- continue;
42642
- const text2 = cut3(norm5(el.innerText));
42643
- if (text2 === "")
42644
- continue;
42645
- if ([...alerts, ...notices].some((a) => a.includes(text2) || text2.includes(a)))
42646
- continue;
42647
- const assertive = el.getAttribute("role") === "alert" || el.getAttribute("aria-live") === "assertive";
42648
- const into = assertive ? alerts : notices;
42649
- if (into.length < limits.items)
42650
- into.push(text2);
42651
- }
42652
- const labelOf2 = (el) => {
42653
- const aria = norm5(el.getAttribute("aria-label"));
42654
- if (aria !== "")
42655
- return aria;
42656
- const labels = el.labels;
42657
- if (labels && labels.length > 0)
42658
- return norm5(labels[0].textContent);
42659
- const owner = el.closest("label");
42660
- if (owner !== null)
42661
- return norm5(owner.textContent);
42662
- return norm5(el.getAttribute("placeholder")) || norm5(el.getAttribute("name")) || el.tagName.toLowerCase();
42663
- };
42664
- const describedBy = (el) => {
42665
- const ids = `${el.getAttribute("aria-errormessage") ?? ""} ${el.getAttribute("aria-describedby") ?? ""}`.trim();
42666
- if (ids === "")
42667
- return "";
42668
- return norm5(ids.split(/\s+/).map((id) => document.getElementById(id)).filter((n3) => n3 !== null && shown(n3)).map((n3) => n3.innerText).join(" "));
42669
- };
42670
- const invalid = [];
42671
- const fields = document.querySelectorAll("input:not([type=hidden]),select,textarea,[aria-invalid=true]");
42672
- for (const el of Array.from(fields)) {
42673
- if (!shown(el))
42674
- continue;
42675
- const f = el;
42676
- const ariaInvalid = norm5(el.getAttribute("aria-invalid")).toLowerCase() === "true";
42677
- const nativeInvalid = typeof f.checkValidity === "function" && f.validity !== void 0 && !f.validity.valid;
42678
- const reached2 = refused.has(el) || typeof f.value === "string" && f.value !== "";
42679
- if (!ariaInvalid && !(nativeInvalid && reached2))
42680
- continue;
42681
- const message2 = norm5(nativeInvalid ? f.validationMessage : "") || describedBy(el) || "marked invalid";
42682
- invalid.push({ name: cut3(labelOf2(el)), message: cut3(message2) });
42683
- if (invalid.length >= limits.items)
42684
- break;
42685
- }
42686
- return { alerts, notices, invalid };
42687
- }
42688
- async function readPageStatus(page) {
42689
- return page.evaluate(readStatusInPage, { chars: STATUS_CHARS, items: STATUS_ITEMS }).catch(() => EMPTY_STATUS);
42690
- }
42691
- function statusDelta(before, now2) {
42692
- const alerts = now2.alerts.filter((a) => !before.alerts.includes(a));
42693
- const notices = now2.notices.filter((a) => !before.notices.includes(a));
42694
- const invalid = now2.invalid.filter((f) => !before.invalid.some((b2) => b2.name === f.name && b2.message === f.message));
42695
- return { alerts, notices, invalid };
42696
- }
42697
- function describeStatus(s3) {
42698
- return [
42699
- ...s3.alerts.map((a) => `alert "${a}"`),
42700
- ...s3.notices.map((a) => `status "${a}"`),
42701
- ...s3.invalid.map((f) => `invalid field "${f.name}": "${f.message}"`)
42702
- ].join("; ");
42703
- }
42704
- function isEmptyStatus(s3) {
42705
- return s3.alerts.length === 0 && s3.notices.length === 0 && s3.invalid.length === 0;
42706
- }
42707
- var IN_PROGRESS_WORDS = "simulating|processing|running|generating|drafting|fetching|rendering|publishing|analy[sz]ing|computing|calculating|loading|preparing|working|thinking|uploading|importing|exporting|syncing|saving|submitting|creating|building|training|indexing|queued|in progress";
42708
- function inProgressInPage(words3) {
42709
- const norm5 = (s3) => (s3 ?? "").replace(/\s+/g, " ").trim();
42710
- const cut3 = (s3) => s3.length > 80 ? `${s3.slice(0, 80)}\u2026` : s3;
42711
- const shown = (el) => {
42712
- const h = el;
42713
- if (h.closest("[hidden],[aria-hidden=true]") !== null)
42714
- return false;
42715
- const st = window.getComputedStyle(h);
42716
- const r = h.getBoundingClientRect();
42717
- return st.display !== "none" && st.visibility !== "hidden" && r.width > 0 && r.height > 0;
42718
- };
42719
- const loose = new RegExp(`\\b(?:${words3})\\b`, "i");
42720
- const trailing = new RegExp(`^\\W*(?:${words3})\\b[^.!?]{0,60}(?:\u2026|\\.\\.\\.)\\s*$`, "i");
42721
- const job = /\b(?:is|are)\s+(?:still\s+|already\s+|currently\s+|now\s+)?(?:running|processing|in progress|being (?:processed|generated|simulated|prepared))\b|\bstill (?:running|working|processing)\b|^\W*in progress\W*$/i;
42722
- for (const el of Array.from(document.querySelectorAll('[aria-busy="true"]'))) {
42723
- if (shown(el))
42724
- return `aria-busy region${norm5(el.innerText) === "" ? "" : ` "${cut3(norm5(el.innerText))}"`}`;
42725
- }
42726
- const live = document.querySelectorAll("[role=status],[aria-live=polite],[aria-live=assertive],[role=progressbar],output");
42727
- for (const el of Array.from(live)) {
42728
- if (!shown(el))
42729
- continue;
42730
- const text2 = norm5(el.innerText || el.getAttribute("aria-valuetext") || el.getAttribute("aria-label"));
42731
- if (text2 !== "" && loose.test(text2))
42732
- return `status "${cut3(text2)}"`;
42733
- if (text2 !== "" && /^\W*[a-z]{3,}ing\b[^.!?]{0,60}(?:…|\.\.\.)\s*$/i.test(text2))
42734
- return `status "${cut3(text2)}"`;
42735
- }
42736
- const body = document.body ? document.body.innerText : "";
42737
- for (const raw of body.split(/\n+/)) {
42738
- const line = norm5(raw);
42739
- if (line === "" || line.length > 200)
42740
- continue;
42741
- if (trailing.test(line) || job.test(line))
42742
- return `status text "${cut3(line)}"`;
42743
- }
42744
- return null;
42745
- }
42746
- async function readInProgressStatus(page) {
42747
- return page.evaluate(inProgressInPage, IN_PROGRESS_WORDS).catch(() => null);
42748
- }
42749
- function workAcknowledgedInPage(words3) {
42750
- const norm5 = (s3) => (s3 ?? "").replace(/\s+/g, " ").trim();
42751
- const shown = (el) => {
42752
- const h = el;
42753
- if (h.closest("[hidden],[aria-hidden=true]") !== null)
42754
- return false;
42755
- const st = window.getComputedStyle(h);
42756
- const r = h.getBoundingClientRect();
42757
- return st.display !== "none" && st.visibility !== "hidden" && r.width > 0 && r.height > 0;
42758
- };
42759
- const label2 = (el) => el instanceof HTMLInputElement ? norm5(el.value) : norm5(el.getAttribute("aria-label") ?? el.innerText ?? el.textContent);
42760
- const disabled = (el) => el.disabled === true || el.getAttribute("aria-disabled") === "true";
42761
- const controls = Array.from(document.querySelectorAll("button,[role=button],input[type=button],input[type=submit]")).filter(shown);
42762
- const progress = new RegExp(`^\\W*(?:${words3})\\b[^.!?]{0,60}(?:\u2026|\\.\\.\\.)?\\s*$`, "i");
42763
- let cancel2 = null;
42764
- let busyControl = null;
42765
- for (const el of controls) {
42766
- const n3 = label2(el);
42767
- if (cancel2 === null && !disabled(el) && /^\W*(?:cancel|stop|abort)\b/i.test(n3))
42768
- cancel2 = `an enabled "${n3.slice(0, 40)}" control`;
42769
- if (busyControl === null && disabled(el) && n3 !== "" && progress.test(n3))
42770
- busyControl = `a disabled "${n3.slice(0, 40)}" control`;
42771
- }
42772
- let bar = null;
42773
- for (const el of Array.from(document.querySelectorAll("[role=progressbar][aria-valuenow],progress[value]"))) {
42774
- if (!shown(el))
42775
- continue;
42776
- const now2 = Number(el.getAttribute("aria-valuenow") ?? el.value);
42777
- const max = Number(el.getAttribute("aria-valuemax") ?? el.max) || 100;
42778
- if (Number.isFinite(now2) && now2 < max) {
42779
- bar = `a progress bar at ${Math.round(now2 / max * 100)}%`;
42780
- break;
42781
- }
42782
- }
42783
- return { busyControl, cancel: cancel2, bar };
42784
- }
42785
- async function readWorkingStatus(page) {
42786
- const ack = await page.evaluate(workAcknowledgedInPage, IN_PROGRESS_WORDS).catch(() => null);
42787
- if (ack === null)
42788
- return null;
42789
- if (ack.busyControl !== null)
42790
- return ack.cancel === null ? ack.busyControl : `${ack.busyControl} and ${ack.cancel}`;
42791
- const extra = ack.cancel ?? ack.bar;
42792
- if (extra === null)
42793
- return null;
42794
- const status = await readInProgressStatus(page);
42795
- return status === null ? null : `${status} with ${extra}`;
42796
- }
42797
- var MAX_DOCUMENTED_WAIT_MS = 30 * 6e4;
42798
- var NUMBER_WORDS = {
42799
- a: 1,
42800
- an: 1,
42801
- one: 1,
42802
- two: 2,
42803
- three: 3,
42804
- four: 4,
42805
- five: 5,
42806
- six: 6,
42807
- seven: 7,
42808
- eight: 8,
42809
- nine: 9,
42810
- ten: 10,
42811
- fifteen: 15,
42812
- twenty: 20,
42813
- thirty: 30,
42814
- forty: 40,
42815
- sixty: 60,
42816
- ninety: 90,
42817
- "a couple of": 3,
42818
- "a couple": 3,
42819
- "a few": 5,
42820
- few: 5,
42821
- several: 10
42822
- };
42823
- var UNIT_MS = [
42824
- [/^(?:seconds?|secs?|s)$/i, 1e3],
42825
- [/^(?:minutes?|mins?|m)$/i, 6e4],
42826
- [/^(?:hours?|hrs?|h)$/i, 36e5]
42827
- ];
42828
- var WAIT_TALK = /\b(?:take|takes|taking|took|wait|waiting|ready|done|finish|finishes|complete|completes|usually|typically|normally|should|may|might|can|up to|less than|under|within|about|around|roughly|approximately|expect)\b/i;
42829
- var DURATION = /\b(\d+(?:\.\d+)?|an?|one|two|three|four|five|six|seven|eight|nine|ten|fifteen|twenty|thirty|forty|sixty|ninety|a couple(?: of)?|a few|few|several)(?:\s*(?:-|–|to)\s*(\d+(?:\.\d+)?))?\s*(seconds?|secs?|minutes?|mins?|hours?|hrs?)\b/gi;
42830
- function documentedWaitMs(line) {
42831
- if (!WAIT_TALK.test(line) || /\bago\b/i.test(line))
42832
- return null;
42833
- let longest = null;
42834
- for (const m of line.matchAll(DURATION)) {
42835
- const [, first, upper, unit] = m;
42836
- const per = UNIT_MS.find(([re]) => re.test(unit ?? ""))?.[1];
42837
- if (per === void 0 || first === void 0)
42838
- continue;
42839
- const n3 = Number(upper ?? first) || NUMBER_WORDS[first.toLowerCase()] || NaN;
42840
- if (!Number.isFinite(n3) || n3 <= 0)
42841
- continue;
42842
- longest = Math.max(longest ?? 0, n3 * per);
42843
- }
42844
- return longest === null ? null : Math.min(longest, MAX_DOCUMENTED_WAIT_MS);
42845
- }
42846
- function durationLinesInPage() {
42847
- const body = document.body ? document.body.innerText : "";
42848
- const out = [];
42849
- for (const raw of body.split(/\n+/)) {
42850
- const line = raw.replace(/\s+/g, " ").trim();
42851
- if (line === "" || line.length > 300)
42852
- continue;
42853
- if (/\b(?:seconds?|secs?|minutes?|mins?|hours?|hrs?)\b/i.test(line))
42854
- out.push(line);
42855
- if (out.length >= 10)
42856
- break;
42857
- }
42858
- return out;
42859
- }
42860
- async function readDocumentedWait(page) {
42861
- const lines = await page.evaluate(durationLinesInPage).catch(() => []);
42862
- for (const line of lines) {
42863
- const ms = documentedWaitMs(line);
42864
- if (ms !== null)
42865
- return { text: line.length > 120 ? `${line.slice(0, 120)}\u2026` : line, ms };
42866
- }
42867
- return null;
42868
- }
42869
-
42870
43464
  // ../explore/dist/read-only.js
42871
43465
  var CHANGE_VERBS = /\b(?:create|add|save|submit|send|post|publish|update|edit|modify|rename|delete|remove|revoke|rotate|buy|purchase|upgrade|downgrade|subscribe|unsubscribe|invite|enable|disable|activate|deactivate|sign ?up|register|archive|restore|import|upload|transfer|assign|ask|reply|fill (?:in|out))\b/i;
42872
43466
  var ASKED_ABOUT = /\b(?:how (?:do|can|would|should|to) (?:i |you |we |users? |one )?|how to |whether (?:i |you |we )?(?:can |could )?|(?:what happens |what if )?(?:if|when|after|before) (?:i |you |we )?|(?:can|could) (?:i|you|we) )\w+/gi;
@@ -43139,6 +43733,60 @@ function buildCrashReport(failure3, signals, heapSamples, opts = {}) {
43139
43733
  };
43140
43734
  }
43141
43735
 
43736
+ // ../explore/dist/loop-cycle.js
43737
+ var CYCLE_REPEATS = 4;
43738
+ var CYCLE_MAX_PERIOD = 2;
43739
+ var LoopCycleDetector = class {
43740
+ repeats;
43741
+ maxPeriod;
43742
+ #window = [];
43743
+ constructor(repeats = CYCLE_REPEATS, maxPeriod = CYCLE_MAX_PERIOD) {
43744
+ this.repeats = repeats;
43745
+ this.maxPeriod = maxPeriod;
43746
+ if (!Number.isInteger(repeats) || repeats < 2)
43747
+ throw new Error(`LoopCycleDetector repeats must be an integer >= 2, got ${String(repeats)}`);
43748
+ if (!Number.isInteger(maxPeriod) || maxPeriod < 1)
43749
+ throw new Error(`LoopCycleDetector maxPeriod must be a positive integer, got ${String(maxPeriod)}`);
43750
+ }
43751
+ /** The steps the current window holds (since the last progress). */
43752
+ get size() {
43753
+ return this.#window.length;
43754
+ }
43755
+ /** Forget the window (a step made progress, or the run's situation changed). */
43756
+ reset() {
43757
+ this.#window = [];
43758
+ }
43759
+ /** Records one executed step; returns the cycle once the window is one, else null. */
43760
+ note(step) {
43761
+ if (step.progress) {
43762
+ this.reset();
43763
+ return null;
43764
+ }
43765
+ const span = this.repeats * this.maxPeriod;
43766
+ this.#window.push(step);
43767
+ if (this.#window.length > span)
43768
+ this.#window.splice(0, this.#window.length - span);
43769
+ if (this.#window.length < span)
43770
+ return null;
43771
+ const labels = /* @__PURE__ */ new Map();
43772
+ const states = /* @__PURE__ */ new Set();
43773
+ for (const s3 of this.#window) {
43774
+ if (!labels.has(s3.action))
43775
+ labels.set(s3.action, s3.label);
43776
+ states.add(s3.state);
43777
+ }
43778
+ if (labels.size > this.maxPeriod || states.size > this.maxPeriod)
43779
+ return null;
43780
+ if (labels.size === 1 && states.size === 1)
43781
+ return null;
43782
+ return { labels: [...labels.values()], states: states.size, period: Math.max(labels.size, states.size), steps: span };
43783
+ }
43784
+ };
43785
+ function describeCycle(v, repeats = CYCLE_REPEATS) {
43786
+ const loop = v.labels.length === 1 ? `${v.labels[0]} over and over` : v.labels.join(" \u2194 ");
43787
+ return `no progress: the run went round a loop \u2014 ${loop} \u2014 ${repeats} times (${v.steps} steps between the same ${v.states} page states, no request sent, nothing new on the page)`;
43788
+ }
43789
+
43142
43790
  // ../explore/dist/action-delta.js
43143
43791
  var DELTA_SNAPSHOT_TIMEOUT_MS = 1500;
43144
43792
  var VOLATILITY_GAP_MS = 1e3;
@@ -44080,6 +44728,9 @@ var stateBesides = (snap, key2) => JSON.stringify([snap.url, snap.controls.map((
44080
44728
  var MAX_EARLY_BLOCKED_REFUSALS = 2;
44081
44729
  var JOB_WAIT_SLICE_MS = 6e4;
44082
44730
  async function waitOutJob(page, budgetMs, stillWorking = async (p) => await readInProgressStatus(p) !== null) {
44731
+ return monitorFor(page).explicitWait(() => sitOutJob(page, budgetMs, stillWorking));
44732
+ }
44733
+ async function sitOutJob(page, budgetMs, stillWorking) {
44083
44734
  const started = clock.now();
44084
44735
  const url2 = safeUrl(page);
44085
44736
  let delay = 1e3;
@@ -44138,6 +44789,24 @@ var actionIdentityOf = (c) => ({
44138
44789
  element: keyOf(c),
44139
44790
  context: JSON.stringify([c.form ?? null, c.container ?? null, c.scope ?? null, c.heading ?? null])
44140
44791
  });
44792
+ function regionOf(el) {
44793
+ const REGION = 'dialog,[role=dialog],[role=alertdialog],form,[role=form],[role=region],[role=group],[role=tabpanel],section,article,li,tr,[role=row],[role=listitem],[class*="card" i],[data-card]';
44794
+ const LANDMARK = "main,[role=main]";
44795
+ const region = el.parentElement?.closest(REGION) ?? el.closest(LANDMARK);
44796
+ if (region === null || region === void 0 || region === document.body || region === document.documentElement)
44797
+ return null;
44798
+ const CONTROL = "a[href],button,input,select,textarea,summary,[role=button],[role=link],[role=tab],[role=checkbox],[role=switch],[role=menuitem],[role=option]";
44799
+ const controls = [...region.querySelectorAll(CONTROL)].filter((c) => c.offsetParent !== null || c.getClientRects().length > 0).map((c) => {
44800
+ const h = c;
44801
+ const name = (c.getAttribute("aria-label") ?? (h.type === "submit" || h.type === "button" ? h.value : "") ?? "") || (c.innerText ?? c.textContent ?? "");
44802
+ return { role: c.getAttribute("role") ?? c.tagName.toLowerCase(), name: name.replace(/\s+/g, " ").trim(), enabled: !h.disabled && c.getAttribute("aria-disabled") !== "true" };
44803
+ });
44804
+ return { text: region.innerText ?? region.textContent ?? "", controls };
44805
+ }
44806
+ async function readRegionState(page, control, secrets) {
44807
+ const region = await descriptorToLocator(page, control.descriptor).first().evaluate(regionOf, void 0, { timeout: 1e3 }).catch(() => null);
44808
+ return region === null ? void 0 : screenState(region.controls, redactPageText(region.text, secrets));
44809
+ }
44141
44810
  function noReply(r) {
44142
44811
  const s3 = Math.round(r.waitedMs / 1e3);
44143
44812
  if (r.endedBy === "ceiling")
@@ -44259,6 +44928,9 @@ async function createRunContext(cfg) {
44259
44928
  ctx.movingScrolls = 0;
44260
44929
  ctx.movingScrollsSignature = null;
44261
44930
  ctx.scrollStreakSignatures = /* @__PURE__ */ new Set();
44931
+ ctx.cycles = new LoopCycleDetector();
44932
+ ctx.cycleAction = null;
44933
+ ctx.cycleMark = clock.now();
44262
44934
  ctx.lastChanceGiven = false;
44263
44935
  ctx.lastChanceTurn = false;
44264
44936
  ctx.fixtureAttached = false;
@@ -44293,6 +44965,7 @@ async function createRunContext(cfg) {
44293
44965
  ctx.lastReportNotFound = false;
44294
44966
  ctx.lastAbsenceUncovered = null;
44295
44967
  ctx.settledClick = null;
44968
+ ctx.clickedRegion = null;
44296
44969
  ctx.wroteOk = false;
44297
44970
  ctx.writeGoal = goalAsksToWrite(cfg.goal);
44298
44971
  ctx.vetoes = new VetoedAnswers();
@@ -44310,6 +44983,7 @@ async function createRunContext(cfg) {
44310
44983
  ctx.idleSteps = 0;
44311
44984
  ctx.idleSince = null;
44312
44985
  ctx.busyWaitedMs = 0;
44986
+ ctx.replyWaitedMs = 0;
44313
44987
  ctx.awaitingReply = false;
44314
44988
  ctx.lastTurn = null;
44315
44989
  ctx.turnWrites = /* @__PURE__ */ new Set();
@@ -44334,13 +45008,17 @@ async function createRunContext(cfg) {
44334
45008
  ctx.scrollsSinceMutation = 0;
44335
45009
  ctx.blockers = {
44336
45010
  failClosed: null,
44337
- target: null
45011
+ target: null,
45012
+ latest: null
44338
45013
  };
44339
45014
  ctx.blockingCause = () => {
44340
45015
  if (ctx.blockers.failClosed !== null)
44341
45016
  return ctx.blockers.failClosed;
44342
- if (ctx.blockers.target !== null)
44343
- return ctx.blockers.target.text;
45017
+ const { target, latest } = ctx.blockers;
45018
+ if (latest !== null && (target === null || latest.step > target.step))
45019
+ return latest.text;
45020
+ if (target !== null)
45021
+ return target.text;
44344
45022
  const alert = ctx.status.alerts[0];
44345
45023
  if (alert !== void 0)
44346
45024
  return `the page shows alert ${quote(alert)}`;
@@ -44356,12 +45034,23 @@ async function createRunContext(cfg) {
44356
45034
  if (r !== "target not enabled" && r !== "target not visible")
44357
45035
  return r;
44358
45036
  const name = quote(c.name || c.summary, 120);
44359
- ctx.blockers.target = { key: keyOf(c), text: `${r === "target not enabled" ? "target disabled" : "target not visible"} \u2014 ${name}` };
45037
+ ctx.blockers.target = { key: keyOf(c), text: `${r === "target not enabled" ? "target disabled" : "target not visible"} \u2014 ${name}`, step: ctx.transcript.nextStep };
44360
45038
  return r === "target not enabled" ? `${r}: ${name} is disabled \u2014 its label may say what it needs first` : `${r}: ${name}`;
44361
45039
  };
44362
45040
  ctx.cleared = (c) => {
44363
45041
  if (ctx.blockers.target?.key === keyOf(c))
44364
45042
  ctx.blockers.target = null;
45043
+ if (ctx.blockers.latest?.key === keyOf(c))
45044
+ ctx.blockers.latest = null;
45045
+ };
45046
+ ctx.noteFailure = (op, c, reason, refused) => {
45047
+ const why = (reason ?? "?").replace(/^typed value rejected:\s*/, "");
45048
+ ctx.blockers.latest = {
45049
+ key: keyOf(c),
45050
+ text: `${op} ${quote(c.name || c.summary, 80)} ${refused ? "rejected" : "failed"}: ${quote(why, 200).slice(1, -1)}`,
45051
+ reason: reason ?? "?",
45052
+ step: ctx.transcript.nextStep
45053
+ };
44365
45054
  };
44366
45055
  ctx.noteFailedAct = async (c, reason) => {
44367
45056
  const v = ctx.failedActs.fail(keyOf(c), quote(c.name || c.summary, 80), reason ?? "?");
@@ -44456,6 +45145,7 @@ async function createRunContext(cfg) {
44456
45145
  ctx.jobWaitMs = cfg.jobWaitMs ?? ctx.replyCeilingMs;
44457
45146
  ctx.jobWaitExplicit = cfg.jobWaitMs !== void 0;
44458
45147
  ctx.jobWaitedMs = 0;
45148
+ ctx.overlayWaitedMs = 0;
44459
45149
  ctx.hangWorkWaitedMs = 0;
44460
45150
  ctx.documentedBudgetMs = 0;
44461
45151
  ctx.noteMutation = (label2, descriptor, before, at3, input2, linkFromRoute = null, clickFromRoute = null) => {
@@ -44484,6 +45174,8 @@ async function createRunContext(cfg) {
44484
45174
  }
44485
45175
 
44486
45176
  // ../explore/dist/goal-loop/step.js
45177
+ var TARGET_OPS = /* @__PURE__ */ new Set(["click", "type", "send", "select", "upload", "edit_text"]);
45178
+ var CYCLE_OPS = /* @__PURE__ */ new Set([...TARGET_OPS, "scroll_up", "scroll_down", "reload"]);
44487
45179
  function newStep(ctx, input2) {
44488
45180
  const { cfg } = ctx;
44489
45181
  const { snap, perception, decision } = input2;
@@ -44492,6 +45184,14 @@ function newStep(ctx, input2) {
44492
45184
  const target = extra.control === void 0 ? decision.control : extra.control;
44493
45185
  if (!actOk && extra.origin === "engine")
44494
45186
  ctx.refusedSinceMutation += 1;
45187
+ if (!actOk && target !== null && TARGET_OPS.has(op))
45188
+ ctx.noteFailure(op, target, reason, extra.origin === "engine");
45189
+ if (CYCLE_OPS.has(op)) {
45190
+ ctx.cycleAction = {
45191
+ action: `${op} ${target === null ? "" : keyOf(target)} ${actOk ? "ok" : "failed"}`,
45192
+ label: target === null ? op.replace("_", " ") : `${op} ${quote(target.name || target.summary, 60)}`
45193
+ };
45194
+ }
44495
45195
  if (op === "type" || op === "send")
44496
45196
  ctx.auth.noteTyped(target, snap.url, actOk, target !== null && ctx.isBound(target));
44497
45197
  if ((op === "type" || op === "send") && actOk && target !== null && (ctx.isBound(target) || isCredentialField(target)))
@@ -44586,11 +45286,11 @@ async function handleReport(ctx, step) {
44586
45286
  const { cfg } = ctx;
44587
45287
  const { snap, record: record2 } = step;
44588
45288
  if (ctx.replyGoal) {
44589
- if (ctx.awaitingReply && ctx.lastTurn !== null && ctx.busyWaitedMs < ctx.replyWaitMs) {
45289
+ if (ctx.awaitingReply && ctx.lastTurn !== null && ctx.replyWaitedMs < ctx.replyWaitMs) {
44590
45290
  const t0 = ctx.now();
44591
- const listen = ctx.replyWaitMs - ctx.busyWaitedMs;
45291
+ const listen = ctx.replyWaitMs - ctx.replyWaitedMs;
44592
45292
  const reply = await waitForReply(ctx.page, { secrets: ctx.secrets, ...ctx.lastTurn, timeoutMs: listen, ceilingMs: listen, quietMs: ctx.replyQuietMs });
44593
- ctx.busyWaitedMs += ctx.now() - t0;
45293
+ ctx.replyWaitedMs += ctx.now() - t0;
44594
45294
  if (reply.received) {
44595
45295
  ctx.conversation.latestReply = reply.text;
44596
45296
  ctx.replies.add(snap.url, reply.text);
@@ -44752,7 +45452,7 @@ async function handleBlocked(ctx, step) {
44752
45452
  return "continue";
44753
45453
  }
44754
45454
  const untried = modelControls.filter((c) => c.enabled);
44755
- if (ctx.actionAttempts === 0 && untried.length > 0 && ctx.reportRejections === 0) {
45455
+ if (ctx.actionAttempts === 0 && untried.length > 0 && ctx.reportRejections === 0 && !await (ctx.cfg.successChecksHoldNow?.() ?? Promise.resolve(false)).catch(() => false)) {
44756
45456
  ctx.earlyBlocked += 1;
44757
45457
  if (ctx.earlyBlocked <= MAX_EARLY_BLOCKED_REFUSALS) {
44758
45458
  const nav = [...untried.filter((c) => (c.landmark ?? null) !== null), ...untried.filter((c) => (c.landmark ?? null) === null)];
@@ -44782,11 +45482,14 @@ async function handleWaitOrScroll(ctx, step) {
44782
45482
  const { snap, decision, record: record2 } = step;
44783
45483
  let changed;
44784
45484
  let note;
44785
- if (decision.op === "wait" && ctx.awaitingReply && ctx.lastTurn !== null && ctx.busyWaitedMs < ctx.replyWaitMs) {
45485
+ if (decision.op === "wait" && ctx.awaitingReply && ctx.lastTurn !== null) {
45486
+ const left = ctx.replyCeilingMs - ctx.replyWaitedMs;
45487
+ if (left <= 0)
45488
+ return replyCeilingSpent(ctx, step, false);
44786
45489
  const t0 = ctx.now();
44787
- const listen = Math.min(ctx.replyWaitMs - ctx.busyWaitedMs, 2e4);
45490
+ const listen = Math.min(left, 2e4);
44788
45491
  const reply = await waitForReply(ctx.page, { secrets: ctx.secrets, ...ctx.lastTurn, timeoutMs: listen, ceilingMs: listen, quietMs: ctx.replyQuietMs });
44789
- ctx.busyWaitedMs += ctx.now() - t0;
45492
+ ctx.replyWaitedMs += ctx.now() - t0;
44790
45493
  if (reply.received) {
44791
45494
  ctx.conversation.latestReply = reply.text;
44792
45495
  ctx.replies.add(snap.url, reply.text);
@@ -44798,6 +45501,10 @@ async function handleWaitOrScroll(ctx, step) {
44798
45501
  changed = !idle;
44799
45502
  ctx.quietWaits = idle ? ctx.quietWaits + 1 : 0;
44800
45503
  record2(true, note, reply.received ? { reply } : {});
45504
+ if (!reply.received && ctx.replyWaitedMs >= ctx.replyCeilingMs) {
45505
+ ctx.history.push(note);
45506
+ return replyCeilingSpent(ctx, step, true);
45507
+ }
44801
45508
  } else if (decision.op === "wait" && ctx.jobWaitedMs < ctx.jobWaitMs && await readInProgressStatus(ctx.page) !== null) {
44802
45509
  const job = await readInProgressStatus(ctx.page) ?? "an in-progress status";
44803
45510
  const w = await waitOutJob(ctx.page, Math.min(ctx.jobWaitMs - ctx.jobWaitedMs, JOB_WAIT_SLICE_MS));
@@ -44868,6 +45575,17 @@ async function handleWaitOrScroll(ctx, step) {
44868
45575
  }
44869
45576
  return "continue";
44870
45577
  }
45578
+ function replyCeilingSpent(ctx, step, recorded) {
45579
+ const reason = `stuck: the reply wait is spent \u2014 ${Math.round(ctx.replyWaitedMs / 1e3)}s waited in total for the reply, the ${Math.round(ctx.replyCeilingMs / 1e3)}s reply ceiling (raise --reply-ceiling-ms for a slower reply)`;
45580
+ if (!recorded)
45581
+ step.record(false, reason, { origin: "engine" });
45582
+ ctx.history.push(reason);
45583
+ ctx.lastActedOp = step.decision.op;
45584
+ ctx.statusAfter = "waiting";
45585
+ ctx.incomplete = reason;
45586
+ ctx.stop = "no-progress";
45587
+ return "stop";
45588
+ }
44871
45589
 
44872
45590
  // ../explore/dist/goal-loop/handle-reload.js
44873
45591
  async function handleReload(ctx, step) {
@@ -45276,7 +45994,8 @@ async function handleMessage(ctx, step, op, forcedNote) {
45276
45994
  ctx.replies.add(snap.url, reply.text);
45277
45995
  }
45278
45996
  ctx.awaitingReply = !reply.received;
45279
- ctx.busyWaitedMs = reply.waitedMs;
45997
+ ctx.busyWaitedMs = 0;
45998
+ ctx.replyWaitedMs = reply.waitedMs;
45280
45999
  ctx.lastTurn = { baseline, sent: message2, sentAt: at3, background: sendBackground };
45281
46000
  ctx.offerBaseline = before;
45282
46001
  ctx.history.push(`sent ${quote(message2)} via ${via?.kind === "click" ? `"${via.control.name}"` : "Enter"} \u2192 ` + (reply.received ? `reply: ${quote(reply.text, 300)}` : noReply(reply)));
@@ -45415,10 +46134,14 @@ async function handleClick(ctx, step) {
45415
46134
  const quickReply = ctx.offeredKeys.has(keyOf(control)) && control.role === "button" && control.name.length <= 60 && !/[→›»]/.test(control.name);
45416
46135
  const turn = submits || quickReply;
45417
46136
  const identity = actionIdentityOf(control);
45418
- const repeat = ctx.sideEffects.check(identity, safePath(snap.url), {
45419
- controlNames: snap.controls.map((c) => c.name),
45420
- alerts: ctx.status.alerts
45421
- });
46137
+ const pageNow = { controlNames: snap.controls.map((c) => c.name), alerts: ctx.status.alerts };
46138
+ let repeat = ctx.sideEffects.check(identity, safePath(snap.url), pageNow);
46139
+ const stateless = ctx.safety.riskOf(control) !== null;
46140
+ const readState = () => stateless ? Promise.resolve(void 0) : readRegionState(ctx.page, control, ctx.secrets);
46141
+ if (repeat.refuse && !repeat.inflight && !stateless) {
46142
+ const state = await readState();
46143
+ repeat = ctx.sideEffects.check(identity, safePath(snap.url), { ...pageNow, ...state === void 0 ? {} : { state } });
46144
+ }
45422
46145
  if (repeat.refuse) {
45423
46146
  let note = repeat.reason;
45424
46147
  if (repeat.inflight) {
@@ -45431,7 +46154,9 @@ async function handleClick(ctx, step) {
45431
46154
  return "continue";
45432
46155
  }
45433
46156
  const baseline = turn ? await readPageText(ctx.page, ctx.secrets) : "";
45434
- ctx.sideEffects.beginClick(identity, control.name || control.summary, safePath(snap.url), ctx.now());
46157
+ const before = await readState();
46158
+ ctx.clickedRegion = readState;
46159
+ ctx.sideEffects.beginClick(identity, control.name || control.summary, safePath(snap.url), ctx.now(), before);
45435
46160
  const r = await act(cfg.actor, { op: "click", control });
45436
46161
  let reply;
45437
46162
  let message2;
@@ -45476,7 +46201,8 @@ async function handleClick(ctx, step) {
45476
46201
  ctx.replies.add(snap.url, reply.text);
45477
46202
  }
45478
46203
  ctx.awaitingReply = !reply.received;
45479
- ctx.busyWaitedMs = reply.waitedMs;
46204
+ ctx.busyWaitedMs = 0;
46205
+ ctx.replyWaitedMs = reply.waitedMs;
45480
46206
  ctx.lastTurn = { baseline, sent: message2, sentAt: at3, background: turnBackground };
45481
46207
  ctx.offerBaseline = new Set(keys.keys());
45482
46208
  ctx.history.push(`clicked ${control.name}${quickReply ? " (a quick reply)" : ""} \u2192 ` + (reply.received ? `reply: ${quote(reply.text, 300)}` : noReply(reply)));
@@ -45520,7 +46246,14 @@ async function handleFill(ctx, step) {
45520
46246
  secrets: ctx.secrets,
45521
46247
  // A text field's value is field-scoped and checked before it is typed (#71); in an
45522
46248
  // add-another flow it is the next item, not one already submitted into this field (#123).
45523
- ...decision.op === "type" ? { field: { tag: control.tag, inputType: control.inputType }, alreadyUsed: ctx.valueLog.used(control.name || control.summary) } : {}
46249
+ ...decision.op === "type" ? {
46250
+ field: { tag: control.tag, inputType: control.inputType },
46251
+ alreadyUsed: ctx.valueLog.used(control.name || control.summary),
46252
+ // #366: only a repeat into this same live field is refused (a reopened dialog's field is new),
46253
+ // and never a literal the field's own prompt asks for ("Type CONFIRM to continue").
46254
+ liveUsed: ctx.valueLog.liveUsed(control.name || control.summary),
46255
+ prompt: [control.scope, control.heading, step.visibleText].filter((t) => typeof t === "string" && t !== "").join("\n")
46256
+ } : {}
45524
46257
  }));
45525
46258
  } catch (e) {
45526
46259
  const reason = `value generation unavailable: ${firstLine2(e)}`;
@@ -45622,7 +46355,9 @@ async function perceiveStep(ctx) {
45622
46355
  ctx.history.push(dialogHistoryLine(d));
45623
46356
  ctx.transcript.attachDialogs(ctx.transcript.nextStep - 1, dialogs);
45624
46357
  }
45625
- ctx.sideEffects.settle();
46358
+ const region = ctx.clickedRegion;
46359
+ ctx.clickedRegion = null;
46360
+ ctx.sideEffects.settle(ctx.sideEffects.clickOpen() && region !== null ? await region() : void 0);
45626
46361
  {
45627
46362
  const lc = ctx.sideEffects.lastClick();
45628
46363
  if (lc !== null && lc !== ctx.settledClick) {
@@ -45813,6 +46548,43 @@ async function checkSettled(ctx, step) {
45813
46548
  }
45814
46549
  }
45815
46550
  if (!perception.rendered) {
46551
+ const overlay = perception.hang === null ? await busyOverlay(ctx.page) : null;
46552
+ if (overlay !== null && ctx.overlayWaitedMs < ctx.jobWaitMs) {
46553
+ const w = await waitOutJob(ctx.page, Math.min(ctx.jobWaitMs - ctx.overlayWaitedMs, JOB_WAIT_SLICE_MS), async (p) => await busyOverlay(p) !== null);
46554
+ ctx.overlayWaitedMs += clock.now() - step.perceiveStartedAt;
46555
+ const note = `no visible control while the page shows ${overlay} \u2014 the app is still working; waited ${(w.waitedMs / 1e3).toFixed(1)}s (${w.cleared ? "the overlay cleared" : `still in progress; ${Math.round(ctx.overlayWaitedMs / 1e3)}s of the ${Math.round(ctx.jobWaitMs / 1e3)}s job-wait budget used`})`;
46556
+ ctx.history.push(note);
46557
+ ctx.transcript.record({
46558
+ op: "wait",
46559
+ control: null,
46560
+ confidence: null,
46561
+ chosenBy: "strategy",
46562
+ strategy: "job-wait",
46563
+ actOk: true,
46564
+ reason: note,
46565
+ snapshot: snap,
46566
+ timing: perception.timing
46567
+ });
46568
+ return "continue";
46569
+ }
46570
+ if (overlay !== null) {
46571
+ const reason = `stuck: the page still shows ${overlay} (no visible control) after ${Math.round(ctx.overlayWaitedMs / 1e3)}s of waiting \u2014 past the ${Math.round(ctx.jobWaitMs / 1e3)}s job-wait budget (a status that never completes); raise --job-wait-ms if this job legitimately takes longer`;
46572
+ ctx.transcript.record({
46573
+ op: "wait",
46574
+ control: null,
46575
+ confidence: null,
46576
+ chosenBy: "strategy",
46577
+ strategy: "job-wait",
46578
+ actOk: false,
46579
+ reason,
46580
+ snapshot: snap,
46581
+ timing: perception.timing
46582
+ });
46583
+ ctx.history.push(reason);
46584
+ ctx.incomplete = reason;
46585
+ ctx.stop = "no-progress";
46586
+ return "stop";
46587
+ }
45816
46588
  ctx.transcript.record({
45817
46589
  op: "wait",
45818
46590
  control: null,
@@ -45826,10 +46598,12 @@ async function checkSettled(ctx, step) {
45826
46598
  ctx.stop = "blocked";
45827
46599
  return "stop";
45828
46600
  }
46601
+ ctx.overlayWaitedMs = 0;
45829
46602
  return "next";
45830
46603
  }
45831
46604
 
45832
46605
  // ../explore/dist/goal-loop/progress.js
46606
+ import { createHash as createHash4 } from "node:crypto";
45833
46607
  async function checkProgress(ctx, step) {
45834
46608
  const { cfg } = ctx;
45835
46609
  const { snap } = step;
@@ -45842,11 +46616,12 @@ async function checkProgress(ctx, step) {
45842
46616
  }
45843
46617
  ctx.statusAfter = null;
45844
46618
  }
45845
- const scrolledMoved = (ctx.lastActedOp === "scroll_down" || ctx.lastActedOp === "scroll_up") && ctx.lastScrollMoved;
45846
- if (!scrolledMoved) {
46619
+ const scrolled = ctx.lastActedOp === "scroll_down" || ctx.lastActedOp === "scroll_up";
46620
+ const scrolledMoved = scrolled && ctx.lastScrollMoved;
46621
+ if (!scrolled) {
45847
46622
  ctx.movingScrolls = 0;
45848
46623
  ctx.scrollStreakSignatures.clear();
45849
- } else {
46624
+ } else if (scrolledMoved) {
45850
46625
  if (ctx.scrollStreakSignatures.size === 0 && ctx.movingScrollsSignature !== null)
45851
46626
  ctx.scrollStreakSignatures.add(ctx.movingScrollsSignature);
45852
46627
  if (ctx.scrollStreakSignatures.has(snap.signature))
@@ -45920,11 +46695,36 @@ async function checkProgress(ctx, step) {
45920
46695
  return "stop";
45921
46696
  }
45922
46697
  }
46698
+ const cycle = await noteCycle(ctx, snap);
46699
+ if (cycle !== null) {
46700
+ ctx.history.push(cycle);
46701
+ ctx.incomplete = cycle;
46702
+ ctx.stop = "no-progress";
46703
+ return "stop";
46704
+ }
45923
46705
  if (ctx.noProgress.streak === 0)
45924
46706
  ctx.lastChanceGiven = false;
45925
46707
  ctx.seen.add(snap.signature);
45926
46708
  return "next";
45927
46709
  }
46710
+ async function noteCycle(ctx, snap) {
46711
+ const step = ctx.cycleAction;
46712
+ ctx.cycleAction = null;
46713
+ const mark = ctx.cycleMark;
46714
+ ctx.cycleMark = ctx.now();
46715
+ if (step === null)
46716
+ return null;
46717
+ const monitor = monitorFor(ctx.page);
46718
+ const background = backgroundEndpoints(monitor, mark, ctx.turnWrites);
46719
+ const wrote = writesStartedSince(monitor, mark, ctx.isWrite).some((k) => !background.has(k));
46720
+ const text2 = await readPageText(ctx.page).catch(() => "");
46721
+ const viewport = ctx.page.viewportSize();
46722
+ const pt = { x: (viewport?.width ?? 0) / 2, y: (viewport?.height ?? 0) / 2 };
46723
+ const scroll = await ctx.page.evaluate(scrollPositionAt, pt).catch(() => null);
46724
+ const state = `${snap.signature}|${createHash4("sha1").update(text2).digest("hex").slice(0, 16)}|${scroll ?? "?"}`;
46725
+ const verdict = ctx.cycles.note({ ...step, state, progress: wrote });
46726
+ return verdict === null ? null : describeCycle(verdict);
46727
+ }
45928
46728
 
45929
46729
  // ../explore/dist/goal-loop/page-view.js
45930
46730
  async function viewPage(ctx, step) {
@@ -45960,6 +46760,7 @@ async function viewPage(ctx, step) {
45960
46760
  ctx.save.reset();
45961
46761
  }
45962
46762
  ctx.lastPath = path2;
46763
+ ctx.valueLog.observe(snap.controls.filter((c) => c.tag === "input" || c.tag === "textarea" || c.richText === true).map((c) => ({ label: c.name || c.summary, value: c.value })));
45963
46764
  if (ctx.listsSeveral && ctx.prevSignature !== null && ctx.prevSignature !== snap.signature) {
45964
46765
  const next = ctx.nextFrom.get(snap.signature);
45965
46766
  if (next !== void 0) {
@@ -46128,12 +46929,19 @@ async function finishRun(ctx) {
46128
46929
  if (ctx.lastReportNotFound && ctx.answer === void 0 && (ctx.stop === "no-progress" || ctx.stop === "blocked") && ctx.failure === void 0) {
46129
46930
  ctx.incomplete = answerNotFoundReason(ctx.observed.pages());
46130
46931
  }
46932
+ if (ctx.awaitingReply && ctx.answer === void 0 && ctx.failure === void 0 && (ctx.stop === "no-progress" || ctx.stop === "blocked" || ctx.stop === "exhausted")) {
46933
+ const sent = ctx.lastTurn?.sent;
46934
+ const missing = `no reply within ${Math.round(ctx.replyWaitedMs / 1e3)}s to the last message sent${sent === void 0 || sent === "" ? "" : ` (${quote(sent, 80)})`}`;
46935
+ ctx.incomplete = `${missing}; ${incompleteReason(ctx.stop, ctx.incomplete, ctx.failure, ctx.hang, ctx.tracker)}`;
46936
+ }
46131
46937
  await ctx.readOnly?.disarm();
46132
46938
  ctx.page.off("request", ctx.onRequestSeen);
46133
46939
  ctx.page.off("response", ctx.onDocumentResponse);
46134
46940
  const finished = ctx.recorder.tryFinish({ intent: cfg.goal });
46135
46941
  const cause = ctx.blockingCause();
46136
- const finalOutcome = ctx.stop === "done" && ctx.outcome !== null && finished.ok ? ctx.outcome : { status: "incomplete", reason: withCause(incompleteReason(ctx.stop, ctx.incomplete, ctx.failure, ctx.hang, ctx.tracker), ctx.stop, cause) };
46942
+ const latest = ctx.blockers.latest;
46943
+ const reasonCause = cause !== null && latest !== null && cause === latest.text && ctx.incomplete !== null && ctx.incomplete.includes(latest.reason) ? null : cause;
46944
+ const finalOutcome = ctx.stop === "done" && ctx.outcome !== null && finished.ok ? ctx.outcome : { status: "incomplete", reason: withCause(incompleteReason(ctx.stop, ctx.incomplete, ctx.failure, ctx.hang, ctx.tracker), ctx.stop, reasonCause) };
46137
46945
  if (!finished.ok) {
46138
46946
  ctx.failure = ctx.failure ?? { kind: "exception", message: `recording rejected: ${finished.reason}` };
46139
46947
  ctx.stop = "crashed";
@@ -46238,6 +47046,8 @@ async function explore(cfg) {
46238
47046
  const settled = await checkSettled(ctx, seen);
46239
47047
  if (settled === "stop")
46240
47048
  break;
47049
+ if (settled === "continue")
47050
+ continue;
46241
47051
  const progressed = await checkProgress(ctx, seen);
46242
47052
  if (progressed === "stop")
46243
47053
  break;
@@ -46551,18 +47361,18 @@ function nearMissHint(requests, method, pathGlob2) {
46551
47361
  return null;
46552
47362
  const groups = /* @__PURE__ */ new Map();
46553
47363
  for (const r of candidates) {
46554
- const key2 = `${r.method} ${r.path} ${r.status ?? "none"}`;
47364
+ const status = describeStatusWithRpc(r);
47365
+ const key2 = `${r.method} ${r.path} ${status}`;
46555
47366
  const g = groups.get(key2);
46556
47367
  if (g === void 0)
46557
- groups.set(key2, { method: r.method, path: r.path, status: r.status, count: 1 });
47368
+ groups.set(key2, { method: r.method, path: r.path, status, count: 1 });
46558
47369
  else
46559
47370
  g.count += 1;
46560
47371
  }
46561
47372
  const top = [...groups.values()].sort((a, b2) => b2.count - a.count)[0];
46562
47373
  if (top === void 0)
46563
47374
  return null;
46564
- const statusText = top.status === null ? "no response" : String(top.status);
46565
- return `saw ${top.method} ${top.path} \u2192 ${statusText} (${top.count}\xD7)`;
47375
+ return `saw ${top.method} ${top.path} \u2192 ${top.status} (${top.count}\xD7)`;
46566
47376
  }
46567
47377
  function evaluateNetworkCheck(check2, requests, truncated = false) {
46568
47378
  const spec = describeCheck(check2);
@@ -46593,8 +47403,11 @@ function evaluateNetworkCheck(check2, requests, truncated = false) {
46593
47403
  detail: `expected ${describeStatus2(check2.status)}, but the ${inFlight} matching request(s) were sent and are still awaiting a response`
46594
47404
  };
46595
47405
  }
46596
- const seen = answered.map((r) => r.status === null ? "no response" : String(r.status));
46597
- const bad = answered.filter((r) => r.status === null || !statusMatches(r.status, check2.status));
47406
+ const seen = answered.map(describeStatusWithRpc);
47407
+ const bad = answered.filter((r) => {
47408
+ const status = effectiveStatus(r);
47409
+ return status === null || !statusMatches(status, check2.status);
47410
+ });
46598
47411
  const waiting = inFlight === 0 ? "" : ` (${inFlight} more still awaiting a response)`;
46599
47412
  return bad.length === 0 ? { check: spec, passed: true, detail: `${answered.length} matching request(s), status ${[...new Set(seen)].join(", ")}${waiting}` } : {
46600
47413
  check: spec,
@@ -46664,13 +47477,7 @@ async function connectCodeOf(response) {
46664
47477
  const buf = await response.body().catch(() => null);
46665
47478
  if (buf === null || buf.length > MAX_BODY_BYTES)
46666
47479
  return null;
46667
- try {
46668
- const body = JSON.parse(buf.toString("utf8"));
46669
- const code = body !== null && typeof body === "object" && "code" in body ? body.code : null;
46670
- return typeof code === "string" && /^[a-z_]+$/.test(code) ? code : null;
46671
- } catch {
46672
- return null;
46673
- }
47480
+ return parseConnectUnaryError(buf.toString("utf8"));
46674
47481
  }
46675
47482
  function pageOf(actor) {
46676
47483
  return actor.ability(BrowseTheWebToken).session.page;
@@ -48062,8 +48869,11 @@ async function adjudicatedRun(cfg, checks, page, capture2, declared) {
48062
48869
  eachSawNot[i] = true;
48063
48870
  else if (eachSawNot[i])
48064
48871
  eachHeldAt[i] = settledSteps;
48065
- else if (settledSteps === 1)
48872
+ else if (settledSteps === 1) {
48066
48873
  eachHeldAtStart[i] = true;
48874
+ if (allowVacuous)
48875
+ eachHeldAt[i] = 1;
48876
+ }
48067
48877
  }
48068
48878
  return;
48069
48879
  }
@@ -48074,12 +48884,15 @@ async function adjudicatedRun(cfg, checks, page, capture2, declared) {
48074
48884
  sawNotHolding = true;
48075
48885
  else if (sawNotHolding)
48076
48886
  heldAtStep = settledSteps;
48077
- else if (settledSteps === 1)
48887
+ else if (settledSteps === 1) {
48078
48888
  heldAtStart = true;
48889
+ if (allowVacuous)
48890
+ heldAtStep = 1;
48891
+ }
48079
48892
  },
48080
48893
  ...stopWhenHeld ? {
48081
48894
  successMetNow: async () => {
48082
- if (settledSteps < 2)
48895
+ if (settledSteps < 2 && !allowVacuous)
48083
48896
  return null;
48084
48897
  if (pageChecks.length > 0 && (each ? eachHeldAt.some((at3) => at3 === null) : heldAtStep === null))
48085
48898
  return null;
@@ -48114,6 +48927,11 @@ async function adjudicatedRun(cfg, checks, page, capture2, declared) {
48114
48927
  ...answerRequired ? { requireAnswer: true } : {},
48115
48928
  // #235: only `reloadThen` checks — the in-run check below evaluates none of them.
48116
48929
  ...checks.every((c) => c.kind === "reloadThen") ? { successCheckDeferred: true } : {},
48930
+ // #377: the checks as they read now (no held/each transition rule) — a `blocked` while
48931
+ // they hold is not refused as "nothing tried yet".
48932
+ ...checks.every((c) => c.kind === "reloadThen") ? {} : {
48933
+ successChecksHoldNow: () => evaluateChecks(cfg, checks.filter((c) => c.kind !== "reloadThen"), page, capture2, scope()).then((rs) => rs.every((r) => r.passed), () => false)
48934
+ },
48117
48935
  successCheck: () => evaluateChecks(cfg, checks.filter((c) => c.kind !== "reloadThen"), page, capture2, scope()).then((rs) => rs.every((r) => each && isPageCheck(r, pageChecks) ? (
48118
48936
  // #337: under `each` a page check counts once IT went from not holding to holding.
48119
48937
  eachHeldAt[pageIndexOf(r)] !== null || r.passed && eachSawNot[pageIndexOf(r)] === true
@@ -48927,39 +49745,6 @@ var CampaignSpecSchema = external_exports.object({
48927
49745
  jobs: external_exports.array(CampaignJobSchema).min(1).max(CAMPAIGN_LIMITS.maxJobs)
48928
49746
  }).strict();
48929
49747
 
48930
- // ../explore/dist/authoring/value-capturing-generation-port.js
48931
- var ValueCapturingGenerationPort = class {
48932
- inner;
48933
- captured = [];
48934
- constructor(inner) {
48935
- this.inner = inner;
48936
- }
48937
- async generate(kind, input2) {
48938
- const result = await this.inner.generate(kind, input2);
48939
- const text2 = extractText(result.output);
48940
- if (text2 !== void 0)
48941
- this.captured.push(text2);
48942
- return result;
48943
- }
48944
- capturedValues(recording) {
48945
- const fillSteps = flattenBaseFillSteps(recording);
48946
- if (fillSteps.length !== this.captured.length) {
48947
- throw new Error(`ValueCapturingGenerationPort: recorded ${this.captured.length} generate() call(s) but the recording has ${fillSteps.length} fill/select step(s) \u2014 cannot correlate values 1:1`);
48948
- }
48949
- const values2 = /* @__PURE__ */ new Map();
48950
- fillSteps.forEach(({ ref }, i) => values2.set(`${ref.page}:${ref.step}`, this.captured[i]));
48951
- return values2;
48952
- }
48953
- };
48954
- function extractText(output3) {
48955
- if (output3 && typeof output3 === "object" && "text" in output3) {
48956
- const text2 = output3.text;
48957
- if (typeof text2 === "string")
48958
- return text2;
48959
- }
48960
- return void 0;
48961
- }
48962
-
48963
49748
  // ../explore/dist/authoring/auto-decide.js
48964
49749
  function autoDecidePostdoc(base, diff2) {
48965
49750
  const baseFillSteps = flattenBaseFillSteps(base);
@@ -48997,6 +49782,21 @@ function pickAutoName(columnClass, originalIndex, used) {
48997
49782
  }
48998
49783
 
48999
49784
  // ../explore/dist/authoring/author-journey.js
49785
+ function goalResultDiagnostics(r) {
49786
+ const failure3 = r.run.failure ?? r.failure;
49787
+ const reason = r.reason ?? failure3?.message;
49788
+ return {
49789
+ outcome: r.outcome,
49790
+ ...reason === void 0 ? {} : { reason },
49791
+ ...r.run.stop === void 0 ? {} : { stop: r.run.stop },
49792
+ ...r.run.outcome === void 0 ? {} : { runOutcome: r.run.outcome },
49793
+ ...failure3 === void 0 ? {} : { failure: { kind: failure3.kind, message: failure3.message } },
49794
+ ...r.checks === void 0 ? {} : { checks: r.checks },
49795
+ ...r.run.decisions === void 0 ? {} : { decisions: r.run.decisions },
49796
+ ...r.run.actions === void 0 ? {} : { actions: r.run.actions },
49797
+ ...r.finalUrl === void 0 ? {} : { finalUrl: r.finalUrl }
49798
+ };
49799
+ }
49000
49800
  async function authorJourney(req) {
49001
49801
  const takes = req.takes ?? 1;
49002
49802
  if (takes < 1)
@@ -49011,45 +49811,43 @@ async function authorJourney(req) {
49011
49811
  if (reload !== void 0) {
49012
49812
  throw new Error(`authorJourney: ${describeCheck(reload)} can't be authored into a Journey yet \u2014 use a page check or a requestMade/responseStatus check`);
49013
49813
  }
49014
- const mission = { successChecks: checks };
49015
- const discoveryGeneration = new ValueCapturingGenerationPort(req.generation);
49016
- const discovery = await runGoalBasedMission({
49017
- goal: req.goal,
49018
- ...mission,
49019
- allowlist: req.allowlist,
49020
- startUrl: req.startUrl,
49021
- bounds: req.bounds,
49022
- actor: req.actor,
49023
- judge: req.judgment,
49024
- gen: discoveryGeneration,
49025
- ...req.safety === void 0 ? {} : { safety: req.safety }
49026
- });
49814
+ const runTake = req.runTake ?? inProcessTake(req);
49815
+ const discovery = await runTake({ index: 0, goal: req.goal, successChecks: checks });
49027
49816
  if (discovery.outcome !== "succeeded") {
49028
- return { outcome: "not-reached", reason: `discovery mission ${discovery.outcome}` };
49817
+ const why = discovery.diagnostics.reason;
49818
+ return {
49819
+ outcome: "not-reached",
49820
+ reason: `discovery mission ${discovery.outcome}${why === void 0 ? "" : `: ${why}`}`,
49821
+ discovery: discovery.diagnostics,
49822
+ takes: { requested: takes, run: 1, succeeded: 0 }
49823
+ };
49029
49824
  }
49030
- const authoringTakes = [
49031
- { recording: discovery.recording, values: discoveryGeneration.capturedValues(discovery.recording) }
49032
- ];
49825
+ const authoringTakes = [authoringTake(discovery.recording)];
49826
+ const corroborating = [];
49033
49827
  for (let i = 1; i < takes; i++) {
49034
- const replayGeneration = new ValueCapturingGenerationPort(req.generation);
49035
- const replay = await runGoalBasedMission({
49036
- goal: req.goal,
49037
- ...mission,
49038
- allowlist: req.allowlist,
49039
- startUrl: req.startUrl,
49040
- bounds: req.bounds,
49041
- actor: req.actor,
49042
- judge: req.judgment,
49043
- gen: replayGeneration,
49044
- ...req.safety === void 0 ? {} : { safety: req.safety }
49045
- });
49828
+ const replay = await runTake({ index: i, goal: req.goal, successChecks: checks });
49829
+ corroborating.push(replay.diagnostics);
49046
49830
  if (replay.outcome !== "succeeded")
49047
49831
  continue;
49048
- authoringTakes.push({ recording: replay.recording, values: replayGeneration.capturedValues(replay.recording) });
49832
+ authoringTakes.push(authoringTake(replay.recording));
49049
49833
  }
49050
49834
  const diff2 = diffTakes(authoringTakes);
49051
- const decisions = autoDecidePostdoc(authoringTakes[0].recording, diff2);
49052
- const materializedRecording = applyPostdoc(authoringTakes[0], diff2, decisions);
49835
+ const base = authoringTakes[0];
49836
+ const secretSteps = secretFillSteps(base.recording);
49837
+ const secretParams = [];
49838
+ const decisions = autoDecidePostdoc(base.recording, diff2).map((d) => {
49839
+ const key2 = `${d.step.page}:${d.step.step}`;
49840
+ if (!secretSteps.has(key2))
49841
+ return d;
49842
+ const name = `secret${secretParams.length + 1}`;
49843
+ secretParams.push({
49844
+ name,
49845
+ secret: true,
49846
+ description: "a value code typed during authoring (a --secret-field/--totp binding, or one holding a --secret) \u2014 never kept in the Journey; pass it with --param at replay"
49847
+ });
49848
+ return { step: d.step, classify: "variable", name };
49849
+ });
49850
+ const materializedRecording = applyPostdoc(base, diff2, decisions);
49053
49851
  const parameterizedRecording = checks.reduce((r, c) => c.kind === "page" ? appendSuccessAssertion(r, c.assertion) : r, materializedRecording);
49054
49852
  const networkChecks = checks.flatMap((c) => c.kind === "requestMade" ? [{ kind: "requestMade", method: c.method, pathGlob: c.pathGlob }] : c.kind === "responseStatus" ? [{ kind: "responseStatus", method: c.method, pathGlob: c.pathGlob, status: { ...c.status } }] : []);
49055
49853
  const metadata = {
@@ -49059,9 +49857,58 @@ async function authorJourney(req) {
49059
49857
  params: deriveParamSchema(parameterizedRecording).required,
49060
49858
  authoredBy: "jev-driven",
49061
49859
  createdAtIso: clock.nowIso(),
49062
- ...networkChecks.length === 0 ? {} : { networkChecks }
49860
+ ...networkChecks.length === 0 ? {} : { networkChecks },
49861
+ ...secretParams.length === 0 ? {} : { parameters: secretParams }
49862
+ };
49863
+ return {
49864
+ outcome: "authored",
49865
+ journey: { metadata, recording: parameterizedRecording },
49866
+ discovery: discovery.diagnostics,
49867
+ takes: { requested: takes, run: takes, succeeded: authoringTakes.length },
49868
+ ...corroborating.length === 0 ? {} : { corroboratingTakes: corroborating }
49869
+ };
49870
+ }
49871
+ function inProcessTake(req) {
49872
+ const { actor, judgment, generation } = req;
49873
+ if (actor === void 0 || judgment === void 0 || generation === void 0) {
49874
+ throw new Error("authorJourney: actor, judgment and generation are required without runTake");
49875
+ }
49876
+ return async ({ goal, successChecks }) => {
49877
+ const r = await runGoalBasedMission({
49878
+ goal,
49879
+ successChecks,
49880
+ allowlist: req.allowlist,
49881
+ startUrl: req.startUrl,
49882
+ bounds: req.bounds,
49883
+ actor,
49884
+ judge: judgment,
49885
+ gen: generation,
49886
+ ...req.safety === void 0 ? {} : { safety: req.safety }
49887
+ });
49888
+ return { outcome: r.outcome, recording: r.recording, diagnostics: goalResultDiagnostics(r) };
49063
49889
  };
49064
- return { outcome: "authored", journey: { metadata, recording: parameterizedRecording } };
49890
+ }
49891
+ var SECRET_PLACEHOLDER = "\0jevitate:code-typed-secret";
49892
+ function authoringTake(recording) {
49893
+ const values2 = /* @__PURE__ */ new Map();
49894
+ recording.pages.forEach((page, p) => page.steps.forEach(({ step }, i) => {
49895
+ if (step.kind !== "fill" && step.kind !== "select")
49896
+ return;
49897
+ const v = step.value;
49898
+ if ("var" in v)
49899
+ return;
49900
+ values2.set(`${p}:${i}`, v.redacted ? SECRET_PLACEHOLDER : v.value);
49901
+ }));
49902
+ return { recording, values: values2 };
49903
+ }
49904
+ function secretFillSteps(recording) {
49905
+ const keys = /* @__PURE__ */ new Set();
49906
+ for (const { ref } of flattenBaseFillSteps(recording)) {
49907
+ const { step } = recording.pages[ref.page].steps[ref.step];
49908
+ if ((step.kind === "fill" || step.kind === "select") && "redacted" in step.value && step.value.redacted)
49909
+ keys.add(`${ref.page}:${ref.step}`);
49910
+ }
49911
+ return keys;
49065
49912
  }
49066
49913
  function appendSuccessAssertion(recording, check2) {
49067
49914
  if (recording.pages.length === 0)
@@ -51991,7 +52838,7 @@ async function runOneReplay(params) {
51991
52838
  }
51992
52839
 
51993
52840
  // ../explore/dist/adversarial/markup-canary.js
51994
- import { createHash as createHash4, randomBytes as randomBytes2 } from "node:crypto";
52841
+ import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
51995
52842
  var CanaryTokens = class {
51996
52843
  prefix;
51997
52844
  #n = 0;
@@ -52049,7 +52896,7 @@ async function renderedCanariesSettled(page, prefix, authorized, expected, waitM
52049
52896
  return seen;
52050
52897
  }
52051
52898
  function markupFingerprint(route, field3, payload) {
52052
- return createHash4("sha256").update(`markup-injection|${route}|${field3}|${payload}`).digest("hex").slice(0, 16);
52899
+ return createHash5("sha256").update(`markup-injection|${route}|${field3}|${payload}`).digest("hex").slice(0, 16);
52053
52900
  }
52054
52901
 
52055
52902
  // ../explore/dist/missions/adversarial-hunt/helpers.js
@@ -52379,7 +53226,7 @@ function startHunt(ctx, params) {
52379
53226
  }
52380
53227
 
52381
53228
  // ../explore/dist/adversarial/identity.js
52382
- import { createHash as createHash5 } from "node:crypto";
53229
+ import { createHash as createHash6 } from "node:crypto";
52383
53230
  var READ_TIMEOUT_MS2 = 2e3;
52384
53231
  function bounded3(p) {
52385
53232
  p.catch(() => void 0);
@@ -52394,7 +53241,7 @@ var AUTH_NAME = /sess|auth|token|jwt|\bsid\b|_sid|sid_|login|user|account|identi
52394
53241
  var AUTH_PATH = /log-?in|log-?out|sign-?in|sign-?up|sign-?out|register|auth|session|verify|oauth|sso|token|impersonat|switch-?user|demo/i;
52395
53242
  var MAX_VALUE_CHARS2 = 16384;
52396
53243
  function hash2(text2) {
52397
- return createHash5("sha256").update(text2).digest("hex").slice(0, 16);
53244
+ return createHash6("sha256").update(text2).digest("hex").slice(0, 16);
52398
53245
  }
52399
53246
  var JWT = /^[A-Za-z0-9_-]+\.([A-Za-z0-9_-]+)\.[A-Za-z0-9_-]*$/;
52400
53247
  var SUBJECT_CLAIMS = ["sub", "user_id", "userId", "uid", "email", "tenant", "tenant_id", "tenantId", "org", "org_id", "orgId"];
@@ -54227,6 +55074,7 @@ var RENDER_BASELINE_RENDERS = 3;
54227
55074
  var RENDER_TREND_RENDERS = 3;
54228
55075
  var RENDER_SLOWDOWN_FACTOR = 5;
54229
55076
  var RENDER_SLOWDOWN_FLOOR_MS = 3e3;
55077
+ var RENDER_CORROBORATING_LOAD_PER_CORE = LAG_CORROBORATING_LOAD_PER_CORE;
54230
55078
  var STARVATION_WINDOW_MS = 15e3;
54231
55079
  var HOST_SAMPLE_INTERVAL_MS = 2e3;
54232
55080
  var DEGRADED_STEP_MAJORITY = 0.5;
@@ -54333,14 +55181,21 @@ var HostHealthSampler = class {
54333
55181
  if (!this.#attribute)
54334
55182
  return null;
54335
55183
  const since = this.#now() - STARVATION_WINDOW_MS;
55184
+ let corroborating = null;
54336
55185
  for (let i = this.#samples.length - 1; i >= 0; i--) {
54337
55186
  const s3 = this.#samples[i];
54338
55187
  if (s3.at < since)
54339
55188
  break;
54340
55189
  if (s3.starved !== null)
54341
55190
  return s3.starved;
55191
+ const load = s3.host.loadPerCore;
55192
+ if (load !== void 0 && load >= RENDER_CORROBORATING_LOAD_PER_CORE)
55193
+ corroborating = Math.max(corroborating ?? 0, load);
54342
55194
  }
54343
- return this.#renderTrend();
55195
+ if (corroborating === null)
55196
+ return null;
55197
+ const trend = this.#renderTrend();
55198
+ return trend === null ? null : `${trend} at load ${fmt3(corroborating)}/core`;
54344
55199
  }
54345
55200
  /**
54346
55201
  * One recorded transcript step (every strategy's runner routes its entries here): counts it as
@@ -54458,8 +55313,8 @@ function degradedEnvironmentOutcome(outcome, health, opts = {}) {
54458
55313
  }
54459
55314
 
54460
55315
  // src/build-info.generated.ts
54461
- var GENERATED_COMMIT = "b82e749";
54462
- var GENERATED_BUILT_AT = "2026-10-04T14:00:35.891Z";
55316
+ var GENERATED_COMMIT = "b0b27ab";
55317
+ var GENERATED_BUILT_AT = "2026-10-05T02:04:30.795Z";
54463
55318
 
54464
55319
  // src/version.ts
54465
55320
  import { readFileSync as readFileSync5 } from "node:fs";
@@ -54481,10 +55336,10 @@ function readCliVersion() {
54481
55336
 
54482
55337
  // src/engine.ts
54483
55338
  function bundledCommit() {
54484
- return true ? "b82e749" : void 0;
55339
+ return true ? "b0b27ab" : void 0;
54485
55340
  }
54486
55341
  function bundledBuiltAt() {
54487
- return true ? "2026-10-04T14:00:35.891Z" : void 0;
55342
+ return true ? "2026-10-05T02:04:30.795Z" : void 0;
54488
55343
  }
54489
55344
  function currentEngineInfo() {
54490
55345
  return {
@@ -70125,7 +70980,7 @@ async function gateJourney(siteGate, recording, opts) {
70125
70980
 
70126
70981
  // src/mission-fixtures.ts
70127
70982
  import { spawn } from "node:child_process";
70128
- import { createHash as createHash6 } from "node:crypto";
70983
+ import { createHash as createHash7 } from "node:crypto";
70129
70984
  import { readFileSync as readFileSync11 } from "node:fs";
70130
70985
 
70131
70986
  // src/fixture-auth.ts
@@ -70540,7 +71395,7 @@ function substituteDeep(v, sub) {
70540
71395
  return v;
70541
71396
  }
70542
71397
  function sha256(text2) {
70543
- return createHash6("sha256").update(text2).digest("hex");
71398
+ return createHash7("sha256").update(text2).digest("hex");
70544
71399
  }
70545
71400
  function canonical2(v) {
70546
71401
  if (Array.isArray(v)) return `[${v.map(canonical2).join(",")}]`;
@@ -72682,6 +73537,7 @@ function observeBrowserSignals(page, sink, now2, onDrop) {
72682
73537
  const method = call(req, "method");
72683
73538
  const failure3 = call(req, "failure");
72684
73539
  if (typeof url2 !== "string") return;
73540
+ if (isExternalSchemeUrl(url2)) return;
72685
73541
  const why = typeof failure3?.errorText === "string" ? failure3.errorText : "failed";
72686
73542
  push({ source: "requestfailed", level: "error", text: `${typeof method === "string" ? method : "GET"} ${redactUrl(url2)} \u2014 ${why}` });
72687
73543
  });
@@ -75280,7 +76136,7 @@ function renderReportMarkdown(input2) {
75280
76136
  ...engines.length === 0 ? [] : [`Engine commit(s): ${engines.map((c) => `\`${c}\``).join(", ")}.`],
75281
76137
  ""
75282
76138
  ];
75283
- const statusOf2 = new Map(input2.diff?.entries.map((e) => [e.key, e.status]) ?? []);
76139
+ const statusOf3 = new Map(input2.diff?.entries.map((e) => [e.key, e.status]) ?? []);
75284
76140
  if (input2.diff !== void 0) {
75285
76141
  out.push("## Diff against baseline", "", "| status | count |", "| --- | --- |", ...DIFF_STATUSES.map((s3) => `| ${s3} | ${input2.diff?.summary[s3] ?? 0} |`), "", "| status | key | severity | title | baseline | current |", "| --- | --- | --- | --- | --- | --- |", ...input2.diff.entries.map((e) => `| ${e.status} | \`${e.key}\` | ${e.defect.severity} | ${cell(e.defect.title)} | ${e.baseline.seen}/${e.baseline.of} | ${e.current.seen}/${e.current.of} |`), "");
75286
76142
  }
@@ -75288,12 +76144,12 @@ function renderReportMarkdown(input2) {
75288
76144
  if (hard.length === 0)
75289
76145
  out.push("None.", "");
75290
76146
  for (const d of hard)
75291
- out.push(...defectSection(d, statusOf2.get(d.key)));
76147
+ out.push(...defectSection(d, statusOf3.get(d.key)));
75292
76148
  out.push("## Advisory findings", "", "Advisory findings (UX, 4xx-correlated console errors, Jev flags) never gate on their own.", "");
75293
76149
  if (advisory.length === 0)
75294
76150
  out.push("None.", "");
75295
76151
  for (const d of advisory)
75296
- out.push(...defectSection(d, statusOf2.get(d.key)));
76152
+ out.push(...defectSection(d, statusOf3.get(d.key)));
75297
76153
  out.push("## Runs", "", "| run | mode | target | started | outcome | build |", "| --- | --- | --- | --- | --- | --- |", ...input2.runs.map((r) => `| \`${r.runId}\` | ${r.mode} | ${r.target ?? ""} | ${r.startedAt ?? ""} | ${r.missionOutcome ?? ""}${r.goalOutcome === void 0 ? "" : ` (goal: ${r.goalOutcome})`} | ${r.targetBuild ?? ""} |`), "");
75298
76154
  return out.join("\n");
75299
76155
  }
@@ -76609,7 +77465,10 @@ async function runAuthorJourney(opts) {
76609
77465
  browserPortFactory: opts.browserPortFactory,
76610
77466
  browser: opts.browser,
76611
77467
  ...opts.storageState !== void 0 ? { storageState: opts.storageState } : {},
76612
- ...opts.safety === void 0 ? {} : { safety: opts.safety }
77468
+ ...opts.safety === void 0 ? {} : { safety: opts.safety },
77469
+ ...opts.goalRun === void 0 ? {} : { goalRun: opts.goalRun },
77470
+ ...opts.outDir === void 0 ? {} : { outDir: opts.outDir },
77471
+ ...opts.usage === void 0 ? {} : { usage: opts.usage }
76613
77472
  });
76614
77473
  if (result.outcome === "authored") {
76615
77474
  const journey = opts.storageState !== void 0 && result.journey.metadata.requiresAuth !== true ? { ...result.journey, metadata: { ...result.journey.metadata, requiresAuth: true } } : result.journey;
@@ -76624,34 +77483,58 @@ async function authorViaBrowser(args) {
76624
77483
  }
76625
77484
  const judge = args.judge;
76626
77485
  const gen = args.gen;
76627
- const portFactory = args.browserPortFactory ?? (() => new PlaywrightBrowserPort());
76628
- const port = portFactory();
76629
- const session2 = await port.open({
76630
- ...sessionLaunchOptions(args.browser),
76631
- allowedOrigins: [...args.allowlist],
76632
- baseUrl: args.origin,
76633
- ...args.storageState !== void 0 ? { storageState: args.storageState } : {}
77486
+ const g = args.goalRun ?? {};
77487
+ const target = g.target ?? (args.safety === void 0 ? void 0 : { safety: args.safety });
77488
+ return authorJourney({
77489
+ goal: args.goal,
77490
+ ...args.successAssertion === void 0 ? {} : { successAssertion: args.successAssertion },
77491
+ ...args.successChecks === void 0 ? {} : { successChecks: args.successChecks },
77492
+ allowlist: args.allowlist,
77493
+ startUrl: args.url,
77494
+ bounds: args.bounds,
77495
+ takes: args.takes,
77496
+ journeyId: args.journeyId,
77497
+ journeyName: args.journeyName,
77498
+ runTake: async ({ goal, successChecks }) => {
77499
+ const r = await runExploration({
77500
+ ...g,
77501
+ ...target === void 0 ? {} : { target },
77502
+ url: args.url,
77503
+ goal,
77504
+ successChecks,
77505
+ allowlist: args.allowlist,
77506
+ judge,
77507
+ gen,
77508
+ ...args.usage === void 0 ? {} : { usage: args.usage },
77509
+ bounds: args.bounds,
77510
+ ...args.outDir === void 0 ? {} : { outDir: args.outDir },
77511
+ ...args.browserPortFactory === void 0 ? {} : { browserPortFactory: args.browserPortFactory },
77512
+ ...args.browser === void 0 ? {} : { browser: args.browser },
77513
+ ...args.storageState === void 0 ? {} : { storageState: args.storageState }
77514
+ });
77515
+ return goalRunTake(r, g);
77516
+ }
76634
77517
  });
76635
- try {
76636
- const actor = CastActor.named("author").whoCan(new BrowseTheWeb(session2, [...args.allowlist]));
76637
- return await authorJourney({
76638
- goal: args.goal,
76639
- ...args.successAssertion === void 0 ? {} : { successAssertion: args.successAssertion },
76640
- ...args.successChecks === void 0 ? {} : { successChecks: args.successChecks },
76641
- allowlist: args.allowlist,
76642
- startUrl: args.url,
76643
- bounds: args.bounds,
76644
- actor,
76645
- judgment: judge,
76646
- generation: gen,
76647
- takes: args.takes,
76648
- journeyId: args.journeyId,
76649
- journeyName: args.journeyName,
76650
- ...args.safety === void 0 ? {} : { safety: args.safety }
76651
- });
76652
- } finally {
76653
- await session2.close();
76654
- }
77518
+ }
77519
+ function goalRunTake(r, g = {}) {
77520
+ const reason = r.reason ?? r.failure?.message;
77521
+ const recordingPath = r.recordingPaths[0];
77522
+ const diagnostics = {
77523
+ outcome: r.goalOutcome,
77524
+ ...reason === void 0 ? {} : { reason },
77525
+ stop: r.stop,
77526
+ runOutcome: r.runOutcome,
77527
+ ...r.failure === void 0 ? {} : { failure: { kind: r.failure.kind, message: r.failure.message } },
77528
+ checks: r.checks,
77529
+ decisions: r.decisions,
77530
+ actions: r.actions,
77531
+ finalUrl: r.finalUrl,
77532
+ resultPath: r.resultPath,
77533
+ transcriptPath: r.transcriptPath,
77534
+ recordingPaths: r.recordingPaths,
77535
+ ...g.screenshots === void 0 || recordingPath === void 0 ? {} : { screenshotsDir: screenshotsDirFor(g.screenshots, recordingPath) }
77536
+ };
77537
+ return { outcome: r.goalOutcome, recording: r.recording, diagnostics };
76655
77538
  }
76656
77539
 
76657
77540
  // src/explore-coverage.ts
@@ -81181,7 +82064,7 @@ function loadUxMaxFindingsPerPage(path2 = resolveDataDir(["config.json"])) {
81181
82064
  }
81182
82065
 
81183
82066
  // src/usability-capture.ts
81184
- import { createHash as createHash7 } from "node:crypto";
82067
+ import { createHash as createHash8 } from "node:crypto";
81185
82068
  import { mkdirSync as mkdirSync8 } from "node:fs";
81186
82069
  import { join as join22 } from "node:path";
81187
82070
  var SECRET_INPUT_SELECTOR = [
@@ -81272,7 +82155,7 @@ function payloadDigest(method, endpoint, body, secrets) {
81272
82155
  }
81273
82156
  try {
81274
82157
  const canon = JSON.stringify(canonical3(value));
81275
- return createHash7("sha256").update(`${method.toUpperCase()} ${endpoint}
82158
+ return createHash8("sha256").update(`${method.toUpperCase()} ${endpoint}
81276
82159
  ${canon}`).digest("hex").slice(0, 32);
81277
82160
  } catch {
81278
82161
  return void 0;
@@ -81322,6 +82205,7 @@ var UsabilityCapture = class {
81322
82205
  }
81323
82206
  #onRequest = (r) => {
81324
82207
  if (!KEPT_TYPES.has(r.resourceType())) return;
82208
+ if (isExternalSchemeUrl(r.url())) return;
81325
82209
  const endpoint = this.#redact(endpointOf(r.method(), r.url()));
81326
82210
  const payloadKey = WRITE_METHODS4.has(r.method().toUpperCase()) ? payloadDigest(r.method(), endpoint, postDataOf(r), this.#opts.secrets) : void 0;
81327
82211
  const rec = {
@@ -84352,7 +85236,7 @@ async function runCheck(opts) {
84352
85236
  const entry = d.keys.map((k) => entryOf.get(k)).find((e) => e !== void 0) ?? entryOf.get(d.key);
84353
85237
  return entry === void 0 ? true : !entry.inBaseline;
84354
85238
  };
84355
- const statusOf2 = (d) => d.keys.map((k) => entryOf.get(k)?.status).find((s3) => s3 !== void 0);
85239
+ const statusOf3 = (d) => d.keys.map((k) => entryOf.get(k)?.status).find((s3) => s3 !== void 0);
84356
85240
  const gating = defects.filter(isGating);
84357
85241
  const itemReports = executed.map(({ item, ex, durationMs }) => {
84358
85242
  const base = {
@@ -84408,7 +85292,7 @@ async function runCheck(opts) {
84408
85292
  cases.push({ suite: "budget", classname: "jevitate.budget", name: "total budget", timeSec: 0, status: "error", type: "budget-exceeded", message: exceeded });
84409
85293
  }
84410
85294
  const findings = defects.map((d) => {
84411
- const status = statusOf2(d);
85295
+ const status = statusOf3(d);
84412
85296
  return {
84413
85297
  key: d.key,
84414
85298
  title: d.title,
@@ -84458,7 +85342,7 @@ async function runCheck(opts) {
84458
85342
  ...opts.targetBuild === void 0 ? {} : { targetBuild: opts.targetBuild },
84459
85343
  suiteUri: opts.suiteUri ?? opts.suite.path,
84460
85344
  automationId: `jevitate-check/${opts.suite.name}/`,
84461
- findings: defects.map((d) => ({ defect: d, gating: gating.includes(d), ...statusOf2(d) === void 0 ? {} : { status: statusOf2(d) } }))
85345
+ findings: defects.map((d) => ({ defect: d, gating: gating.includes(d), ...statusOf3(d) === void 0 ? {} : { status: statusOf3(d) } }))
84462
85346
  });
84463
85347
  await writeFile12(sarifPath, `${JSON.stringify(sarif, null, 2)}
84464
85348
  `, "utf8");
@@ -85183,7 +86067,7 @@ import { userInfo } from "node:os";
85183
86067
  import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
85184
86068
 
85185
86069
  // src/run-key-preflight.ts
85186
- import { createHash as createHash8 } from "node:crypto";
86070
+ import { createHash as createHash9 } from "node:crypto";
85187
86071
  var NO_KEY_VERIFY_ENV = "JEVITATE_NO_KEY_VERIFY";
85188
86072
  var InvalidCredentialError = class extends MissingCredentialError {
85189
86073
  constructor(feature, invalid) {
@@ -85202,7 +86086,7 @@ var InvalidCredentialError = class extends MissingCredentialError {
85202
86086
  };
85203
86087
  var cache = /* @__PURE__ */ new Map();
85204
86088
  function cacheKey(key2, value) {
85205
- return `${key2}:${createHash8("sha256").update(value).digest("hex")}`;
86089
+ return `${key2}:${createHash9("sha256").update(value).digest("hex")}`;
85206
86090
  }
85207
86091
  function keyPreflightDisabled(env) {
85208
86092
  return /^(?:1|true|yes)$/i.test((env[NO_KEY_VERIFY_ENV] ?? "").trim());
@@ -85475,7 +86359,7 @@ var GitSourceManager = class {
85475
86359
  };
85476
86360
 
85477
86361
  // ../sources/dist/hash.js
85478
- import { createHash as createHash9 } from "node:crypto";
86362
+ import { createHash as createHash10 } from "node:crypto";
85479
86363
  function canonicalJson(value) {
85480
86364
  if (value === null)
85481
86365
  return "null";
@@ -85494,7 +86378,7 @@ function canonicalJson(value) {
85494
86378
  }
85495
86379
  function canonicalJourneyHash(file2) {
85496
86380
  const validated = SharedJourneyFileSchema.parse(file2);
85497
- const hex3 = createHash9("sha256").update(canonicalJson(validated), "utf8").digest("hex");
86381
+ const hex3 = createHash10("sha256").update(canonicalJson(validated), "utf8").digest("hex");
85498
86382
  return `sha256:${hex3}`;
85499
86383
  }
85500
86384
 
@@ -86243,7 +87127,7 @@ import { existsSync as realExistsSync } from "node:fs";
86243
87127
  import { readFile as readFile13, writeFile as writeFile18, mkdir as mkdir17 } from "node:fs/promises";
86244
87128
  import { homedir as realHomedir } from "node:os";
86245
87129
  import { join as join38, dirname as dirname14 } from "node:path";
86246
- import { createHash as createHash10 } from "node:crypto";
87130
+ import { createHash as createHash11 } from "node:crypto";
86247
87131
  function detectRuntimes(deps = {}) {
86248
87132
  const existsSync33 = deps.existsSync ?? realExistsSync;
86249
87133
  const homedir4 = deps.homedir ?? realHomedir;
@@ -86267,7 +87151,7 @@ function resolveInstallTargetPaths(deps = {}) {
86267
87151
  };
86268
87152
  }
86269
87153
  function sha2562(s3) {
86270
- return createHash10("sha256").update(s3, "utf8").digest("hex");
87154
+ return createHash11("sha256").update(s3, "utf8").digest("hex");
86271
87155
  }
86272
87156
  async function planFileInstall(targetPath, newContent, lastInstalledHash, opts = {}) {
86273
87157
  let current2;
@@ -89741,6 +90625,209 @@ function registerCampaignCommands(program2, deps, buildProgram2) {
89741
90625
  });
89742
90626
  }
89743
90627
 
90628
+ // src/goal-run-flags.ts
90629
+ import { Option } from "commander";
90630
+ var collect6 = (v, prev) => [...prev, v];
90631
+ var GOAL_RUN_OPTIONS = {
90632
+ successWhen: () => new Option(
90633
+ "--success-when <when>",
90634
+ "when the --success page checks must hold: final (default; on the final page) | held (on the final page, or all together at any settled step \u2014 a one-time secret, a toast) | each (each went from not holding to holding at some settled step, in any order \u2014 checks on different pages; the run stops once all have). reloadThen is always final"
90635
+ ),
90636
+ allowVacuousChecks: () => new Option(
90637
+ "--allow-vacuous-checks",
90638
+ "downgrade a vacuous --success check to a warning. By default a check satisfied before the run's first action \u2014 a page check that held on the seed page and never changed (an empty result container), a requestMade/responseStatus matched only by a page-load or polling request \u2014 FAILS: it cannot verify the goal"
90639
+ ),
90640
+ actionDeltas: () => new Option(
90641
+ "--action-deltas",
90642
+ "opt-in (#303; every --strategy, not --feature): record what each action changed on the page \u2014 an accessibility snapshot before and after, announcements, the action's requests \u2014 redacted, with a code verdict per step (no-change | relevant-change | inconclusive) used by the goal loop's no-progress check and a persistence re-check after writes (goal), and as defect evidence (adversarial, coverage); adds `delta` to every transcript step (and Recording step, goal) and `actionDeltas` to the result. Costs about 50-100 ms per action on a small page, 0.3-0.5 s on a large one"
90643
+ ),
90644
+ secret: () => new Option(
90645
+ "--secret <value|env:VAR>",
90646
+ "REDACTION ONLY: a secret/PII value kept out of every model call and artifact (repeatable); env:VAR reads it from the environment (preferred: a literal is visible in the process list and shell history). It is never typed into a field \u2014 to log in, bind it with --secret-field (or start from --storage-state)"
90647
+ ).default([]).argParser(collect6),
90648
+ secretField: () => new Option(
90649
+ "--secret-field <binding>",
90650
+ "goal/usability strategy: '<label|testId|type|id|name>=<value>=env:<VAR>' (repeatable), e.g. 'label=Password=env:APP_PASSWORD'. When the run types into a matching field, code types $VAR itself; the model sees only \xABsecret:VAR\xBB and the Recording {redacted:true}. A value delivered during the run (an emailed code): '<descriptor>=cmd:<command>' runs the command when the field is typed and types its stdout (needs --allow-secret-cmd)"
90651
+ ).default([]).argParser(collect6),
90652
+ totp: () => new Option(
90653
+ "--totp <binding>",
90654
+ "goal/usability strategy: '<descriptor>=env:<VAR>' with $VAR a base32 TOTP seed (repeatable), e.g. 'label=Authentication code=env:APP_TOTP_SEED'. The 6-digit code is computed locally (RFC 6238) when the field is typed; the seed never reaches a model or disk"
90655
+ ).default([]).argParser(collect6),
90656
+ typeFixture: () => new Option(
90657
+ "--type-fixture <binding>",
90658
+ "goal strategy: '<label|testId|type|id|name>=<value>=<file>' (repeatable), e.g. 'label=Paste your text=./fixtures/import.txt'. When the run types into a matching field, code types the file's exact text verbatim (line breaks kept, never paraphrased or capped); the model sees only \xABfixture:<file name>\xBB. Recorded as typed unless it holds a --secret"
90659
+ ).default([]).argParser(collect6),
90660
+ fixture: () => new Option("--fixture <path>", "local file the upload op attaches to a file input (goal and usability strategies); must exist"),
90661
+ saveStorageState: () => new Option(
90662
+ "--save-storage-state <file>",
90663
+ "write the context's storageState (cookies + origin storage) here when the run ends; mode 0600, contents never logged. Useful with a rotating refresh token: --storage-state's file goes stale after one authenticated run refreshes it, so point --save-storage-state at the SAME file (or a new one) to keep it usable for the next run. Written on every exit path -- a crash or a SIGTERM/SIGINT kill included (#159), not only a clean end -- but never over a good file with a session that already looks lost/logged-out; the last known-good state is used instead, or nothing is written if none was ever captured."
90664
+ ),
90665
+ replyWaitMs: () => new Option(
90666
+ "--reply-wait-ms <ms>",
90667
+ "conversational pages: how long to keep waiting for a reply while the page shows no sign of working on one (goal and usability; default 60000). While a request the message started is in flight, a busy indicator shows, or the reply is still growing, the wait continues up to --reply-ceiling-ms"
90668
+ ).argParser(positiveIntArg),
90669
+ replyQuietMs: () => new Option(
90670
+ "--reply-quiet-ms <ms>",
90671
+ "conversational pages: how long a reply must hold still (no new text, no busy sign) before it is read as complete (goal and usability; default 1000). Raise it for an assistant that answers in several parts (a sentence, then a card a moment later)"
90672
+ ).argParser(positiveIntArg),
90673
+ replyCeilingMs: () => new Option(
90674
+ "--reply-ceiling-ms <ms>",
90675
+ "conversational pages: hard ceiling on the TOTAL wait for one sent message's reply \u2014 the send's own wait plus every later 'wait' \u2014 however busy the page stays; once spent the run ends naming the missing reply (default 180000; never below --reply-wait-ms) (default 180000; never below --reply-wait-ms)"
90676
+ ).argParser(positiveIntArg),
90677
+ replyMaxChars: () => new Option("--reply-max-chars <n>", "conversational pages: cap on each generated chat message (goal and usability; default 300)").argParser(
90678
+ intArg({ min: 20, max: 2e3 })
90679
+ ),
90680
+ jobWaitMs: () => new Option(
90681
+ "--job-wait-ms <ms>",
90682
+ `goal and usability: while the page shows an in-progress status ("Simulating\u2026", aria-busy, a job "is running"), waits keep waiting with backoff \u2014 and a model 'blocked' is deferred \u2014 up to this budget (default: --reply-ceiling-ms, 180000); it also bounds a busy indicator the app visibly keeps working behind (live progress, a job poll) before it is a hang, and a wait the page documents ("usually takes a minute") can raise it`
90683
+ ).argParser(positiveIntArg),
90684
+ deny: () => new Option(
90685
+ "--deny <pattern>",
90686
+ "a control no mission may click (repeatable): an accessible-name regex (/Archive/i or Archive) or a descriptor role=button;name=Archive. Session-ending (Sign out), destructive (Delete, Revoke, Rotate) and paid (Buy, Run simulation, Generate, Send invite) controls are refused by default"
90687
+ ).default([]).argParser(collect6),
90688
+ paid: () => new Option(
90689
+ "--paid <pattern>",
90690
+ "an app control that costs money or credits (repeatable; same syntax as --deny), e.g. /^(Analyze|Draft|Improve)\\b/i: treated like the built-in paid vocabulary \u2014 the budget guard sees it, hang replays never repeat it, and a goal that asks for it may still click it"
90691
+ ).default([]).argParser(collect6),
90692
+ allowDestructive: () => new Option(
90693
+ "--allow-destructive",
90694
+ "let missions click session-ending, destructive and paid controls (a --deny pattern still holds). A goal run already may click one its goal asks for"
90695
+ ),
90696
+ dialogs: () => new Option(
90697
+ "--dialogs <policy>",
90698
+ "native window.confirm/prompt dialogs: dismiss (default) or accept. accept still dismisses one whose message names a session-ending, destructive or paid action the run may not take (without --allow-destructive or a goal asking for it); every dialog is logged"
90699
+ ),
90700
+ readRpc: () => new Option(
90701
+ "--read-rpc <glob>",
90702
+ "a POST request that only READS (repeatable): an RPC-method glob (Estimate*, pkg.Service/Preview*) or a path glob (/api/search*). gRPC-web/Connect Get*/List*/Search*/Find*/Watch*/Stream*/Count*/Describe*/Read* methods are reads already. Reads are never guarded or reported as duplicate writes"
90703
+ ).default([]).argParser(collect6),
90704
+ hangReplays: () => new Option("--hang-replays <n>", "fresh-context replays that confirm a hang (default 2; 0 = don't replay, the hang is reported unconfirmed)").argParser(
90705
+ nonNegativeIntArg
90706
+ ),
90707
+ settleIgnore: () => new Option("--settle-ignore <pattern>", "a request URL pattern the target marks as background (never pending work; repeatable, * wildcard)").default([]).argParser(collect6),
90708
+ longPollMs: () => new Option("--long-poll-ms <n>", "a request pending this long on an interactive page is a long-poll (default 5000)").argParser(nonNegativeIntArg),
90709
+ apiPrefix: () => new Option("--api-prefix <path>", "a path prefix whose requests are the app's API in the timing summary (repeatable), e.g. /api/").default([]).argParser(collect6),
90710
+ ignoreNoProgress: () => new Option("--ignore-no-progress <pattern>", "a route / action label / busy indicator where ui-no-progress is expected (repeatable, * wildcard)").default([]).argParser(collect6),
90711
+ allowSecretCmd: () => new Option(
90712
+ "--allow-secret-cmd",
90713
+ "opt-in: a --secret-field <descriptor>=cmd:<command> may run its command (in a shell, at type time, 60s timeout) and type its output (operator-declared only; refused otherwise)"
90714
+ ).default(false),
90715
+ secretCmdAttempts: () => new Option(
90716
+ "--secret-cmd-attempts <n>",
90717
+ "#359: how many times one cmd: secret field's command may run in this run (default 3); past it, typing that field fails without running the command again (read-the-code commands usually have side effects)"
90718
+ ).argParser(positiveIntArg)
90719
+ };
90720
+ var GoalRunFlagError = class extends Error {
90721
+ constructor(code, message2) {
90722
+ super(message2);
90723
+ this.code = code;
90724
+ this.name = "GoalRunFlagError";
90725
+ }
90726
+ code;
90727
+ };
90728
+ var asError = (err) => err instanceof GoalRunFlagError ? err : new GoalRunFlagError(
90729
+ err instanceof SessionFileInProjectError || err instanceof TargetConfigError ? err.code : "E_EXPLORE_ARGS",
90730
+ err instanceof Error ? err.message : String(err)
90731
+ );
90732
+ function dialogsFromFlags(o) {
90733
+ if (o.dialogs === void 0) return void 0;
90734
+ if (o.dialogs !== "dismiss" && o.dialogs !== "accept") {
90735
+ throw new GoalRunFlagError("E_EXPLORE_ARGS", `--dialogs must be dismiss or accept, got ${JSON.stringify(o.dialogs)}`);
90736
+ }
90737
+ return o.dialogs;
90738
+ }
90739
+ function successWhenFromFlags(o) {
90740
+ if (o.successWhen === void 0) return void 0;
90741
+ if (o.successWhen !== "held" && o.successWhen !== "final" && o.successWhen !== "each") {
90742
+ throw new GoalRunFlagError("E_EXPLORE_ARGS", `--success-when must be "final", "held" or "each", got ${JSON.stringify(o.successWhen)}`);
90743
+ }
90744
+ return o.successWhen;
90745
+ }
90746
+ function conversationFromFlags(o) {
90747
+ const conversation = {
90748
+ ...o.replyWaitMs === void 0 ? {} : { replyWaitMs: Number(o.replyWaitMs) },
90749
+ ...o.replyQuietMs === void 0 ? {} : { replyQuietMs: Number(o.replyQuietMs) },
90750
+ ...o.replyCeilingMs === void 0 ? {} : { replyCeilingMs: Number(o.replyCeilingMs) },
90751
+ ...o.replyMaxChars === void 0 ? {} : { replyMaxChars: Number(o.replyMaxChars) },
90752
+ ...o.jobWaitMs === void 0 ? {} : { jobWaitMs: Number(o.jobWaitMs) }
90753
+ };
90754
+ if (conversation.replyWaitMs !== void 0 && !(Number.isInteger(conversation.replyWaitMs) && conversation.replyWaitMs > 0) || conversation.replyCeilingMs !== void 0 && !(Number.isInteger(conversation.replyCeilingMs) && conversation.replyCeilingMs > 0) || conversation.replyMaxChars !== void 0 && !(Number.isInteger(conversation.replyMaxChars) && conversation.replyMaxChars >= 20 && conversation.replyMaxChars <= 2e3)) {
90755
+ throw new GoalRunFlagError("E_EXPLORE_ARGS", "--reply-wait-ms and --reply-ceiling-ms must be positive integers; --reply-max-chars an integer in 20..2000");
90756
+ }
90757
+ if (conversation.jobWaitMs !== void 0 && !(Number.isInteger(conversation.jobWaitMs) && conversation.jobWaitMs > 0)) {
90758
+ throw new GoalRunFlagError("E_EXPLORE_ARGS", "--job-wait-ms must be a positive integer");
90759
+ }
90760
+ return conversation;
90761
+ }
90762
+ function secretFieldsFromFlags(o, env = process.env) {
90763
+ try {
90764
+ return [
90765
+ ...o.secretField.map((s3) => parseSecretField(s3, "value", env, { allowCmd: o.allowSecretCmd === true })),
90766
+ ...o.totp.map((s3) => parseSecretField(s3, "totp", env))
90767
+ ];
90768
+ } catch (err) {
90769
+ throw asError(err);
90770
+ }
90771
+ }
90772
+ function targetFlagsFromFlags(o, dialogs) {
90773
+ return {
90774
+ settleIgnore: o.settleIgnore ?? [],
90775
+ ignoreNoProgress: o.ignoreNoProgress ?? [],
90776
+ apiPrefixes: o.apiPrefix ?? [],
90777
+ deny: o.deny ?? [],
90778
+ paid: o.paid ?? [],
90779
+ readRpc: o.readRpc ?? [],
90780
+ ...o.allowDestructive === true ? { allowDestructive: true } : {},
90781
+ ...dialogs === void 0 ? {} : { dialogs },
90782
+ ...o.allowWrites === true ? { allowWrites: true } : {},
90783
+ allowWrite: o.allowWrite ?? [],
90784
+ ...o.hangReplayWrites === true ? { hangReplayWrites: true } : {},
90785
+ ...o.longPollMs === void 0 ? {} : { longPollMs: Number(o.longPollMs) }
90786
+ };
90787
+ }
90788
+ function resolveGoalRunFlags(o, url2, deps = {}) {
90789
+ const env = deps.env ?? process.env;
90790
+ try {
90791
+ const dialogs = dialogsFromFlags(o);
90792
+ const successWhen = successWhenFromFlags(o);
90793
+ if (o.saveStorageState !== void 0) assertSessionFileOutsideProject(o.saveStorageState, "--save-storage-state");
90794
+ const resolved = resolveSecretArgs(o.secret, env, "--secret");
90795
+ if (resolved.literals > 0) deps.warn?.(LITERAL_SECRET_WARNING);
90796
+ const screenshots = parseScreenshotsArg(o.screenshots);
90797
+ const conversation = conversationFromFlags(o);
90798
+ validateDenyPatterns(o.deny);
90799
+ validateDenyPatterns(o.paid, "--paid");
90800
+ const emulation = emulationFromFlags(o);
90801
+ const secretFields = secretFieldsFromFlags(o, env);
90802
+ const typeFixtures = o.typeFixture.length === 0 ? [] : loadTypeFixtures(o.typeFixture);
90803
+ let target;
90804
+ try {
90805
+ target = resolveTargetConfig(loadTargetsFile(deps.targetsConfigPath), new URL(url2).origin, targetFlagsFromFlags(o, dialogs));
90806
+ } catch (err) {
90807
+ if (!(err instanceof TypeError)) throw err;
90808
+ }
90809
+ return {
90810
+ ...successWhen === void 0 ? {} : { successWhen },
90811
+ ...o.allowVacuousChecks === true ? { allowVacuousChecks: true } : {},
90812
+ ...o.actionDeltas === true ? { actionDeltas: true } : {},
90813
+ ...resolved.secrets.length === 0 ? {} : { secrets: resolved.secrets },
90814
+ ...secretFields.length === 0 ? {} : { secretFields },
90815
+ ...secretFields.some((f) => f.kind === "cmd") ? { secretCommand: secretCommandRunner() } : {},
90816
+ ...o.secretCmdAttempts === void 0 ? {} : { secretCommandAttempts: o.secretCmdAttempts },
90817
+ ...typeFixtures.length === 0 ? {} : { typeFixtures },
90818
+ ...o.fixture === void 0 ? {} : { fixture: o.fixture },
90819
+ ...o.saveStorageState === void 0 ? {} : { saveStorageState: o.saveStorageState },
90820
+ ...screenshots === void 0 ? {} : { screenshots },
90821
+ ...emulation === void 0 ? {} : { emulation },
90822
+ ...target === void 0 ? {} : { target },
90823
+ ...o.hangReplays === void 0 ? {} : { hangReplays: o.hangReplays },
90824
+ ...Object.keys(conversation).length === 0 ? {} : { conversation }
90825
+ };
90826
+ } catch (err) {
90827
+ throw asError(err);
90828
+ }
90829
+ }
90830
+
89744
90831
  // src/explore-cli.ts
89745
90832
  var SWEEP_OWNED = /* @__PURE__ */ new Set([
89746
90833
  "fromJourney",
@@ -89819,16 +90906,7 @@ function registerExploreCommands(program2, deps, buildProgram2) {
89819
90906
  ].join(" "),
89820
90907
  (v, prev) => [...prev, v],
89821
90908
  []
89822
- ).option(
89823
- "--success-when <when>",
89824
- "when the --success page checks must hold: final (default; on the final page) | held (on the final page, or all together at any settled step \u2014 a one-time secret, a toast) | each (each went from not holding to holding at some settled step, in any order \u2014 checks on different pages; the run stops once all have). reloadThen is always final"
89825
- ).option(
89826
- "--allow-vacuous-checks",
89827
- "downgrade a vacuous --success check to a warning. By default a check satisfied before the run's first action \u2014 a page check that held on the seed page and never changed (an empty result container), a requestMade/responseStatus matched only by a page-load or polling request \u2014 FAILS: it cannot verify the goal"
89828
- ).option(
89829
- "--action-deltas",
89830
- "opt-in (#303; every --strategy, not --feature): record what each action changed on the page \u2014 an accessibility snapshot before and after, announcements, the action's requests \u2014 redacted, with a code verdict per step (no-change | relevant-change | inconclusive) used by the goal loop's no-progress check and a persistence re-check after writes (goal), and as defect evidence (adversarial, coverage); adds `delta` to every transcript step (and Recording step, goal) and `actionDeltas` to the result. Costs about 50-100 ms per action on a small page, 0.3-0.5 s on a large one"
89831
- ).option("--feature <name>", "run the capability-scoped feature-testing mission (instead of --goal/--success)").option(
90909
+ ).addOption(GOAL_RUN_OPTIONS.successWhen()).addOption(GOAL_RUN_OPTIONS.allowVacuousChecks()).addOption(GOAL_RUN_OPTIONS.actionDeltas()).option("--feature <name>", "run the capability-scoped feature-testing mission (instead of --goal/--success)").option(
89832
90910
  "--route <glob>",
89833
90911
  "in-scope route glob (repeatable), e.g. /thread/** \u2014 for --feature it replaces the default scope (the start URL's route and everything under it); it widens --strategy adversarial/coverage/exploratory beyond the start URL's route",
89834
90912
  (v, prev) => [...prev, v],
@@ -89841,30 +90919,7 @@ function registerExploreCommands(program2, deps, buildProgram2) {
89841
90919
  "authorized origin (repeatable); REPLACES the default allowlist when given (the URL's own origin is used only when --allow is omitted entirely) -- include the URL's own origin explicitly if you still need it",
89842
90920
  (v, prev) => [...prev, v],
89843
90921
  []
89844
- ).option(
89845
- "--secret <value|env:VAR>",
89846
- "REDACTION ONLY: a secret/PII value kept out of every model call and artifact (repeatable); env:VAR reads it from the environment (preferred: a literal is visible in the process list and shell history). It is never typed into a field \u2014 to log in, bind it with --secret-field (or start from --storage-state)",
89847
- (v, prev) => [...prev, v],
89848
- []
89849
- ).option(
89850
- "--secret-field <binding>",
89851
- "goal/usability strategy: '<label|testId|type|id|name>=<value>=env:<VAR>' (repeatable), e.g. 'label=Password=env:APP_PASSWORD'. When the run types into a matching field, code types $VAR itself; the model sees only \xABsecret:VAR\xBB and the Recording {redacted:true}. A value delivered during the run (an emailed code): '<descriptor>=cmd:<command>' runs the command when the field is typed and types its stdout (needs --allow-secret-cmd)",
89852
- (v, prev) => [...prev, v],
89853
- []
89854
- ).option(
89855
- "--totp <binding>",
89856
- "goal/usability strategy: '<descriptor>=env:<VAR>' with $VAR a base32 TOTP seed (repeatable), e.g. 'label=Authentication code=env:APP_TOTP_SEED'. The 6-digit code is computed locally (RFC 6238) when the field is typed; the seed never reaches a model or disk",
89857
- (v, prev) => [...prev, v],
89858
- []
89859
- ).option(
89860
- "--type-fixture <binding>",
89861
- "goal strategy: '<label|testId|type|id|name>=<value>=<file>' (repeatable), e.g. 'label=Paste your text=./fixtures/import.txt'. When the run types into a matching field, code types the file's exact text verbatim (line breaks kept, never paraphrased or capped); the model sees only \xABfixture:<file name>\xBB. Recorded as typed unless it holds a --secret",
89862
- (v, prev) => [...prev, v],
89863
- []
89864
- ).option(
89865
- "--fixture <path>",
89866
- "local file the upload op attaches to a file input (goal and usability strategies); must exist"
89867
- ).option(
90922
+ ).addOption(GOAL_RUN_OPTIONS.secret()).addOption(GOAL_RUN_OPTIONS.secretField()).addOption(GOAL_RUN_OPTIONS.totp()).addOption(GOAL_RUN_OPTIONS.typeFixture()).addOption(GOAL_RUN_OPTIONS.fixture()).option(
89868
90923
  "--storage-state <file>",
89869
90924
  "Playwright storageState JSON to start the session authenticated (deterministic login pre-step); must exist"
89870
90925
  ).option(
@@ -89872,50 +90927,11 @@ function registerExploreCommands(program2, deps, buildProgram2) {
89872
90927
  "multi-actor mission (#147, goal only; repeatable): the FIRST actor is the primary (the only one the model drives, from its own storageState); every other actor is an observer in its OWN fresh context that only runs the --invariants' cross-actor checks (capture + probe as:/deniedAs) \u2014 never clicks or types. Replaces --storage-state",
89873
90928
  (v, prev) => [...prev, v],
89874
90929
  []
89875
- ).option(
89876
- "--save-storage-state <file>",
89877
- "write the context's storageState (cookies + origin storage) here when the run ends; mode 0600, contents never logged. Useful with a rotating refresh token: --storage-state's file goes stale after one authenticated run refreshes it, so point --save-storage-state at the SAME file (or a new one) to keep it usable for the next run. Written on every exit path -- a crash or a SIGTERM/SIGINT kill included (#159), not only a clean end -- but never over a good file with a session that already looks lost/logged-out; the last known-good state is used instead, or nothing is written if none was ever captured."
89878
- ).option("--max-actions <n>", "hard cap on executed actions", positiveIntArg).option("--max-decisions <n>", "hard cap on model decisions", positiveIntArg).option(
90930
+ ).addOption(GOAL_RUN_OPTIONS.saveStorageState()).option("--max-actions <n>", "hard cap on executed actions", positiveIntArg).option("--max-decisions <n>", "hard cap on model decisions", positiveIntArg).option(
89879
90931
  "--stall-timeout <seconds>",
89880
90932
  "--strategy coverage/exploratory and --feature: end the run inconclusive (stalled) when no step completes within this many seconds (default 120)",
89881
90933
  positiveNumberArg
89882
- ).option(
89883
- "--reply-wait-ms <ms>",
89884
- "conversational pages: how long to keep waiting for a reply while the page shows no sign of working on one (goal and usability; default 60000). While a request the message started is in flight, a busy indicator shows, or the reply is still growing, the wait continues up to --reply-ceiling-ms",
89885
- positiveIntArg
89886
- ).option(
89887
- "--reply-quiet-ms <ms>",
89888
- "conversational pages: how long a reply must hold still (no new text, no busy sign) before it is read as complete (goal and usability; default 1000). Raise it for an assistant that answers in several parts (a sentence, then a card a moment later)",
89889
- positiveIntArg
89890
- ).option(
89891
- "--reply-ceiling-ms <ms>",
89892
- "conversational pages: hard ceiling on one reply wait, however busy the page stays (default 180000; never below --reply-wait-ms)",
89893
- positiveIntArg
89894
- ).option(
89895
- "--reply-max-chars <n>",
89896
- "conversational pages: cap on each generated chat message (goal and usability; default 300)",
89897
- intArg({ min: 20, max: 2e3 })
89898
- ).option(
89899
- "--job-wait-ms <ms>",
89900
- `goal and usability: while the page shows an in-progress status ("Simulating\u2026", aria-busy, a job "is running"), waits keep waiting with backoff \u2014 and a model 'blocked' is deferred \u2014 up to this budget (default: --reply-ceiling-ms, 180000); it also bounds a busy indicator the app visibly keeps working behind (live progress, a job poll) before it is a hang, and a wait the page documents ("usually takes a minute") can raise it`,
89901
- positiveIntArg
89902
- ).option(
89903
- "--deny <pattern>",
89904
- "a control no mission may click (repeatable): an accessible-name regex (/Archive/i or Archive) or a descriptor role=button;name=Archive. Session-ending (Sign out), destructive (Delete, Revoke, Rotate) and paid (Buy, Run simulation, Generate, Send invite) controls are refused by default",
89905
- (v, prev) => [...prev, v],
89906
- []
89907
- ).option(
89908
- "--paid <pattern>",
89909
- "an app control that costs money or credits (repeatable; same syntax as --deny), e.g. /^(Analyze|Draft|Improve)\\b/i: treated like the built-in paid vocabulary \u2014 the budget guard sees it, hang replays never repeat it, and a goal that asks for it may still click it",
89910
- (v, prev) => [...prev, v],
89911
- []
89912
- ).option(
89913
- "--allow-destructive",
89914
- "let missions click session-ending, destructive and paid controls (a --deny pattern still holds). A goal run already may click one its goal asks for"
89915
- ).option(
89916
- "--dialogs <policy>",
89917
- "native window.confirm/prompt dialogs: dismiss (default) or accept. accept still dismisses one whose message names a session-ending, destructive or paid action the run may not take (without --allow-destructive or a goal asking for it); every dialog is logged"
89918
- ).option(
90934
+ ).addOption(GOAL_RUN_OPTIONS.replyWaitMs()).addOption(GOAL_RUN_OPTIONS.replyQuietMs()).addOption(GOAL_RUN_OPTIONS.replyCeilingMs()).addOption(GOAL_RUN_OPTIONS.replyMaxChars()).addOption(GOAL_RUN_OPTIONS.jobWaitMs()).addOption(GOAL_RUN_OPTIONS.deny()).addOption(GOAL_RUN_OPTIONS.paid()).addOption(GOAL_RUN_OPTIONS.allowDestructive()).addOption(GOAL_RUN_OPTIONS.dialogs()).option(
89919
90935
  "--allow-writes",
89920
90936
  "let a find-out goal (no --success check, ended by report) change the app. By default it is read-only: controls that start a write flow (checkout, upgrade, create, save, submit\u2026) are refused and the write requests an action fires are blocked, unless the goal itself asks for a change"
89921
90937
  ).option(
@@ -89923,33 +90939,13 @@ function registerExploreCommands(program2, deps, buildProgram2) {
89923
90939
  "a write-request path a read-only find-out goal never blocks (repeatable; ** spans segments; a glob starting with https:// matches origin + path, e.g. https://abc.supabase.co/rest/v1/**), beyond the built-in auth-refresh ones (**/refresh*, **/token*, **/oauth/**, **/auth/**/refresh*). The app's background writes outside an action always pass",
89924
90940
  (v, prev) => [...prev, v],
89925
90941
  []
89926
- ).option(
89927
- "--read-rpc <glob>",
89928
- "a POST request that only READS (repeatable): an RPC-method glob (Estimate*, pkg.Service/Preview*) or a path glob (/api/search*). gRPC-web/Connect Get*/List*/Search*/Find*/Watch*/Stream*/Count*/Describe*/Read* methods are reads already. Reads are never guarded or reported as duplicate writes",
89929
- (v, prev) => [...prev, v],
89930
- []
89931
- ).option("--real", "use live Jev + OpenRouter gateways (requires keys)", false).option("--fake-ai", "use deterministic fake gateways (pipeline smoke only)", false).option("--out <dir>", "directory to write the emitted Recording").option(
90942
+ ).addOption(GOAL_RUN_OPTIONS.readRpc()).option("--real", "use live Jev + OpenRouter gateways (requires keys)", false).option("--fake-ai", "use deterministic fake gateways (pipeline smoke only)", false).option("--out <dir>", "directory to write the emitted Recording").option(
89932
90943
  "--file-issues",
89933
90944
  "file findings as issues (needs a repo: --issue-repo or ~/.jevitate/filing.json); default: drafts only"
89934
- ).option("--issue-repo <owner/name>", "the system-under-test repo findings for THIS target are filed to").option("--hang-replays <n>", "fresh-context replays that confirm a hang (default 2; 0 = don't replay, the hang is reported unconfirmed)", nonNegativeIntArg).option(
90945
+ ).option("--issue-repo <owner/name>", "the system-under-test repo findings for THIS target are filed to").addOption(GOAL_RUN_OPTIONS.hangReplays()).option(
89935
90946
  "--hang-replay-writes",
89936
90947
  "let hang replays re-send a paid/destructive write the run sent (default: such a hang is reported inconclusive, never replayed)"
89937
- ).option(
89938
- "--settle-ignore <pattern>",
89939
- "a request URL pattern the target marks as background (never pending work; repeatable, * wildcard)",
89940
- (v, prev) => [...prev, v],
89941
- []
89942
- ).option("--long-poll-ms <n>", "a request pending this long on an interactive page is a long-poll (default 5000)", nonNegativeIntArg).option(
89943
- "--api-prefix <path>",
89944
- "a path prefix whose requests are the app's API in the timing summary (repeatable), e.g. /api/",
89945
- (v, prev) => [...prev, v],
89946
- []
89947
- ).option(
89948
- "--ignore-no-progress <pattern>",
89949
- "a route / action label / busy indicator where ui-no-progress is expected (repeatable, * wildcard)",
89950
- (v, prev) => [...prev, v],
89951
- []
89952
- ).option("--jevitate-repo <owner/name>", "where jevitate engine findings are filed (default matt-cochran/jevitate)").option(
90948
+ ).addOption(GOAL_RUN_OPTIONS.settleIgnore()).addOption(GOAL_RUN_OPTIONS.longPollMs()).addOption(GOAL_RUN_OPTIONS.apiPrefix()).addOption(GOAL_RUN_OPTIONS.ignoreNoProgress()).option("--jevitate-repo <owner/name>", "where jevitate engine findings are filed (default matt-cochran/jevitate)").option(
89953
90949
  "--min-control-coverage <ratio>",
89954
90950
  "adversarial: share of the target's controls (0..1) a run must exercise before 'found nothing' is clean (default 0.25); below it the run is inconclusive",
89955
90951
  ratioArg
@@ -89966,15 +90962,7 @@ function registerExploreCommands(program2, deps, buildProgram2) {
89966
90962
  "backend log source (repeatable; every strategy, incl. usability): file:<path> (tailed from its current end) | docker:<container> (docker logs -f --since 0s) | cmd:<command> (needs --allow-log-cmd). Read-only, operator-declared, never the model's choice. Error/warning lines are correlated to the step they landed during and attached to its transcript evidence, redacted",
89967
90963
  (v, prev) => [...prev, v],
89968
90964
  []
89969
- ).option(
89970
- "--allow-secret-cmd",
89971
- "opt-in: a --secret-field <descriptor>=cmd:<command> may run its command (in a shell, at type time, 60s timeout) and type its output (operator-declared only; refused otherwise)",
89972
- false
89973
- ).option(
89974
- "--secret-cmd-attempts <n>",
89975
- "#359: how many times one cmd: secret field's command may run in this run (default 3); past it, typing that field fails without running the command again (read-the-code commands usually have side effects)",
89976
- positiveIntArg
89977
- ).option(
90965
+ ).addOption(GOAL_RUN_OPTIONS.allowSecretCmd()).addOption(GOAL_RUN_OPTIONS.secretCmdAttempts()).option(
89978
90966
  "--allow-log-cmd",
89979
90967
  "opt-in: a --log-source cmd:<command> may run as a subprocess (operator-declared only; refused otherwise)",
89980
90968
  false
@@ -90063,8 +91051,12 @@ function registerExploreCommands(program2, deps, buildProgram2) {
90063
91051
  emitExplore(fail("E_EXPLORE_ARGS", `unknown strategy ${JSON.stringify(o.strategy)} (one of ${EXPLORE_STRATEGIES.join(", ")})`));
90064
91052
  return;
90065
91053
  }
90066
- if (o.dialogs !== void 0 && o.dialogs !== "dismiss" && o.dialogs !== "accept") {
90067
- emitExplore(fail("E_EXPLORE_ARGS", `--dialogs must be dismiss or accept, got ${JSON.stringify(o.dialogs)}`));
91054
+ let dialogs;
91055
+ try {
91056
+ dialogs = dialogsFromFlags(o);
91057
+ } catch (err) {
91058
+ if (!(err instanceof GoalRunFlagError)) throw err;
91059
+ emitExplore(fail(err.code, err.message));
90068
91060
  return;
90069
91061
  }
90070
91062
  if (o.saveStorageState !== void 0) {
@@ -90248,19 +91240,12 @@ function registerExploreCommands(program2, deps, buildProgram2) {
90248
91240
  return;
90249
91241
  }
90250
91242
  setKillSwitchOutput(o.json === true ? "envelope" : "human");
90251
- const conversation = {
90252
- ...o.replyWaitMs === void 0 ? {} : { replyWaitMs: Number(o.replyWaitMs) },
90253
- ...o.replyQuietMs === void 0 ? {} : { replyQuietMs: Number(o.replyQuietMs) },
90254
- ...o.replyCeilingMs === void 0 ? {} : { replyCeilingMs: Number(o.replyCeilingMs) },
90255
- ...o.replyMaxChars === void 0 ? {} : { replyMaxChars: Number(o.replyMaxChars) },
90256
- ...o.jobWaitMs === void 0 ? {} : { jobWaitMs: Number(o.jobWaitMs) }
90257
- };
90258
- if (conversation.replyWaitMs !== void 0 && !(Number.isInteger(conversation.replyWaitMs) && conversation.replyWaitMs > 0) || conversation.replyCeilingMs !== void 0 && !(Number.isInteger(conversation.replyCeilingMs) && conversation.replyCeilingMs > 0) || conversation.replyMaxChars !== void 0 && !(Number.isInteger(conversation.replyMaxChars) && conversation.replyMaxChars >= 20 && conversation.replyMaxChars <= 2e3)) {
90259
- emitExplore(fail("E_EXPLORE_ARGS", "--reply-wait-ms and --reply-ceiling-ms must be positive integers; --reply-max-chars an integer in 20..2000"));
90260
- return;
90261
- }
90262
- if (conversation.jobWaitMs !== void 0 && !(Number.isInteger(conversation.jobWaitMs) && conversation.jobWaitMs > 0)) {
90263
- emitExplore(fail("E_EXPLORE_ARGS", "--job-wait-ms must be a positive integer"));
91243
+ let conversation;
91244
+ try {
91245
+ conversation = conversationFromFlags(o);
91246
+ } catch (err) {
91247
+ if (!(err instanceof GoalRunFlagError)) throw err;
91248
+ emitExplore(fail(err.code, err.message));
90264
91249
  return;
90265
91250
  }
90266
91251
  try {
@@ -90301,20 +91286,11 @@ function registerExploreCommands(program2, deps, buildProgram2) {
90301
91286
  let target;
90302
91287
  if (o.url !== void 0) {
90303
91288
  try {
90304
- target = resolveTargetConfig(loadTargetsFile(deps.explore?.targetsConfigPath), new URL(setupRefFreeUrl(o.url)).origin, {
90305
- settleIgnore: o.settleIgnore,
90306
- ignoreNoProgress: o.ignoreNoProgress,
90307
- apiPrefixes: o.apiPrefix,
90308
- deny: o.deny,
90309
- paid: o.paid,
90310
- readRpc: o.readRpc,
90311
- ...o.allowDestructive === true ? { allowDestructive: true } : {},
90312
- ...o.dialogs === void 0 ? {} : { dialogs: o.dialogs },
90313
- ...o.allowWrites === true ? { allowWrites: true } : {},
90314
- allowWrite: o.allowWrite,
90315
- ...o.hangReplayWrites === true ? { hangReplayWrites: true } : {},
90316
- ...o.longPollMs === void 0 ? {} : { longPollMs: Number(o.longPollMs) }
90317
- });
91289
+ target = resolveTargetConfig(
91290
+ loadTargetsFile(deps.explore?.targetsConfigPath),
91291
+ new URL(setupRefFreeUrl(o.url)).origin,
91292
+ targetFlagsFromFlags(o, dialogs)
91293
+ );
90318
91294
  } catch (err) {
90319
91295
  if (err instanceof TargetConfigError) {
90320
91296
  emitExplore(fail(err.code, err.message));
@@ -90451,12 +91427,9 @@ function registerExploreCommands(program2, deps, buildProgram2) {
90451
91427
  return;
90452
91428
  }
90453
91429
  try {
90454
- secretFields = [
90455
- ...o.secretField.map((s3) => parseSecretField(s3, "value", process.env, { allowCmd: o.allowSecretCmd === true })),
90456
- ...o.totp.map((s3) => parseSecretField(s3, "totp", process.env))
90457
- ];
91430
+ secretFields = secretFieldsFromFlags(o);
90458
91431
  } catch (err) {
90459
- if (!(err instanceof SecretFieldSpecError)) throw err;
91432
+ if (!(err instanceof GoalRunFlagError)) throw err;
90460
91433
  emitExplore(fail(err.code, err.message));
90461
91434
  return;
90462
91435
  }
@@ -90791,11 +91764,14 @@ function registerExploreCommands(program2, deps, buildProgram2) {
90791
91764
  emitExplore(fail("E_EXPLORE_ASSERTION", String(err instanceof Error ? err.message : err)));
90792
91765
  return;
90793
91766
  }
90794
- if (o.successWhen !== void 0 && o.successWhen !== "held" && o.successWhen !== "final" && o.successWhen !== "each") {
90795
- emitExplore(fail("E_EXPLORE_ARGS", `--success-when must be "final", "held" or "each", got ${JSON.stringify(o.successWhen)}`));
91767
+ let successWhen;
91768
+ try {
91769
+ successWhen = successWhenFromFlags(o);
91770
+ } catch (err) {
91771
+ if (!(err instanceof GoalRunFlagError)) throw err;
91772
+ emitExplore(fail(err.code, err.message));
90796
91773
  return;
90797
91774
  }
90798
- const successWhen = o.successWhen === "held" || o.successWhen === "final" || o.successWhen === "each" ? o.successWhen : void 0;
90799
91775
  const allowlist = resolveExploreAllowlist(setupRefFreeUrl(o.url), o.allow);
90800
91776
  let fx;
90801
91777
  try {
@@ -91010,8 +91986,63 @@ function registerVerifyFixCommands(program2, deps) {
91010
91986
 
91011
91987
  // src/author-journey-cli.ts
91012
91988
  import { existsSync as existsSync24 } from "node:fs";
91989
+ var AUTHOR_GOAL_RUN_FLAGS = [
91990
+ "successWhen",
91991
+ "allowVacuousChecks",
91992
+ "actionDeltas",
91993
+ "secret",
91994
+ "secretField",
91995
+ "totp",
91996
+ "typeFixture",
91997
+ "fixture",
91998
+ "saveStorageState",
91999
+ "replyWaitMs",
92000
+ "replyQuietMs",
92001
+ "replyCeilingMs",
92002
+ "replyMaxChars",
92003
+ "jobWaitMs",
92004
+ "deny",
92005
+ "paid",
92006
+ "allowDestructive",
92007
+ "dialogs",
92008
+ "readRpc",
92009
+ "hangReplays",
92010
+ "settleIgnore",
92011
+ "longPollMs",
92012
+ "apiPrefix",
92013
+ "ignoreNoProgress",
92014
+ "allowSecretCmd",
92015
+ "secretCmdAttempts"
92016
+ ];
92017
+ var innerOfReloadThen = (spec) => spec.replace(/^\s*reloadThen:/, "");
92018
+ function takeLines(label2, d) {
92019
+ const lines = [` ${label2}: ${d.outcome}${d.stop === void 0 ? "" : ` (stop: ${d.stop})`}${d.actions === void 0 ? "" : ` \xB7 ${d.actions} action(s), ${d.decisions ?? 0} decision(s)`}`];
92020
+ if (d.reason !== void 0) lines.push(` reason: ${d.reason}`);
92021
+ for (const c of d.checks ?? []) lines.push(` check ${c.passed ? "held" : "FAILED"}: ${c.check}${c.passed ? "" : ` \u2014 ${c.detail}`}`);
92022
+ if (d.resultPath !== void 0) lines.push(` result: ${d.resultPath}`);
92023
+ if (d.transcriptPath !== void 0) lines.push(` transcript: ${d.transcriptPath}`);
92024
+ for (const r of d.recordingPaths ?? []) lines.push(` recording: ${r}`);
92025
+ if (d.screenshotsDir !== void 0) lines.push(` screenshots: ${d.screenshotsDir}`);
92026
+ return lines;
92027
+ }
92028
+ function formatAuthorJourneyHuman(data) {
92029
+ const r = data;
92030
+ const lines = [];
92031
+ if (r.outcome === "authored") {
92032
+ const m = r.journey.metadata;
92033
+ lines.push(`AUTHORED: journey ${m.id} (unpromoted; \`jevitate journey promote ${m.id}\` makes it runnable)`);
92034
+ if (m.params.length > 0) lines.push(` params: ${m.params.join(", ")}`);
92035
+ } else {
92036
+ lines.push(`NOT REACHED: ${r.reason}`);
92037
+ }
92038
+ if (r.takes !== void 0) lines.push(` takes: ${r.takes.succeeded}/${r.takes.run} reached the goal (${r.takes.requested} requested)`);
92039
+ if (r.discovery !== void 0) lines.push(...takeLines("discovery", r.discovery));
92040
+ if (r.outcome === "authored") (r.corroboratingTakes ?? []).forEach((d, i) => lines.push(...takeLines(`take ${i + 2}`, d)));
92041
+ return `${lines.join("\n")}
92042
+ `;
92043
+ }
91013
92044
  function registerAuthorJourneyCommands(program2, deps) {
91014
- withBrowserLaunchFlags(
92045
+ const cmd = withBrowserLaunchFlags(
91015
92046
  program2.command("explore-author-journey").description("Jev-driving authors a promotable Journey (authoring plane); never auto-promoted")
91016
92047
  ).option("--url <url>", "target URL (must be an authorized origin)").option("--goal <text>", "natural-language goal").option(
91017
92048
  "--success <spec>",
@@ -91026,7 +92057,19 @@ function registerAuthorJourneyCommands(program2, deps) {
91026
92057
  "authorized origin (repeatable); REPLACES the default allowlist when given (the URL's own origin is used only when --allow is omitted entirely) -- include the URL's own origin explicitly if you still need it",
91027
92058
  (v, prev) => [...prev, v],
91028
92059
  []
91029
- ).option("--max-actions <n>", "hard cap on executed actions", positiveIntArg).option("--max-decisions <n>", "hard cap on model decisions", positiveIntArg).option("--real", "use live Jev + OpenRouter gateways (requires keys)", false).option("--fake-ai", "use deterministic fake gateways (pipeline smoke only)", false).option("--json", "emit a JSON envelope").action(async function() {
92060
+ ).option("--max-actions <n>", "hard cap on executed actions", positiveIntArg).option("--max-decisions <n>", "hard cap on model decisions", positiveIntArg).option("--out <dir>", "directory each take's result, transcript and Recording are written to (default: .jevitate/logs/<date>)").option("--real", "use live Jev + OpenRouter gateways (requires keys)", false).option("--fake-ai", "use deterministic fake gateways (pipeline smoke only)", false).option("--json", "emit a JSON envelope");
92061
+ for (const name of AUTHOR_GOAL_RUN_FLAGS) cmd.addOption(GOAL_RUN_OPTIONS[name]());
92062
+ withScreenshotsFlag(withEmulationFlags(cmd)).addHelpText(
92063
+ "after",
92064
+ [
92065
+ "",
92066
+ "Each take is a full goal run (the same runner and run-shaping flags as `jevitate explore --strategy goal`),",
92067
+ "writing its own result, transcript and Recording (under --out). A take that does not reach the goal",
92068
+ "is reported with its stop reason, its checks' verdicts and those paths. A field code types itself",
92069
+ "(--secret-field/--totp) becomes a secret Journey parameter (secret1, \u2026), passed with --param at replay.",
92070
+ "A reloadThen: check is refused before any browser opens: author with its inner check instead."
92071
+ ].join("\n")
92072
+ ).action(async function() {
91030
92073
  const o = this.opts();
91031
92074
  if (o.storageState !== void 0 && !existsSync24(o.storageState)) {
91032
92075
  emitJson(program2, fail("E_EXPLORE_ARGS", `storage state not found: ${o.storageState}`));
@@ -91042,7 +92085,7 @@ function registerAuthorJourneyCommands(program2, deps) {
91042
92085
  const check2 = parseSuccessSpec(spec);
91043
92086
  if (check2.kind === "reloadThen") {
91044
92087
  throw new Error(
91045
- `${JSON.stringify(spec)}: reloadThen can't be authored into a Journey yet \u2014 use a page check or a requestMade/responseStatus check`
92088
+ `--success ${JSON.stringify(spec)}: a reloadThen check can't be authored into a Journey (a Journey has no reload step); nothing was run. Author with its inner check (--success ${JSON.stringify(innerOfReloadThen(spec))}) and prove persistence with \`jevitate explore --success ${JSON.stringify(spec)}\`, or use a requestMade/responseStatus check`
91046
92089
  );
91047
92090
  }
91048
92091
  successChecks.push(check2);
@@ -91051,6 +92094,17 @@ function registerAuthorJourneyCommands(program2, deps) {
91051
92094
  emitJson(program2, fail("E_EXPLORE_ASSERTION", String(err instanceof Error ? err.message : err)));
91052
92095
  return;
91053
92096
  }
92097
+ let goalRun;
92098
+ try {
92099
+ goalRun = resolveGoalRunFlags(o, o.url, {
92100
+ targetsConfigPath: deps.explore?.targetsConfigPath,
92101
+ warn: (text2) => program2.configureOutput().writeErr?.(text2)
92102
+ });
92103
+ } catch (err) {
92104
+ if (!(err instanceof GoalRunFlagError)) throw err;
92105
+ emitJson(program2, fail(err.code, err.message));
92106
+ return;
92107
+ }
91054
92108
  const allowlist = allowWithExtensions(o.url, resolveExploreAllowlist(o.url, o.allow), browserLaunchFromFlags(o));
91055
92109
  const bounds2 = {};
91056
92110
  if (o.maxActions !== void 0) bounds2.maxActions = Number(o.maxActions);
@@ -91083,19 +92137,24 @@ function registerAuthorJourneyCommands(program2, deps) {
91083
92137
  bounds: Object.keys(bounds2).length > 0 ? bounds2 : void 0,
91084
92138
  browserPortFactory: deps.explore?.browserPortFactory,
91085
92139
  browser: browserLaunchFromFlags(o),
91086
- ...o.storageState !== void 0 ? { storageState: o.storageState } : {}
92140
+ ...o.storageState !== void 0 ? { storageState: o.storageState } : {},
92141
+ goalRun,
92142
+ ...o.out === void 0 ? {} : { outDir: o.out },
92143
+ usage: authorUsage
91087
92144
  }).then((r) => ({ ...r, usage: authorUsage.snapshot() }));
91088
92145
  const envelope = ok(result);
91089
92146
  if (o.json) {
91090
92147
  emitJson(program2, envelope);
91091
92148
  if (result.outcome !== "authored") process.exitCode = 1;
91092
92149
  } else {
91093
- writeRawResult(program2, result);
92150
+ writeHumanResult(program2, result, formatAuthorJourneyHuman);
91094
92151
  process.exitCode = result.outcome === "authored" ? 0 : 1;
91095
92152
  }
91096
92153
  } catch (err) {
91097
92154
  if (err instanceof UnauthorizedExploreTargetError) {
91098
92155
  emitJson(program2, fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
92156
+ } else if (err instanceof FixtureNotFoundError) {
92157
+ emitJson(program2, fail("E_EXPLORE_FIXTURE", err.message));
91099
92158
  } else {
91100
92159
  emitJson(program2, fail("E_AUTHOR_JOURNEY", String(err instanceof Error ? err.message : err)));
91101
92160
  }
@@ -91456,6 +92515,8 @@ async function demoAspect(opts) {
91456
92515
  successAssertion: opts.successAssertion,
91457
92516
  allowlist: env.allowedOrigins,
91458
92517
  journeysDir: join48(work, "explored"),
92518
+ // #369: the discovery take's own artifacts stay in the scratch dir with the rest.
92519
+ outDir: join48(work, "explored-runs"),
91459
92520
  journeyId: id,
91460
92521
  journeyName: aspect,
91461
92522
  takes: 1,
@@ -92536,7 +93597,7 @@ async function promoteMissionTarget(ctx, id) {
92536
93597
 
92537
93598
  // src/mission-queue-cli.ts
92538
93599
  import { readFileSync as readFileSync29 } from "node:fs";
92539
- import { Option } from "commander";
93600
+ import { Option as Option2 } from "commander";
92540
93601
 
92541
93602
  // src/mcp-api.ts
92542
93603
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -93704,7 +94765,7 @@ var CLI_TOOL_SPECS = [
93704
94765
  },
93705
94766
  {
93706
94767
  name: "author_journey",
93707
- description: "`jevitate explore-author-journey`: explore url toward goal (checked by success) several takes, and author an UNPROMOTED Journey from the verified path (promote_journey makes it runnable). Needs real or fakeAi.",
94768
+ description: "`jevitate explore-author-journey`: explore url toward goal (checked by success) several takes, and author an UNPROMOTED Journey from the verified path (promote_journey makes it runnable). Each take is run_exploration's goal run (same run-shaping arguments); a not-reached result carries the discovery take's stop reason, checks and result/transcript/Recording paths. Needs real or fakeAi.",
93708
94769
  command: {
93709
94770
  path: "explore-author-journey",
93710
94771
  params: {
@@ -93719,9 +94780,43 @@ var CLI_TOOL_SPECS = [
93719
94780
  maxActions: n2("--max-actions"),
93720
94781
  maxDecisions: n2("--max-decisions"),
93721
94782
  storageState: session("--storage-state"),
94783
+ out: path("--out"),
94784
+ // #369: every take is explore's goal run — the same run-shaping arguments as run_exploration.
94785
+ successWhen: s2("--success-when", { enum: ["final", "held", "each"] }),
94786
+ allowVacuousChecks: b("--allow-vacuous-checks"),
94787
+ actionDeltas: b("--action-deltas"),
94788
+ typeFixture: { kind: "bound-paths", flag: "--type-fixture" },
94789
+ fixture: path("--fixture"),
94790
+ saveStorageState: session("--save-storage-state"),
94791
+ replyWaitMs: n2("--reply-wait-ms"),
94792
+ replyQuietMs: n2("--reply-quiet-ms"),
94793
+ replyCeilingMs: n2("--reply-ceiling-ms"),
94794
+ replyMaxChars: n2("--reply-max-chars"),
94795
+ jobWaitMs: n2("--job-wait-ms"),
94796
+ deny: many("--deny"),
94797
+ paid: many("--paid"),
94798
+ allowDestructive: b("--allow-destructive"),
94799
+ dialogs: s2("--dialogs", { enum: ["dismiss", "accept"] }),
94800
+ readRpc: many("--read-rpc"),
94801
+ hangReplays: n2("--hang-replays"),
94802
+ settleIgnore: many("--settle-ignore"),
94803
+ longPollMs: n2("--long-poll-ms"),
94804
+ apiPrefix: many("--api-prefix"),
94805
+ ignoreNoProgress: many("--ignore-no-progress"),
94806
+ screenshots: { kind: "screenshots", flag: "--screenshots" },
94807
+ ...EMULATION,
93722
94808
  ...AI
93723
94809
  },
93724
- omitted: { "--journeys-dir": OMIT.storeDir, ...BROWSER_FLAGS, ...JSON_FLAG }
94810
+ omitted: {
94811
+ "--journeys-dir": OMIT.storeDir,
94812
+ ...BROWSER_FLAGS,
94813
+ ...JSON_FLAG,
94814
+ "--secret": OMIT.envSecret,
94815
+ "--secret-field": OMIT.envSecret,
94816
+ "--totp": OMIT.envSecret,
94817
+ "--allow-secret-cmd": OMIT.envSecret,
94818
+ "--secret-cmd-attempts": OMIT.envSecret
94819
+ }
93725
94820
  }
93726
94821
  },
93727
94822
  {
@@ -94886,7 +95981,7 @@ next: jevitate mission run \u2014 then jevitate mission result ${id}
94886
95981
  }
94887
95982
  function registerMissionQueueCommands(program2, mission, deps) {
94888
95983
  withEmulationFlags(
94889
- mission.command("queue <target>").description("enqueue an exploration mission against a PROMOTED target \u2014 only queues; `mission run` drains (MCP queue_exploration)").addOption(new Option("--strategy <strategy>", `${MISSION_STRATEGIES.join(" | ")} (required)`).choices(MISSION_STRATEGIES)).option("--goal <text>", "goal-based: the objective (exactly one of --goal/--feature/--route)").option("--feature <name>", "feature: the capability to test (goal-based: the objective)").option("--route <glob>", "coverage/exploratory/adversarial/feature: an in-scope route glob, e.g. /thread/** (goal-based: the objective)").option("--success <spec>", "goal-based: the independent success check (required there), e.g. urlIncludes:/done \u2014 the `explore --success` page-check forms").option("--max-actions <n>", "budget: max actions (bounded by the queue's ceiling)", positiveIntArg).option("--max-decisions <n>", "budget: max decisions", positiveIntArg).option("--max-candidates <n>", "budget: max candidates", positiveIntArg).option("--invariants <file>", "app-declared invariants JSON file (the `explore --invariants` format; probes GET/HEAD on the target's origins; no authFrom.secret)").option("--record-video", "record a video of the run (headless too), written next to its result; listed as videoPaths").option("--screenshots [mode]", "masked screenshots + index.md next to the result: screens (default, one per distinct screen) | steps (one per step)").option("--evidence-video", "per defect: a captioned evidence clip of its minimal repro + before/at screenshots (defects[].evidence)").option("--persona <name>", "run as this persona: its session in ~/.jevitate/targets.json (personas) for the target's origin \u2014 a name, never a path"),
95984
+ mission.command("queue <target>").description("enqueue an exploration mission against a PROMOTED target \u2014 only queues; `mission run` drains (MCP queue_exploration)").addOption(new Option2("--strategy <strategy>", `${MISSION_STRATEGIES.join(" | ")} (required)`).choices(MISSION_STRATEGIES)).option("--goal <text>", "goal-based: the objective (exactly one of --goal/--feature/--route)").option("--feature <name>", "feature: the capability to test (goal-based: the objective)").option("--route <glob>", "coverage/exploratory/adversarial/feature: an in-scope route glob, e.g. /thread/** (goal-based: the objective)").option("--success <spec>", "goal-based: the independent success check (required there), e.g. urlIncludes:/done \u2014 the `explore --success` page-check forms").option("--max-actions <n>", "budget: max actions (bounded by the queue's ceiling)", positiveIntArg).option("--max-decisions <n>", "budget: max decisions", positiveIntArg).option("--max-candidates <n>", "budget: max candidates", positiveIntArg).option("--invariants <file>", "app-declared invariants JSON file (the `explore --invariants` format; probes GET/HEAD on the target's origins; no authFrom.secret)").option("--record-video", "record a video of the run (headless too), written next to its result; listed as videoPaths").option("--screenshots [mode]", "masked screenshots + index.md next to the result: screens (default, one per distinct screen) | steps (one per step)").option("--evidence-video", "per defect: a captioned evidence clip of its minimal repro + before/at screenshots (defects[].evidence)").option("--persona <name>", "run as this persona: its session in ~/.jevitate/targets.json (personas) for the target's origin \u2014 a name, never a path"),
94890
95985
  // #329: a queued mission persists only viewport/device; --geolocation is not offered here.
94891
95986
  { geolocation: false }
94892
95987
  ).option("--dir <path>", QUEUE_DIR_HELP).option("--targets-dir <path>", "mission targets directory (default: ~/.jevitate/missions/targets)").option("--json", "emit a JSON envelope").action(async function(target) {
@@ -95809,7 +96904,7 @@ result ${o.result}
95809
96904
 
95810
96905
  // src/inbox-cli.ts
95811
96906
  import { readFileSync as readFileSync32 } from "node:fs";
95812
- import { Option as Option2 } from "commander";
96907
+ import { Option as Option3 } from "commander";
95813
96908
  var INBOX_DIR_HELP = "inbox store directory (default: ~/.jevitate/inbox \u2014 the dir `jevitate mcp` and `jevitate ui` use)";
95814
96909
  var REDACTED = "***REDACTED***";
95815
96910
  function withInboxFlags(cmd) {
@@ -95945,7 +97040,7 @@ function registerInboxCommands(program2, deps) {
95945
97040
  if (args !== void 0) await run(this, "queue_retrieval", args, formatQueuedHuman, "cannot queue the item");
95946
97041
  });
95947
97042
  queueFlags(
95948
- inbox.command("queue-action").description("ask a human for a decision \u2014 an 'approval' item by default; only queues (MCP queue_action)").addOption(new Option2("--kind <kind>", "approval (default) | handback | review").choices(["approval", "handback", "review"]))
97043
+ inbox.command("queue-action").description("ask a human for a decision \u2014 an 'approval' item by default; only queues (MCP queue_action)").addOption(new Option3("--kind <kind>", "approval (default) | handback | review").choices(["approval", "handback", "review"]))
95949
97044
  ).action(async function() {
95950
97045
  const args = queueArgs(this);
95951
97046
  if (args !== void 0) await run(this, "queue_action", args, formatQueuedHuman, "cannot queue the item");