@proagentstore/cli 0.4.121 → 0.4.123
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.
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* The trace carries classes, domains, decisions and artifact handles — never a typed value.
|
|
26
26
|
*/
|
|
27
27
|
import { classifyApplyClick } from "./contract.js";
|
|
28
|
+
import { LISTING_EXPIRED_RE, LISTING_UNAVAILABLE_RE } from "./unavailable.js";
|
|
28
29
|
import { CONFIRMATION_INTERVAL_SECONDS, CONFIRMATION_LOOKS, CONFIRMATION_TEXT_RE, confirmationMarker, observe, } from "./confirmation.js";
|
|
29
30
|
import { ELEMENT_PROBE_FN } from "../commit-guard.js";
|
|
30
31
|
import { normalize } from "../local-artifact/engine.js";
|
|
@@ -71,9 +72,13 @@ const SAFE_KEYS = /^(Tab|Shift\+Tab|Arrow(Up|Down|Left|Right)|Escape|Home|End|Pa
|
|
|
71
72
|
* The success vocabulary is INTERPOLATED from `confirmation.ts` rather than written out here
|
|
72
73
|
* (#994): a second copy of that regex is how the "sent" family — the exact word SEEK uses — was
|
|
73
74
|
* missing from the detector while being present in nobody's tests, through three live runs.
|
|
75
|
+
* The listing-gone vocabulary is interpolated from `unavailable.ts` for the same reason (#1022): its
|
|
76
|
+
* copy here lacked SEEK's "no longer advertised" through three live runs.
|
|
74
77
|
*/
|
|
75
|
-
const INSPECT_PAGE = `() => {
|
|
78
|
+
export const INSPECT_PAGE = `() => {
|
|
76
79
|
const CONFIRMATION_TEXT_SOURCE = ${JSON.stringify(CONFIRMATION_TEXT_RE.source)};
|
|
80
|
+
const UNAVAILABLE_TEXT_SOURCE = ${JSON.stringify(LISTING_UNAVAILABLE_RE.source)};
|
|
81
|
+
const EXPIRED_TEXT_SOURCE = ${JSON.stringify(LISTING_EXPIRED_RE.source)};
|
|
77
82
|
const text = (document.body ? document.body.innerText : "").slice(0, 20000).toLowerCase();
|
|
78
83
|
const has = (s) => !!document.querySelector(s);
|
|
79
84
|
return {
|
|
@@ -85,9 +90,7 @@ const INSPECT_PAGE = `() => {
|
|
|
85
90
|
duplicate: /you('ve| have) already applied|already applied (for|to) this|application (already exists|has already been (submitted|received))|duplicate application/.test(text),
|
|
86
91
|
antiBot: /unusual traffic|access (is )?denied|request (has been )?blocked|bot detected|checking (if the site connection is secure|your browser)/.test(text),
|
|
87
92
|
confirmed: new RegExp(CONFIRMATION_TEXT_SOURCE).test(text),
|
|
88
|
-
unavailable:
|
|
89
|
-
? (/expired/.test(text) ? "expired" : "unavailable")
|
|
90
|
-
: null,
|
|
93
|
+
unavailable: new RegExp(UNAVAILABLE_TEXT_SOURCE, "i").test(text) ? (new RegExp(EXPIRED_TEXT_SOURCE, "i").test(text) ? "expired" : "unavailable") : null,
|
|
91
94
|
};
|
|
92
95
|
}`;
|
|
93
96
|
const text = (t, isError = false) => ({ content: [{ type: "text", text: t }], ...(isError ? { isError: true } : {}) });
|
|
@@ -35,6 +35,7 @@ import { finalText, missingLogin, signInHelp } from "../local-browser/engine.js"
|
|
|
35
35
|
import { resolveWorkspacePath } from "../local-browser/runtime.js";
|
|
36
36
|
import { ApplyBridge } from "./bridge.js";
|
|
37
37
|
import { confirmationSignals } from "./confirmation.js";
|
|
38
|
+
import { listingUnavailableReason } from "./unavailable.js";
|
|
38
39
|
import { LOCAL_APPLY_AUTH_MODES, LOCAL_APPLY_CAPS, LOCAL_APPLY_ENGINES, LOCAL_APPLY_MODES, LOCAL_APPLY_TASK_TYPE, parseLocalApplyResult, } from "./contract.js";
|
|
39
40
|
import { applyPrompt, buildApplyEngineSpec, observedApplyAuth } from "./engine.js";
|
|
40
41
|
import { LocalApplyHandoffRuntime } from "./runtime-handoff.js";
|
|
@@ -218,9 +219,9 @@ export class LocalApplyRuntime {
|
|
|
218
219
|
if (snapshot.isError || /captcha|verify (?:you(?:'| a)re|that you are) human|access denied|unusual traffic|checking (?:if )?your browser/i.test(page)) {
|
|
219
220
|
return { state: "unverifiable", jobUrl: parsed.toString(), reason: "access_blocked" };
|
|
220
221
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
const gone = listingUnavailableReason(page);
|
|
223
|
+
if (gone)
|
|
224
|
+
return { state: "unavailable", jobUrl: parsed.toString(), reason: gone };
|
|
224
225
|
// An accessible Apply control is an active path, unlike a generic careers/search URL.
|
|
225
226
|
if (/(?:button|link)\s+"[^"]{0,80}\bapply(?: now| for| here)?\b|\bapply (?:now|for (?:this )?(?:job|role|position))\b|\bstart application\b/i.test(page)) {
|
|
226
227
|
return { state: "live", jobUrl: parsed.toString(), applyUrl: parsed.toString(), evidence: "apply_control_present" };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does the page say the job listing is gone? (#1022)
|
|
3
|
+
*
|
|
4
|
+
* ── The live failure this module exists for
|
|
5
|
+
*
|
|
6
|
+
* Every Runner run for application `cc90cd13…` (runs `26d8f1c1…`, `28c8f18f…`, `9c87cd10…`, 2026-10-09
|
|
7
|
+
* and 2026-10-13) opened SEEK's listing, read "This job is no longer advertised — Jobs remain on SEEK
|
|
8
|
+
* for 30 days, unless the advertiser removes them sooner", and called `report_job_unavailable` — which
|
|
9
|
+
* the runner refused every time ("could not verify an expired or unavailable listing notice"). So each
|
|
10
|
+
* run ended `awaiting_review` with nothing to review, and a retry repeated it. The vocabulary knew
|
|
11
|
+
* "expired", "closed", "been filled", "no longer available" and "no longer accepting applications" —
|
|
12
|
+
* not "no longer advertised", SEEK's own word.
|
|
13
|
+
*
|
|
14
|
+
* It was also written TWICE — in the in-page probe (`bridge.ts` `INSPECT_PAGE`) and in the reconciliation
|
|
15
|
+
* check (`runtime.ts`) — and the probe's copy was never tested on real text, because the bridge's tests
|
|
16
|
+
* fake the probe's RESULT. That is the gap #994 closed for the confirmation wording; this closes it here:
|
|
17
|
+
* one vocabulary, interpolated into the probe, tested against the wording the boards publish.
|
|
18
|
+
*
|
|
19
|
+
* A notice, never an inference: the page must SAY the listing is gone. A missing Apply button is not
|
|
20
|
+
* this — that stays a review state.
|
|
21
|
+
*/
|
|
22
|
+
const NOUN = "(?:job|position|posting|role|opportunity|listing|advert|job ad)";
|
|
23
|
+
const GONE = "(?:has )?(?:expired|closed|been filled|been removed|is no longer (?:available|advertised|listed|accepting applications)|no longer (?:available|advertised|listed))";
|
|
24
|
+
const GONE_FIRST = "(?:expired|closed|no longer available|no longer advertised|no longer listed|no longer accepting applications)";
|
|
25
|
+
/** Specific enough to stand alone: LinkedIn's closed-listing badge names no job noun near it. */
|
|
26
|
+
const ALONE = "no longer accepting applications";
|
|
27
|
+
/**
|
|
28
|
+
* The notice, in either order ("this job is no longer advertised" / "no longer accepting applications
|
|
29
|
+
* for this role"), within 120 characters, or a phrase that needs no noun. Case-insensitive; the in-page
|
|
30
|
+
* probe compiles `.source` with "i".
|
|
31
|
+
*/
|
|
32
|
+
export const LISTING_UNAVAILABLE_RE = new RegExp(`${NOUN}.{0,120}${GONE}|${GONE_FIRST}.{0,120}${NOUN}|${ALONE}`, "i");
|
|
33
|
+
/** Which of the two reasons a notice is: "expired" when the page says so, otherwise "unavailable". */
|
|
34
|
+
export const LISTING_EXPIRED_RE = /expired/i;
|
|
35
|
+
/** The listing's own notice, read from page text — or null when the page says nothing of the kind. */
|
|
36
|
+
export function listingUnavailableReason(text) {
|
|
37
|
+
if (!LISTING_UNAVAILABLE_RE.test(text))
|
|
38
|
+
return null;
|
|
39
|
+
return LISTING_EXPIRED_RE.test(text) ? "expired" : "unavailable";
|
|
40
|
+
}
|
|
@@ -134,6 +134,8 @@ export class BrowserBridge {
|
|
|
134
134
|
pages = 0;
|
|
135
135
|
lastSnapshot = "";
|
|
136
136
|
visited = new Set();
|
|
137
|
+
/** The host of the page the browser is on now — not the last NEW host: a Set keeps first-insertion order. */
|
|
138
|
+
here = null;
|
|
137
139
|
findings = [];
|
|
138
140
|
sourceFailures = [];
|
|
139
141
|
summary = null;
|
|
@@ -186,8 +188,7 @@ export class BrowserBridge {
|
|
|
186
188
|
return { content: [{ type: "text", text: textOf(res) }], ...(res.isError ? { isError: true } : {}) };
|
|
187
189
|
}
|
|
188
190
|
currentHost() {
|
|
189
|
-
|
|
190
|
-
return last ? { domain: last } : {};
|
|
191
|
+
return this.here ? { domain: this.here } : {};
|
|
191
192
|
}
|
|
192
193
|
/** Refuse, on the trace — naming the site and the owner's decision behind it when there is one. */
|
|
193
194
|
refuse(tool, reason, host) {
|
|
@@ -307,6 +308,7 @@ export class BrowserBridge {
|
|
|
307
308
|
if (!host)
|
|
308
309
|
return;
|
|
309
310
|
this.visited.add(host);
|
|
311
|
+
this.here = host;
|
|
310
312
|
this.host.emit({ type: "browser.navigated", url: state.url, domain: host, detail: redactDetail({ title: state.title }) });
|
|
311
313
|
}
|
|
312
314
|
async inspect() {
|