@enricai/barnacle 1.6.2 → 1.6.3
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/README.md +23 -18
- package/dist/api/errors.d.ts +3 -3
- package/dist/api/errors.d.ts.map +1 -1
- package/dist/api/errors.js +4 -4
- package/dist/api/errors.js.map +1 -1
- package/dist/api/helpers/reply.d.ts +1 -1
- package/dist/api/helpers/reply.js +1 -1
- package/dist/cache/keyed-ttl-cache.d.ts +4 -4
- package/dist/cache/keyed-ttl-cache.js +2 -2
- package/dist/lib/applicant-payload.d.ts +4 -4
- package/dist/lib/applicant-payload.js +4 -4
- package/dist/lib/application-address.d.ts +3 -3
- package/dist/lib/application-address.js +5 -5
- package/dist/lib/application-address.js.map +1 -1
- package/dist/lib/application-resume.d.ts +4 -4
- package/dist/lib/application-resume.js +4 -4
- package/dist/lib/dispatch-metrics.js +1 -1
- package/dist/lib/llm/judges/verify-submit.d.ts +1 -1
- package/dist/lib/llm/judges/verify-submit.js +1 -1
- package/dist/lib/llm/schemas.js +3 -3
- package/dist/lib/phone.d.ts +1 -1
- package/dist/lib/phone.js +1 -1
- package/dist/lib/telemetry/call-types.d.ts +1 -1
- package/dist/lib/telemetry/call-types.js +1 -1
- package/dist/lib/tracking-click.d.ts +5 -3
- package/dist/lib/tracking-click.d.ts.map +1 -1
- package/dist/lib/tracking-click.js +5 -3
- package/dist/lib/tracking-click.js.map +1 -1
- package/dist/lib/us-states.d.ts +1 -1
- package/dist/lib/us-states.js +1 -1
- package/dist/plugins/loader.js +2 -2
- package/dist/scraper/cookie-jar.d.ts +2 -2
- package/dist/scraper/cookie-jar.js +2 -2
- package/dist/scraper/errors.d.ts +1 -12
- package/dist/scraper/errors.d.ts.map +1 -1
- package/dist/scraper/errors.js +2 -16
- package/dist/scraper/errors.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +43 -27
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +70 -60
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/http-client.d.ts +11 -1
- package/dist/scraper/http-client.d.ts.map +1 -1
- package/dist/scraper/http-client.js +90 -3
- package/dist/scraper/http-client.js.map +1 -1
- package/dist/scraper/raw-fetch.d.ts +2 -2
- package/dist/scraper/raw-fetch.js +1 -1
- package/dist/scraper/require-response-field.d.ts +1 -1
- package/dist/scraper/require-response-field.js +1 -1
- package/dist/scraper/session-warmup.d.ts +1 -1
- package/dist/scraper/session-warmup.js +1 -1
- package/dist/scripts/migrate-telemetry-dir-names.d.ts +2 -2
- package/dist/scripts/migrate-telemetry-dir-names.d.ts.map +1 -1
- package/dist/scripts/migrate-telemetry-dir-names.js +1 -1
- package/dist/scripts/recon-browser.d.ts +2 -2
- package/dist/scripts/recon-browser.js +14 -14
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +24 -0
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +23 -7
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/scripts/recon-replay-jobs.d.ts +9 -4
- package/dist/scripts/recon-replay-jobs.d.ts.map +1 -1
- package/dist/scripts/recon-replay-jobs.js +15 -8
- package/dist/scripts/recon-replay-jobs.js.map +1 -1
- package/dist/scripts/recon-shared.d.ts +10 -0
- package/dist/scripts/recon-shared.d.ts.map +1 -1
- package/dist/scripts/recon-shared.js +25 -0
- package/dist/scripts/recon-shared.js.map +1 -1
- package/dist/site-plugin.d.ts +4 -4
- package/dist/testing/persona-fixture.d.ts +4 -4
- package/dist/testing/persona-fixture.d.ts.map +1 -1
- package/dist/testing/persona-fixture.js +4 -4
- package/dist/testing/persona-fixture.js.map +1 -1
- package/dist/types/dispatch-metrics.d.ts +1 -1
- package/dist/types/dispatch-metrics.js +1 -1
- package/package.json +1 -1
|
@@ -355,7 +355,7 @@ const SELECT_SETTLE_MS = 400;
|
|
|
355
355
|
/**
|
|
356
356
|
* Extra bounded poll window for a network-only advance whose real
|
|
357
357
|
* `TransitionWorklet(type="next")` POST lands AFTER the `STEP_PAUSE_MS`
|
|
358
|
-
* snapshot.
|
|
358
|
+
* snapshot. The wizard ATS's "Next" click fires a fast `WorkletPayload` autosave
|
|
359
359
|
* first, then the actual transition ~0.6-2s+ later — so a one-shot check
|
|
360
360
|
* false-negatives the advance, retries the click, and the stale retry fires a
|
|
361
361
|
* `back` that bounces the wizard. Additive to `STEP_PAUSE_MS`; only spent on an
|
|
@@ -429,7 +429,7 @@ exports.STEP_WATCHDOG_MS = 120_000;
|
|
|
429
429
|
* browser-context `page.evaluate` expression string, where a Node RegExp can't
|
|
430
430
|
* cross the boundary.
|
|
431
431
|
*
|
|
432
|
-
* Why the additions beyond the original Angular/Bootstrap set:
|
|
432
|
+
* Why the additions beyond the original Angular/Bootstrap set: the Angular/MUI
|
|
433
433
|
* wizard mixes Angular pages AND Material-UI (React) pages (Review, self-ID,
|
|
434
434
|
* COMPENSATION). MUI marks invalid controls with `Mui-error` (class) +
|
|
435
435
|
* `aria-invalid="true"` (attribute), NONE of which the ng-only regex matched —
|
|
@@ -496,11 +496,11 @@ async function snapshotPage(page, signalCounter) {
|
|
|
496
496
|
/**
|
|
497
497
|
* Detect whether a 2xx response body indicates the server REJECTED the
|
|
498
498
|
* application despite returning a 2xx HTTP status. Many ATSs use a "200 OK
|
|
499
|
-
* with rejection envelope" pattern instead of a 4xx:
|
|
499
|
+
* with rejection envelope" pattern instead of a 4xx: one JSON-envelope ATS returns
|
|
500
500
|
* `{not_qualified: true, error: "Not qualified reason: <field>"}`,
|
|
501
501
|
* Greenhouse uses `{rejected: true, reason: "..."}`, Lever uses
|
|
502
502
|
* `{qualified: false, reason: "..."}`, Workday uses
|
|
503
|
-
* `{status: "rejected"}`. Empirically verified on
|
|
503
|
+
* `{status: "rejected"}`. Empirically verified on a JSON-envelope ATS: 4/6 historical
|
|
504
504
|
* /integrated_apply 200s on this codebase had `not_qualified: true` and
|
|
505
505
|
* we treated them as wins because the audit only checked HTTP status.
|
|
506
506
|
*
|
|
@@ -688,7 +688,7 @@ function latestCaptureIndex(recentCaptures) {
|
|
|
688
688
|
* read straight from `capturesDir` — NOT from the in-memory `recentCaptures`
|
|
689
689
|
* array, which is front-evicted to `RECENT_CAPTURES_WINDOW` and therefore drops
|
|
690
690
|
* a step's transition when >20 captures flood during the step (measured 43
|
|
691
|
-
* across one
|
|
691
|
+
* across one wizard-ATS cascade). Scanning disk by filename index is eviction-proof:
|
|
692
692
|
* the transition file is always on disk regardless of array churn. `.decoded.json`
|
|
693
693
|
* sidecars are excluded (the raw file carries `requestPostData`). Sorted by index
|
|
694
694
|
* so callers scan in capture order. Returns [] when the dir is unreadable.
|
|
@@ -712,7 +712,7 @@ function capturesAfterIndex(preIdx, capturesDir) {
|
|
|
712
712
|
* Detects a multi-page-wizard RESTART / backward navigation by scanning the
|
|
713
713
|
* captures written during this step for a configured restart-signal pattern
|
|
714
714
|
* (e.g. `init-apply`, `application_canceled=true`). The restart signal is often
|
|
715
|
-
* a plain GET (
|
|
715
|
+
* a plain GET (the wizard ATS's `GET .../init-apply?...&application_canceled=true`),
|
|
716
716
|
* so it scans the raw capture files' `url` field (GETs are written to disk even
|
|
717
717
|
* though they're dropped from `recentCaptureMeta`). Window scoped by
|
|
718
718
|
* `preIdx` via {@link capturesAfterIndex} (eviction-proof). Returns the matching
|
|
@@ -722,7 +722,7 @@ function capturesAfterIndex(preIdx, capturesDir) {
|
|
|
722
722
|
* Does any same-window network capture's REQUEST BODY match the configured
|
|
723
723
|
* transition pattern? Proves an interior "advance"/"Next" step really moved the
|
|
724
724
|
* wizard forward when advance and non-advance mutations share one endpoint URL
|
|
725
|
-
* (e.g.
|
|
725
|
+
* (e.g. the wizard ATS's `/gq`: a real advance is a `TransitionWorklet` mutation, a
|
|
726
726
|
* field edit is `EditQuestionItem` — same URL, only the body differs, so a
|
|
727
727
|
* URL/meta-based check can't tell them apart). Window scoped by `preIdx` via
|
|
728
728
|
* {@link capturesAfterIndex} (disk-scan by filename index — eviction-proof, so a
|
|
@@ -761,7 +761,7 @@ function windowHasTransitionBody(params) {
|
|
|
761
761
|
* Stricter sibling of {@link windowHasTransitionBody}: a same-window capture
|
|
762
762
|
* whose request body matches the transition pattern AND whose parsed
|
|
763
763
|
* `variables.input.type === "next"`. The mutation NAME alone is a weak
|
|
764
|
-
* distinguisher — on
|
|
764
|
+
* distinguisher — on the wizard ATS a `back` bounce is ALSO a `TransitionWorklet`
|
|
765
765
|
* mutation (its body contains the pattern too) and would wrongly count as an
|
|
766
766
|
* advance; and the fast `WorkletPayload` autosave that precedes the real
|
|
767
767
|
* transition is a different mutation with no `input.type`. Requiring the parsed
|
|
@@ -808,7 +808,7 @@ function windowHasAdvanceTransition(params) {
|
|
|
808
808
|
* the RC2 gate is unit-testable.
|
|
809
809
|
*
|
|
810
810
|
* An interior "Next" on an SPA where an advance and a mere field-edit share one
|
|
811
|
-
* endpoint (
|
|
811
|
+
* endpoint (the wizard ATS's `/gq`: TransitionWorklet vs EditQuestionItem — same URL,
|
|
812
812
|
* different body) can fire a network POST that is NOT a real advance; the
|
|
813
813
|
* fallback's htmlDelta/textChanged/checked-radio signals are then validation
|
|
814
814
|
* re-renders / field toggles that don't move the wizard. So for an opted-in
|
|
@@ -833,7 +833,7 @@ function shouldVetoFallbackAdvance(params) {
|
|
|
833
833
|
* transition fired — a URL change OR a real `type=next` (`networkIsRealAdvance`).
|
|
834
834
|
* A DOM change alone is a validation re-render / field toggle that never moves
|
|
835
835
|
* the wizard. Crucially this must veto even when a NON-advancing network POST
|
|
836
|
-
* fired (
|
|
836
|
+
* fired (the wizard ATS's `WorkletPayload` autosave): keying the veto on "no network
|
|
837
837
|
* at all" let a rephrase that triggered an autosave + DOM reflow false-verify an
|
|
838
838
|
* advance, desyncing the flow from the wizard. Returns whether the DOM signal is
|
|
839
839
|
* ALLOWED to verify: false = veto it. Non-advance/field steps, sites without the
|
|
@@ -924,7 +924,7 @@ function describeAttemptEffectSignals(pre, post, recentCaptureMeta, preMetaLengt
|
|
|
924
924
|
* keeps healing opportunistically.
|
|
925
925
|
*/
|
|
926
926
|
function shouldSkipTechnique(params) {
|
|
927
|
-
const { technique, priorAttempts, advanceUnmovedAfterAttempt1, phantomClickAfterAttempt1 } = params;
|
|
927
|
+
const { technique, priorAttempts, advanceUnmovedAfterAttempt1, phantomClickAfterAttempt1, submitShapedStep, } = params;
|
|
928
928
|
// Unmoved-advance short-circuit (measured: attempts 2-4 recovered a stuck
|
|
929
929
|
// advance 0 times in 289 steps). When attempt-1's act-string clicked the Next
|
|
930
930
|
// and the wizard did NOT move forward (non-advancing POST, or no effect),
|
|
@@ -954,14 +954,20 @@ function shouldSkipTechnique(params) {
|
|
|
954
954
|
// light-DOM-only view of the page and would no-op identically, so skip
|
|
955
955
|
// straight to deep-submit-locator (attempt 2) instead. llm-rephrase
|
|
956
956
|
// (attempt 5) is never skipped — a differently-worded instruction is still
|
|
957
|
-
// a distinct attempt worth trying if the deep locator also fails.
|
|
957
|
+
// a distinct attempt worth trying if the deep locator also fails. Gated on
|
|
958
|
+
// submitShapedStep: the deep submit-control locator ranks submit-shaped
|
|
959
|
+
// candidates only, so on a non-submit control (e.g. a radio/checkbox) it
|
|
960
|
+
// is a guaranteed no-op — leave the normal ladder intact so
|
|
961
|
+
// structured-click / observe-act-exclude, the techniques that can
|
|
962
|
+
// actually click it, still run.
|
|
958
963
|
if (phantomClickAfterAttempt1 === true &&
|
|
964
|
+
submitShapedStep === true &&
|
|
959
965
|
(technique === "observe-act" ||
|
|
960
966
|
technique === "structured-click" ||
|
|
961
967
|
technique === "observe-act-exclude")) {
|
|
962
968
|
return {
|
|
963
969
|
skip: true,
|
|
964
|
-
reason: "attempt 1 was a phantom click (reported success, zero observable effect); re-observe/re-click cannot reach a target the light-DOM resolver can't see — escalating to the deep submit-control locator",
|
|
970
|
+
reason: "attempt 1 was a phantom click (reported success, zero observable effect) on a submit-shaped step; re-observe/re-click cannot reach a target the light-DOM resolver can't see — escalating to the deep submit-control locator",
|
|
965
971
|
};
|
|
966
972
|
}
|
|
967
973
|
if (technique === "structured-click") {
|
|
@@ -1035,7 +1041,7 @@ function isSubmitRevealedInvalid(params) {
|
|
|
1035
1041
|
* landed within the poll window (`networkIsRealAdvance` false), the button works
|
|
1036
1042
|
* but the wizard is refusing to advance (a precondition isn't met, e.g. a
|
|
1037
1043
|
* required field the flow answers on a LATER step). Re-clicking the same button
|
|
1038
|
-
* only re-fires the autosave / a `back` bounce — measured across
|
|
1044
|
+
* only re-fires the autosave / a `back` bounce — measured across wizard-ATS runs as the
|
|
1039
1045
|
* next→back oscillation. Break to replan instead, which can reorder a later step
|
|
1040
1046
|
* forward. Conservative: any condition unmet → run the full cascade as before.
|
|
1041
1047
|
* Never fires on final/submit steps (they own `isSubmitRevealedInvalid`) or on
|
|
@@ -1088,7 +1094,7 @@ async function rephraseWithLLM(client, originalStep, triedSelectors, observeCand
|
|
|
1088
1094
|
* invalid form fields and visible error messages so the rephrase LLM can
|
|
1089
1095
|
* propose corrective fills instead of
|
|
1090
1096
|
* just "click harder" — the previous limitation observed in the
|
|
1091
|
-
* telemetry of the
|
|
1097
|
+
* telemetry of the JSON-envelope ATS run (every rephrase converged on
|
|
1092
1098
|
* "Click Submit Application using JavaScript" because the prompt had no
|
|
1093
1099
|
* signal that the form was invalid).
|
|
1094
1100
|
*/
|
|
@@ -1433,8 +1439,8 @@ const BODY_EXCERPT_FORM_WINDOW = 16_000;
|
|
|
1433
1439
|
* Default: first 8KB. That window held for tenants whose form was at the
|
|
1434
1440
|
* top of the page (early sweeps in 2026-06).
|
|
1435
1441
|
*
|
|
1436
|
-
* For pages where the form HTML lives below 8KB (verified on
|
|
1437
|
-
*
|
|
1442
|
+
* For pages where the form HTML lives below 8KB (verified on an Angular
|
|
1443
|
+
* apply SPA: ng-invalid first appears at byte ~15,500 after a header
|
|
1438
1444
|
* of Angular hydration JS + chrome), the 8KB cap silently produced
|
|
1439
1445
|
* "FORM FIELDS CURRENTLY MARKED INVALID: (none)" in the replan prompt,
|
|
1440
1446
|
* leaving the LLM with no evidence and causing it to hallucinate steps
|
|
@@ -1445,7 +1451,7 @@ const BODY_EXCERPT_FORM_WINDOW = 16_000;
|
|
|
1445
1451
|
*
|
|
1446
1452
|
* Site-agnostic: the markers we look for (ng-invalid, mat-form-field-
|
|
1447
1453
|
* invalid, is-invalid, <form) are framework-level CSS-class conventions
|
|
1448
|
-
* used across countless SPAs, not
|
|
1454
|
+
* used across countless SPAs, not specific to any one ATS.
|
|
1449
1455
|
*/
|
|
1450
1456
|
function selectBodyExcerpt(body) {
|
|
1451
1457
|
if (body.length <= BODY_EXCERPT_DEFAULT_CAP)
|
|
@@ -1476,7 +1482,7 @@ async function extractLivePageFormEvidence(page, options) {
|
|
|
1476
1482
|
}
|
|
1477
1483
|
// Pick a body excerpt that's likely to contain the form's invalid-field
|
|
1478
1484
|
// markers. Default 8KB cap unless the body has ng-invalid / <form past
|
|
1479
|
-
// the cap (typical of
|
|
1485
|
+
// the cap (typical of an Angular apply SPA, where the form starts
|
|
1480
1486
|
// ~15KB into a page of Angular hydration scaffolding). See
|
|
1481
1487
|
// selectBodyExcerpt for details.
|
|
1482
1488
|
const bodyExcerpt = selectBodyExcerpt(body);
|
|
@@ -1542,7 +1548,7 @@ async function extractLivePageFormEvidence(page, options) {
|
|
|
1542
1548
|
* matches `ng-invalid` because of bubbling, but its child `<app-input
|
|
1543
1549
|
* class="ng-invalid">` ALSO matches; `:not(:has(...))` filters out the parent.
|
|
1544
1550
|
*
|
|
1545
|
-
*
|
|
1551
|
+
* A JSON-envelope ATS tenant's smoke (run 1781478440322) showed E1's prompt
|
|
1546
1552
|
* instruction ("prefer the leaf, not the bubbled parent") only got Haiku from
|
|
1547
1553
|
* 5 wrong fields → 1 wrong field — still surfaced `(unlabeled) <ol>` instead
|
|
1548
1554
|
* of `<app-input autocomplete="zip-code">` at byte 95,033. All 3 replans + 4
|
|
@@ -1828,7 +1834,7 @@ ownBackendHostnames, capturesDir = (0, recon_shared_1.resolveReconRunDir)().grap
|
|
|
1828
1834
|
}
|
|
1829
1835
|
/**
|
|
1830
1836
|
* Surface Google Analytics Measurement Protocol events captured during a
|
|
1831
|
-
* step's attempt window.
|
|
1837
|
+
* step's attempt window. A JSON-envelope ATS (and most GA4-instrumented SPAs) emits
|
|
1832
1838
|
* `view_secondPage`, `view_thankYouPage`, `form_submit` and similar events
|
|
1833
1839
|
* via `https://www.google-analytics.com/g/collect` — the engine already
|
|
1834
1840
|
* stores these in `recentCaptures[]` but no code reads them. Without
|
|
@@ -2101,9 +2107,9 @@ function harvestFieldErrors(body) {
|
|
|
2101
2107
|
return [];
|
|
2102
2108
|
const out = [];
|
|
2103
2109
|
const rec = body;
|
|
2104
|
-
// Singular `{error: "message"}` shape used by
|
|
2110
|
+
// Singular `{error: "message"}` shape used by JSON-envelope ATSs, Lever, Greenhouse,
|
|
2105
2111
|
// and any REST API following the {error:string} terse-error convention.
|
|
2106
|
-
// Verified on
|
|
2112
|
+
// Verified on a JSON-envelope ATS tenant: /integrated_apply 422 body is
|
|
2107
2113
|
// exactly {"error":"Resume is blank"} — no `errors`, no `message`. Before
|
|
2108
2114
|
// J', this was caught by neither the array branch below nor the
|
|
2109
2115
|
// extractSubmitFailureEvidence fallback at line 2028, resulting in
|
|
@@ -2180,7 +2186,7 @@ const UPLOAD_NETWORK_POLL_INTERVAL_MS = 250;
|
|
|
2180
2186
|
/**
|
|
2181
2187
|
* How long the upload primitive waits for the async ResumeUpload widget (and its
|
|
2182
2188
|
* lazily-mounted `<input type=file>`) to render before deciding the input is
|
|
2183
|
-
* absent.
|
|
2189
|
+
* absent. The wizard ATS mounts the MUI/react-dropzone widget ~5s after the
|
|
2184
2190
|
* wizard lands on the Apply page, so a single probe races that mount and the
|
|
2185
2191
|
* primitive wrongly falls into the click-to-surface path (or skips). Only
|
|
2186
2192
|
* reached on `upload:true` steps, so it never slows a page with no upload step.
|
|
@@ -2190,7 +2196,7 @@ const UPLOAD_WIDGET_RENDER_ATTEMPTS = 17;
|
|
|
2190
2196
|
/**
|
|
2191
2197
|
* URL substrings that mark a POST as an actual resume/attachment upload rather
|
|
2192
2198
|
* than coincidental traffic (analytics beacons, `/interruption_check`,
|
|
2193
|
-
* geocoders).
|
|
2199
|
+
* geocoders). The wizard ATS posts the resume to an `attachment_upload_url` that
|
|
2194
2200
|
* matches `/attachment`; the others cover the other ATS upload sinks. Module-
|
|
2195
2201
|
* level so both the raw-input path and the click-to-surface path share one list.
|
|
2196
2202
|
*/
|
|
@@ -2229,7 +2235,7 @@ async function waitForUploadNetworkSignal(params) {
|
|
|
2229
2235
|
* True when a control's text/aria-label denotes a resume-upload affordance
|
|
2230
2236
|
* (the button that surfaces a hidden `<input type=file>` or opens a chooser).
|
|
2231
2237
|
* Pure + exported for unit tests; the vocabulary is intentionally generic so it
|
|
2232
|
-
* benefits any MUI/React/dropzone ATS, not just
|
|
2238
|
+
* benefits any MUI/React/dropzone ATS, not just the wizard ATS. Rejects negative
|
|
2233
2239
|
* lookalikes ("upload later", "no file", a bare "submit") so the click-to-
|
|
2234
2240
|
* surface path never fires a skip/decline/submit control.
|
|
2235
2241
|
*/
|
|
@@ -2303,7 +2309,7 @@ async function tryUploadPrimitive(params) {
|
|
|
2303
2309
|
return false;
|
|
2304
2310
|
}
|
|
2305
2311
|
if ((inputCount ?? 0) === 0) {
|
|
2306
|
-
//
|
|
2312
|
+
// MUI wizard and other react-dropzone widgets can render NO <input type=file>
|
|
2307
2313
|
// at all (a click surfaces it, or a native chooser opens). Try to surface it
|
|
2308
2314
|
// (click-to-mount or CDP native-chooser interception) before giving up.
|
|
2309
2315
|
logger.info("upload primitive: no <input type=file> after render wait; attempting click-to-surface");
|
|
@@ -2345,7 +2351,7 @@ async function attachToSurfacedInput(params) {
|
|
|
2345
2351
|
// ControlValueAccessor binds at the wrapper level — and the wrapper's
|
|
2346
2352
|
// change handler doesn't observe input.files mutations directly.
|
|
2347
2353
|
//
|
|
2348
|
-
// Verified on
|
|
2354
|
+
// Verified on a JSON-envelope ATS tenant: setInputFiles
|
|
2349
2355
|
// populated input.files but the subsequent /integrated_apply submit
|
|
2350
2356
|
// had no `apply[resume]` multipart field — the framework wrapper
|
|
2351
2357
|
// never registered the file in its internal state. Server returned
|
|
@@ -2408,7 +2414,7 @@ async function attachToSurfacedInput(params) {
|
|
|
2408
2414
|
}
|
|
2409
2415
|
/**
|
|
2410
2416
|
* Recover an upload for widgets that mount NO `<input type=file>` until a button
|
|
2411
|
-
* is clicked (
|
|
2417
|
+
* is clicked (the MUI wizard's `ResumeUpload`, react-dropzone). Ordered cheapest-
|
|
2412
2418
|
* first: (DZ) a synthetic drag-drop on the dropzone; then, arming CDP native-
|
|
2413
2419
|
* chooser interception BEFORE any click (a chooser-opening click with no
|
|
2414
2420
|
* interception blocks the run — the single biggest risk), click the upload
|
|
@@ -2595,7 +2601,7 @@ async function setFilesViaCdp(params) {
|
|
|
2595
2601
|
* Parse a select/dropdown flow step into the option to choose and (when
|
|
2596
2602
|
* present) the question label that scopes which dropdown it targets.
|
|
2597
2603
|
*
|
|
2598
|
-
* Why:
|
|
2604
|
+
* Why: the Angular/MUI wizard ATS renders dropdowns as `MuiNativeSelect` native `<select>`
|
|
2599
2605
|
* with `tabindex="-1"` — removed from the accessibility tree, so Stagehand
|
|
2600
2606
|
* observe returns `[]` and the cascade can never select an option. The select
|
|
2601
2607
|
* primitive answers these directly from the DOM, but needs the target option
|
|
@@ -2644,7 +2650,7 @@ function parseSelectStep(instruction) {
|
|
|
2644
2650
|
* Why this exists (sibling of `parseSelectStep`): `parseSelectStep`
|
|
2645
2651
|
* deliberately excludes bare radio steps ("a bare 'click the Yes answer' is a
|
|
2646
2652
|
* radio"), leaving radios with no DOM-direct primitive — they fall to the
|
|
2647
|
-
* observe cascade, which on
|
|
2653
|
+
* observe cascade, which on the wizard ATS's MUI radio markup resolves the step
|
|
2648
2654
|
* to a wrapper `<div>`/`<span>` (not the `<input type=radio>`) and commits via
|
|
2649
2655
|
* a bare `el.click()` that never triggers React's controlled-state `onChange`.
|
|
2650
2656
|
* The field stays `Mui-error` "required", Next no-ops, and the wizard walls at
|
|
@@ -2696,7 +2702,7 @@ const PRIMITIVE_ENUMERATE_ATTEMPTS = 5;
|
|
|
2696
2702
|
const PRIMITIVE_ENUMERATE_RETRY_MS = 600;
|
|
2697
2703
|
/**
|
|
2698
2704
|
* Run a primitive's read-only DOM enumerate with a bounded settle-retry. SPA
|
|
2699
|
-
* wizards
|
|
2705
|
+
* wizards frequently render the target widget a beat AFTER the flow
|
|
2700
2706
|
* step fires — the first evaluate sees an empty page, so the primitive would
|
|
2701
2707
|
* give up even though the widget appears moments later. Re-run the enumerate up
|
|
2702
2708
|
* to `PRIMITIVE_ENUMERATE_ATTEMPTS` times, waiting `PRIMITIVE_ENUMERATE_RETRY_MS`
|
|
@@ -2723,7 +2729,7 @@ async function pollEnumerate(page, expr, isPresent, opts) {
|
|
|
2723
2729
|
* Bounded poll for the real advance-transition POST to appear in this step's
|
|
2724
2730
|
* capture window. The verifiers snapshot once after `STEP_PAUSE_MS`, but the
|
|
2725
2731
|
* genuine `TransitionWorklet(type="next")` POST can land hundreds of ms to 2s+
|
|
2726
|
-
* AFTER that snapshot (
|
|
2732
|
+
* AFTER that snapshot (the wizard ATS fires a fast `WorkletPayload` autosave first). A
|
|
2727
2733
|
* one-shot check false-negatives the advance, retries the click, and the stale
|
|
2728
2734
|
* retry fires a `back` — a next→back oscillation that never leaves the page.
|
|
2729
2735
|
* Re-check {@link windowHasAdvanceTransition} every `intervalMs` until it matches
|
|
@@ -2757,7 +2763,7 @@ async function waitForTransitionBody(params) {
|
|
|
2757
2763
|
* Site-agnostic select primitive: answer a native `<select>` dropdown by
|
|
2758
2764
|
* directly setting its value in the DOM, bypassing Stagehand observe/act.
|
|
2759
2765
|
*
|
|
2760
|
-
* Why this exists (parallels `tryUploadPrimitive`):
|
|
2766
|
+
* Why this exists (parallels `tryUploadPrimitive`): MUI wizard dropdowns are
|
|
2761
2767
|
* `MuiNativeSelect` native `<select>` elements carrying `tabindex="-1"`, which
|
|
2762
2768
|
* removes them from the accessibility tree. Stagehand observe returns `[]` for
|
|
2763
2769
|
* them, so the cascade's `selectOption` (which needs a resolved locator) never
|
|
@@ -2787,7 +2793,7 @@ async function waitForTransitionBody(params) {
|
|
|
2787
2793
|
* validation tick can be awaited: MUI `NativeSelect` re-runs required-validation
|
|
2788
2794
|
* a beat after the synthetic `change`, so an immediate `sel.value === value`
|
|
2789
2795
|
* readback passes even when the FormControl still flags the field required (and
|
|
2790
|
-
* a later worklet re-render then wipes the DOM-only value — the exact
|
|
2796
|
+
* a later worklet re-render then wipes the DOM-only value — the exact wizard-ATS
|
|
2791
2797
|
* Job-Related failure). Returns `stillInvalid` so {@link trySelectPrimitive} can
|
|
2792
2798
|
* refuse to claim success on an uncommitted select, routing to the cascade/replan
|
|
2793
2799
|
* instead of silently advancing. Walks ≤6 ancestors for the invalid marker, same
|
|
@@ -2986,7 +2992,7 @@ const DECLINE_OPTION_MARKERS = [
|
|
|
2986
2992
|
* Pick an option to satisfy a REQUIRED select on a catch-all step, from the
|
|
2987
2993
|
* select's option TEXTS (placeholder already excluded upstream). Policy: take
|
|
2988
2994
|
* the first non-decline option (a plausible substantive answer — the operator
|
|
2989
|
-
* accepts LLM-plausible answers reaching
|
|
2995
|
+
* accepts LLM-plausible answers reaching ATS prod); fall back to the first
|
|
2990
2996
|
* option only if every option is a decline/placeholder. Returns null when there
|
|
2991
2997
|
* is nothing selectable. Pure + exported so the policy is unit-testable; the LLM
|
|
2992
2998
|
* path ({@link judgeSelectOptionWithLLM}) is preferred when a client is present,
|
|
@@ -3133,7 +3139,7 @@ async function tryFillRequiredSelectsPrimitive(params) {
|
|
|
3133
3139
|
* question by directly checking the matching option in the DOM, bypassing
|
|
3134
3140
|
* Stagehand observe/act.
|
|
3135
3141
|
*
|
|
3136
|
-
* Why this exists (parallels `trySelectPrimitive`):
|
|
3142
|
+
* Why this exists (parallels `trySelectPrimitive`): the Angular/MUI wizard ATS renders
|
|
3137
3143
|
* multi-select screening questions ("In which settings have you worked…",
|
|
3138
3144
|
* certifications) as `c-MultiCheckboxInput` groups — a `<fieldset>`/`<legend>`
|
|
3139
3145
|
* question with `<input type=checkbox>` options, each tied to its text by a
|
|
@@ -3301,7 +3307,7 @@ async function tryCheckboxPrimitive(params) {
|
|
|
3301
3307
|
}
|
|
3302
3308
|
/**
|
|
3303
3309
|
* Build an XPath predicate that matches an `<input>` by its `id`, safe for any
|
|
3304
|
-
* id value. MUI
|
|
3310
|
+
* id value. MUI wizard radio ids are base64-ish (no double-quote), so a plain
|
|
3305
3311
|
* quoted literal suffices — but if an id ever contains a `"`, fall back to
|
|
3306
3312
|
* `concat(...)` so the XPath stays valid. Pure + exported for unit tests.
|
|
3307
3313
|
*/
|
|
@@ -3316,7 +3322,7 @@ function buildRadioIdXPath(id) {
|
|
|
3316
3322
|
* positionally. Pure (no DOM/LLM) so it is unit-testable — the crux of the
|
|
3317
3323
|
* unlabeled-radio disambiguation.
|
|
3318
3324
|
*
|
|
3319
|
-
* Why this exists:
|
|
3325
|
+
* Why this exists: the wizard ATS's Basic Info has multiple UNLABELED yes/no groups
|
|
3320
3326
|
* (visa-sponsorship, common-domicile), answered by consecutive flow steps. The
|
|
3321
3327
|
* old in-browser matcher treated an unlabeled group (`label===""`) as matching
|
|
3322
3328
|
* ANY question (`"".includes(q)`/`q.includes("")===0`), so two unlabeled "No"
|
|
@@ -3458,12 +3464,12 @@ function selectRadioGroupOption(params) {
|
|
|
3458
3464
|
* observe/act.
|
|
3459
3465
|
*
|
|
3460
3466
|
* Why this exists (parallels `trySelectPrimitive`/`tryCheckboxPrimitive`):
|
|
3461
|
-
*
|
|
3467
|
+
* The Angular/MUI wizard ATS renders eligibility/screening questions as MUI radio groups.
|
|
3462
3468
|
* Stagehand observe resolves the step to a wrapper `<div>`/`<span>`, not the
|
|
3463
3469
|
* `<input type=radio>`, so the cascade's `el.click()` fallback fires on the
|
|
3464
3470
|
* wrapper (or sets `.checked` without triggering React's `onChange`) and the
|
|
3465
3471
|
* controlled value never commits — the field stays `Mui-error` "required", Next
|
|
3466
|
-
* no-ops, and the wizard walls (measured on
|
|
3472
|
+
* no-ops, and the wizard walls (measured on the wizard ATS: the "Are you at least 18?"
|
|
3467
3473
|
* radio was the sole unfilled field blocking Basic Information → Step 2 of 10).
|
|
3468
3474
|
* This primitive finds the radio group by raw DOM, matches the requested option
|
|
3469
3475
|
* by its `<label for>` text, and commits via the React-safe native `checked`
|
|
@@ -4146,7 +4152,7 @@ const FORM_VALIDITY_PROBE_EXPR = `(() => {
|
|
|
4146
4152
|
const leafInvalid = INVALID_CLASS_RX.test(ctrlClass);
|
|
4147
4153
|
// A <select> whose currently-selected option is .disabled is the
|
|
4148
4154
|
// Angular "Please select..." placeholder state. Some custom-element
|
|
4149
|
-
// dropdowns (e.g.
|
|
4155
|
+
// dropdowns (e.g. an Angular apply SPA's app-dropdown) bind a truthy sentinel value
|
|
4150
4156
|
// like "0: null" to the disabled placeholder, so ctrl.value !== ""
|
|
4151
4157
|
// even though no real option is chosen. Without this branch the empty
|
|
4152
4158
|
// check below silently drops every such wrapper and the pre-submit
|
|
@@ -4366,7 +4372,7 @@ async function probeStepBeforeAttempts(params) {
|
|
|
4366
4372
|
// Focused observe under-returns on some controlled-component forms:
|
|
4367
4373
|
// Stagehand's instruction-scoped observe can resolve zero candidates for a
|
|
4368
4374
|
// declarative "Fill in the X field with 'Y'" step even when the field is
|
|
4369
|
-
// present and actionable (confirmed on
|
|
4375
|
+
// present and actionable (confirmed on the MUI/React wizard ATS's form — an
|
|
4370
4376
|
// unfocused observe enumerated every field and a direct act filled them).
|
|
4371
4377
|
// Treating focused-empty as hard "absent" skips the whole cascade and burns
|
|
4372
4378
|
// a global replan per step. Fall back to an UNFOCUSED observe as a
|
|
@@ -4409,12 +4415,12 @@ async function executeStepWithHealing(params) {
|
|
|
4409
4415
|
// multi-signal evidence with strict prompting instead.
|
|
4410
4416
|
//
|
|
4411
4417
|
// Gate accepts (isFinalStep || submitStep): flows whose canonical Submit
|
|
4412
|
-
// click lives mid-list (e.g.
|
|
4418
|
+
// click lives mid-list (e.g. one JSON-envelope ATS flow has Submit at step 55/328 with
|
|
4413
4419
|
// 273 post-submit verification steps) opt in via the per-step
|
|
4414
4420
|
// `submitStep: true` flag in the flow file. Without this opt-in, the pre-
|
|
4415
4421
|
// submit DOM probe gated solely on `isFinalStep` never fires on the real
|
|
4416
4422
|
// Submit, and unfilled required fields produce silent submit failures
|
|
4417
|
-
// (verified 2026-06-15 on
|
|
4423
|
+
// (verified 2026-06-15 on one measured tenant's telemetry). Site-agnostic: any flow
|
|
4418
4424
|
// whose submit is mid-list can mark its submit step explicitly.
|
|
4419
4425
|
const requireSubmitEndpoint = (isFinalStep || submitStep) && submitEndpointPattern !== null;
|
|
4420
4426
|
const attempts = [];
|
|
@@ -4439,7 +4445,7 @@ async function executeStepWithHealing(params) {
|
|
|
4439
4445
|
}
|
|
4440
4446
|
// When the step is a native-<select> dropdown selection, answer it directly
|
|
4441
4447
|
// in the DOM ahead of the cascade. Critical for MuiNativeSelect/tabindex=-1
|
|
4442
|
-
// dropdowns (
|
|
4448
|
+
// dropdowns (the MUI wizard ATS) that Stagehand observe can't surface — the
|
|
4443
4449
|
// cascade would otherwise skip them ("no candidates") and leave a required
|
|
4444
4450
|
// question unanswered. No-op (returns false → falls through) when the step
|
|
4445
4451
|
// isn't a single-dropdown select or no option matches.
|
|
@@ -4449,7 +4455,7 @@ async function executeStepWithHealing(params) {
|
|
|
4449
4455
|
return "completed";
|
|
4450
4456
|
}
|
|
4451
4457
|
// When the step is a "select 'X'" against a multi-select CHECKBOX group
|
|
4452
|
-
// (c-MultiCheckboxInput) rather than a <select> —
|
|
4458
|
+
// (c-MultiCheckboxInput) rather than a <select> — the wizard ATS renders some
|
|
4453
4459
|
// screening questions this way — answer it directly in the DOM. Runs AFTER
|
|
4454
4460
|
// trySelectPrimitive (which handles <select> and no-ops on checkbox-only
|
|
4455
4461
|
// pages). No-op (falls through) when there's no checkbox group or no match.
|
|
@@ -4462,7 +4468,7 @@ async function executeStepWithHealing(params) {
|
|
|
4462
4468
|
// the question '…'"), commit it directly in the DOM. Runs AFTER select/
|
|
4463
4469
|
// checkbox (which own their verbs) and BEFORE the cascade, so radios never
|
|
4464
4470
|
// reach the observe cascade's el.click() fallback that fails to commit MUI/
|
|
4465
|
-
// React controlled state (the
|
|
4471
|
+
// React controlled state (the wizard ATS's Basic-Info Step-2 wall). No-op (falls
|
|
4466
4472
|
// through) when there's no radio group or no confident option match.
|
|
4467
4473
|
if (await tryRadioPrimitive({ page, instruction: step, logger, anthropic, captureFn })) {
|
|
4468
4474
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} resolved by radio primitive`);
|
|
@@ -4661,7 +4667,7 @@ async function executeStepWithHealing(params) {
|
|
|
4661
4667
|
// techniques faster.
|
|
4662
4668
|
if (attempt > 1) {
|
|
4663
4669
|
const wouldBeTechnique = attempt === 2
|
|
4664
|
-
? phantomClickAfterAttempt1
|
|
4670
|
+
? phantomClickAfterAttempt1 && (isFinalStep || submitStep)
|
|
4665
4671
|
? "deep-submit-locator"
|
|
4666
4672
|
: "observe-act"
|
|
4667
4673
|
: attempt === 3
|
|
@@ -4678,6 +4684,7 @@ async function executeStepWithHealing(params) {
|
|
|
4678
4684
|
})),
|
|
4679
4685
|
advanceUnmovedAfterAttempt1,
|
|
4680
4686
|
phantomClickAfterAttempt1,
|
|
4687
|
+
submitShapedStep: isFinalStep || submitStep,
|
|
4681
4688
|
});
|
|
4682
4689
|
if (decision.skip) {
|
|
4683
4690
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt} (${wouldBeTechnique}) skipped: ${decision.reason}`);
|
|
@@ -4748,7 +4755,7 @@ async function executeStepWithHealing(params) {
|
|
|
4748
4755
|
}
|
|
4749
4756
|
}
|
|
4750
4757
|
}
|
|
4751
|
-
else if (attempt === 2 && phantomClickAfterAttempt1) {
|
|
4758
|
+
else if (attempt === 2 && phantomClickAfterAttempt1 && (isFinalStep || submitStep)) {
|
|
4752
4759
|
// Deep submit-control locator: attempt 1 phantom-clicked (Stagehand
|
|
4753
4760
|
// reported success but pre/post showed zero effect), so the target is
|
|
4754
4761
|
// almost certainly unreachable via document.querySelectorAll — most
|
|
@@ -4992,7 +4999,7 @@ async function executeStepWithHealing(params) {
|
|
|
4992
4999
|
// browser's default label-click action does NOT toggle .checked for
|
|
4993
5000
|
// hidden inputs (Bootstrap, Tailwind, plain HTML labels-control,
|
|
4994
5001
|
// Angular Material's mat-radio-button, PrimeNG's p-radioButton, and
|
|
4995
|
-
// custom Angular components like
|
|
5002
|
+
// custom Angular components like an apply SPA's app-radio-button all fit
|
|
4996
5003
|
// this shape). The framework's FormControl/state listens on the
|
|
4997
5004
|
// input's `change` event, which never fires when the visible click
|
|
4998
5005
|
// never reaches a clickable input — so the form stays invalid even
|
|
@@ -5221,7 +5228,7 @@ async function executeStepWithHealing(params) {
|
|
|
5221
5228
|
? await verifyDomEffect(page, resolvedAction)
|
|
5222
5229
|
: false;
|
|
5223
5230
|
// Interior-advance transition gate (opt-in). On SPAs where a page advance
|
|
5224
|
-
// and a mere field-edit share one endpoint URL (
|
|
5231
|
+
// and a mere field-edit share one endpoint URL (the wizard ATS's `/gq`:
|
|
5225
5232
|
// TransitionWorklet vs EditQuestionItem — identical URLs, only the body
|
|
5226
5233
|
// differs), a `networkFired` signal on an interior "Next" can be a
|
|
5227
5234
|
// non-advancing POST. When the flow configures `advanceTransitionBodyPattern`
|
|
@@ -5257,7 +5264,7 @@ async function executeStepWithHealing(params) {
|
|
|
5257
5264
|
// instruction) whose ONLY signal is that DOM state change, do NOT count it
|
|
5258
5265
|
// verified: an advance requires a REAL transition (a `type=next` per
|
|
5259
5266
|
// `networkIsRealAdvance`, or a URL change). Keyed on `networkIsRealAdvance`,
|
|
5260
|
-
// NOT `!networkFired` — a non-advancing autosave POST (
|
|
5267
|
+
// NOT `!networkFired` — a non-advancing autosave POST (the wizard ATS's
|
|
5261
5268
|
// `WorkletPayload`) fires network=true while the wizard doesn't move, and the
|
|
5262
5269
|
// old `!networkFired` guard let a rephrase ride that autosave + a DOM reflow
|
|
5263
5270
|
// to a FALSE advance, desyncing the flow from the wizard. Field-answer steps
|
|
@@ -5281,7 +5288,7 @@ async function executeStepWithHealing(params) {
|
|
|
5281
5288
|
// submitEndpointPattern regex with a Haiku 4.5 LLM judgment over
|
|
5282
5289
|
// multi-signal evidence (network captures, page URL/title, DOM
|
|
5283
5290
|
// submitted-state probe, site-supplied criteria). The regex
|
|
5284
|
-
// mislabeled successful submits as failures whenever
|
|
5291
|
+
// mislabeled successful submits as failures whenever an ATS used a
|
|
5285
5292
|
// POST URL outside the regex's narrow expectation — empirically
|
|
5286
5293
|
// observed in last night's sweep and validated 2026-06-11 against
|
|
5287
5294
|
// claude-haiku-4-5-20251001 with the SUBMIT_VERDICT_SCHEMA.
|
|
@@ -5480,7 +5487,7 @@ async function executeStepWithHealing(params) {
|
|
|
5480
5487
|
// change AND the page still has required-invalid controls (MUI-aware
|
|
5481
5488
|
// via countNgInvalidContainers), treat the DOM-only signal as NOT
|
|
5482
5489
|
// verifying so the cascade routes to the fill-invalid-fields replan.
|
|
5483
|
-
// Confirmed no-op signature on
|
|
5490
|
+
// Confirmed no-op signature on the wizard ATS's COMPENSATION page (network=false
|
|
5484
5491
|
// url=false htmlDelta>0 textChanged) mis-scored verified=true(dom).
|
|
5485
5492
|
const clickWasDomOnly = probeResult.kind === "click" && !retryNetworkFired && !retryUrlChanged;
|
|
5486
5493
|
const clickBlockedByInvalid = clickWasDomOnly && (await countNgInvalidContainers(page).catch(() => 0)) > 0;
|
|
@@ -5494,7 +5501,7 @@ async function executeStepWithHealing(params) {
|
|
|
5494
5501
|
// `advanceTransitionBodyPattern`. Without this, a non-advancing POST
|
|
5495
5502
|
// (retryNetworkFired=true) both satisfied retryVerified AND disarmed the
|
|
5496
5503
|
// old !retryNetworkFired-gated veto, so the fallback rode past a
|
|
5497
|
-
// validation-blocked Next while the page stayed put (
|
|
5504
|
+
// validation-blocked Next while the page stayed put (the wizard ATS's Basic Info
|
|
5498
5505
|
// stage1d). Only arms when the site opted into the pattern; field-answer
|
|
5499
5506
|
// (non-advance) steps keep their checkboxStateVerified/DOM path.
|
|
5500
5507
|
// Poll for the real TransitionWorklet(type="next") like the primary
|
|
@@ -5625,7 +5632,7 @@ async function executeStepWithHealing(params) {
|
|
|
5625
5632
|
// Why log first-try wins explicitly: prior to this change, attempt-1
|
|
5626
5633
|
// successes were silent — only attempts 2+ emitted "healed on attempt
|
|
5627
5634
|
// N" lines. That under-reporting caused a 2026-06-15 telemetry-vs-log
|
|
5628
|
-
// contradiction where
|
|
5635
|
+
// contradiction where one measured tenant's run looked like a "cascade
|
|
5629
5636
|
// collapse" (log showed 2 heals) but telemetry calls.ndjson showed
|
|
5630
5637
|
// 32 successful Stagehand acts. Surfacing attempt-1 wins lets the
|
|
5631
5638
|
// log match telemetry and prevents the same false alarm.
|
|
@@ -5659,7 +5666,7 @@ async function executeStepWithHealing(params) {
|
|
|
5659
5666
|
// outcome="impossible" instead of looping on the same plan. Silent
|
|
5660
5667
|
// no-op on sites where the DOM pattern doesn't match — the existing
|
|
5661
5668
|
// failure-reason and replan/cycle-detection paths still fire.
|
|
5662
|
-
// Empirically grounded: 22 of 22
|
|
5669
|
+
// Empirically grounded: 22 of 22 JSON-envelope ATS Continue/Submit step-failure
|
|
5663
5670
|
// dumps in a 2026-06-10 survey had the paired touched+dirty + visible
|
|
5664
5671
|
// error text pattern with 3 distinct rejection messages.
|
|
5665
5672
|
if (record.resolvedMethod === "click" && (isFinalStep || submitStep)) {
|
|
@@ -5707,7 +5714,10 @@ async function executeStepWithHealing(params) {
|
|
|
5707
5714
|
phantomClickAfterAttempt1 = record.phantomClickVerdict === "phantom";
|
|
5708
5715
|
if (phantomClickAfterAttempt1) {
|
|
5709
5716
|
const suppressedCount = getSuppressedAisdkElementIdErrorCount?.();
|
|
5710
|
-
|
|
5717
|
+
const escalationTarget = isFinalStep || submitStep
|
|
5718
|
+
? "escalating attempt 2 to deep-submit-locator"
|
|
5719
|
+
: "non-submit step — leaving the normal structured-click/observe-act-exclude ladder intact";
|
|
5720
|
+
logger.warn(`${formatStepPrefix(stepIndex, totalSteps)} phantom click detected on attempt 1 (${record.technique}): reported success with no network/url/dom change${suppressedCount !== undefined ? `; ${suppressedCount} AISDK elementId errors suppressed this session (corroborating, not causal)` : ""} — ${escalationTarget}`);
|
|
5711
5721
|
}
|
|
5712
5722
|
const postAttemptInvalidCount = await countNgInvalidContainers(page);
|
|
5713
5723
|
const earlyExit = isSubmitRevealedInvalid({
|
|
@@ -5730,7 +5740,7 @@ async function executeStepWithHealing(params) {
|
|
|
5730
5740
|
// Telemetry-driven early-exit for interior ADVANCE steps: if the Next
|
|
5731
5741
|
// click fired and hit the network but produced no real forward
|
|
5732
5742
|
// transition, attempts 2-N only re-fire the autosave / bounce the wizard
|
|
5733
|
-
// back (the measured
|
|
5743
|
+
// back (the measured wizard-ATS next→back oscillation). Route to replan — which
|
|
5734
5744
|
// can reorder a later step forward — instead of burning the cascade.
|
|
5735
5745
|
const advanceStalled = isAdvanceStalled({
|
|
5736
5746
|
isAdvance: isAdvanceStep(step),
|