@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.
Files changed (77) hide show
  1. package/README.md +23 -18
  2. package/dist/api/errors.d.ts +3 -3
  3. package/dist/api/errors.d.ts.map +1 -1
  4. package/dist/api/errors.js +4 -4
  5. package/dist/api/errors.js.map +1 -1
  6. package/dist/api/helpers/reply.d.ts +1 -1
  7. package/dist/api/helpers/reply.js +1 -1
  8. package/dist/cache/keyed-ttl-cache.d.ts +4 -4
  9. package/dist/cache/keyed-ttl-cache.js +2 -2
  10. package/dist/lib/applicant-payload.d.ts +4 -4
  11. package/dist/lib/applicant-payload.js +4 -4
  12. package/dist/lib/application-address.d.ts +3 -3
  13. package/dist/lib/application-address.js +5 -5
  14. package/dist/lib/application-address.js.map +1 -1
  15. package/dist/lib/application-resume.d.ts +4 -4
  16. package/dist/lib/application-resume.js +4 -4
  17. package/dist/lib/dispatch-metrics.js +1 -1
  18. package/dist/lib/llm/judges/verify-submit.d.ts +1 -1
  19. package/dist/lib/llm/judges/verify-submit.js +1 -1
  20. package/dist/lib/llm/schemas.js +3 -3
  21. package/dist/lib/phone.d.ts +1 -1
  22. package/dist/lib/phone.js +1 -1
  23. package/dist/lib/telemetry/call-types.d.ts +1 -1
  24. package/dist/lib/telemetry/call-types.js +1 -1
  25. package/dist/lib/tracking-click.d.ts +5 -3
  26. package/dist/lib/tracking-click.d.ts.map +1 -1
  27. package/dist/lib/tracking-click.js +5 -3
  28. package/dist/lib/tracking-click.js.map +1 -1
  29. package/dist/lib/us-states.d.ts +1 -1
  30. package/dist/lib/us-states.js +1 -1
  31. package/dist/plugins/loader.js +2 -2
  32. package/dist/scraper/cookie-jar.d.ts +2 -2
  33. package/dist/scraper/cookie-jar.js +2 -2
  34. package/dist/scraper/errors.d.ts +1 -12
  35. package/dist/scraper/errors.d.ts.map +1 -1
  36. package/dist/scraper/errors.js +2 -16
  37. package/dist/scraper/errors.js.map +1 -1
  38. package/dist/scraper/flow-runner.d.ts +43 -27
  39. package/dist/scraper/flow-runner.d.ts.map +1 -1
  40. package/dist/scraper/flow-runner.js +70 -60
  41. package/dist/scraper/flow-runner.js.map +1 -1
  42. package/dist/scraper/http-client.d.ts +11 -1
  43. package/dist/scraper/http-client.d.ts.map +1 -1
  44. package/dist/scraper/http-client.js +90 -3
  45. package/dist/scraper/http-client.js.map +1 -1
  46. package/dist/scraper/raw-fetch.d.ts +2 -2
  47. package/dist/scraper/raw-fetch.js +1 -1
  48. package/dist/scraper/require-response-field.d.ts +1 -1
  49. package/dist/scraper/require-response-field.js +1 -1
  50. package/dist/scraper/session-warmup.d.ts +1 -1
  51. package/dist/scraper/session-warmup.js +1 -1
  52. package/dist/scripts/migrate-telemetry-dir-names.d.ts +2 -2
  53. package/dist/scripts/migrate-telemetry-dir-names.d.ts.map +1 -1
  54. package/dist/scripts/migrate-telemetry-dir-names.js +1 -1
  55. package/dist/scripts/recon-browser.d.ts +2 -2
  56. package/dist/scripts/recon-browser.js +14 -14
  57. package/dist/scripts/recon-browser.js.map +1 -1
  58. package/dist/scripts/recon-generate.d.ts +24 -0
  59. package/dist/scripts/recon-generate.d.ts.map +1 -1
  60. package/dist/scripts/recon-generate.js +23 -7
  61. package/dist/scripts/recon-generate.js.map +1 -1
  62. package/dist/scripts/recon-replay-jobs.d.ts +9 -4
  63. package/dist/scripts/recon-replay-jobs.d.ts.map +1 -1
  64. package/dist/scripts/recon-replay-jobs.js +15 -8
  65. package/dist/scripts/recon-replay-jobs.js.map +1 -1
  66. package/dist/scripts/recon-shared.d.ts +10 -0
  67. package/dist/scripts/recon-shared.d.ts.map +1 -1
  68. package/dist/scripts/recon-shared.js +25 -0
  69. package/dist/scripts/recon-shared.js.map +1 -1
  70. package/dist/site-plugin.d.ts +4 -4
  71. package/dist/testing/persona-fixture.d.ts +4 -4
  72. package/dist/testing/persona-fixture.d.ts.map +1 -1
  73. package/dist/testing/persona-fixture.js +4 -4
  74. package/dist/testing/persona-fixture.js.map +1 -1
  75. package/dist/types/dispatch-metrics.d.ts +1 -1
  76. package/dist/types/dispatch-metrics.js +1 -1
  77. 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. HCA/Talemetry's "Next" click fires a fast `WorkletPayload` autosave
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: HCA's Talemetry
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: AppCast returns
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 AppCast: 4/6 historical
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 HCA cascade). Scanning disk by filename index is eviction-proof:
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 (Talemetry's `GET .../init-apply?...&application_canceled=true`),
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. Talemetry `/gq`: a real advance is a `TransitionWorklet` mutation, a
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 Talemetry a `back` bounce is ALSO a `TransitionWorklet`
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 (Talemetry `/gq`: TransitionWorklet vs EditQuestionItem — same URL,
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 (Talemetry's `WorkletPayload` autosave): keying the veto on "no network
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 HCA runs as the
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 appcast Encompass run (every rephrase converged on
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 AppCast's
1437
- * applyboard SPA: ng-invalid first appears at byte ~15,500 after a header
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 AppCast-specific.
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 AppCast applyboard SPA, where the form starts
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
- * Today's Encompass-Fitchburg smoke (run 1781478440322) showed E1's prompt
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. AppCast (and most GA4-instrumented SPAs) emit
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 AppCast, Lever, Greenhouse,
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 AppCast Encompass-Fitchburg: /integrated_apply 422 body is
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. HCA/Talemetry mounts the MUI/react-dropzone widget ~5s after the
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). Talemetry posts the resume to an `attachment_upload_url` that
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 Talemetry. Rejects negative
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
- // Talemetry/MUI and other react-dropzone widgets can render NO <input type=file>
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 AppCast Encompass-Fitchburg today: setInputFiles
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 (Talemetry/MUI `ResumeUpload`, react-dropzone). Ordered cheapest-
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: HCA/Talemetry render dropdowns as `MuiNativeSelect` native `<select>`
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 HCA/Talemetry's MUI radio markup resolves the step
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 (Talemetry) frequently render the target widget a beat AFTER the flow
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 (HCA fires a fast `WorkletPayload` autosave first). A
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`): Talemetry/MUI dropdowns are
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 HCA
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 HCA prod); fall back to the first
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`): HCA/Talemetry render
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/Talemetry radio ids are base64-ish (no double-quote), so a plain
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: HCA/Talemetry Basic Info has multiple UNLABELED yes/no groups
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
- * HCA/Talemetry render eligibility/screening questions as MUI radio groups.
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 HCA: the "Are you at least 18?"
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. AppCast's app-dropdown) bind a truthy sentinel value
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 HCA's MUI/React Talemetry form — an
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. AppCast's flow has Submit at step 55/328 with
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 UVA Verona telemetry). Site-agnostic: any flow
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 (HCA/Talemetry) that Stagehand observe can't surface — the
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> — Talemetry renders some
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 HCA Basic-Info Step-2 wall). No-op (falls
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 AppCast's app-radio-button all fit
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 (Talemetry `/gq`:
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 (Talemetry
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 AppCast used a
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 HCA COMPENSATION (network=false
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 (HCA Basic Info
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 UVA Verona's run looked like a "cascade
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 AppCast Continue/Submit step-failure
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
- 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)` : ""} — escalating attempt 2 to deep-submit-locator`);
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 HCA next→back oscillation). Route to replan — which
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),