@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: /(?:this |the )?(?:job|position|posting|role|opportunity).{0,120}(?:has )?(?:expired|closed|been filled|is no longer available|is no longer accepting applications)|(?:expired|closed|no longer available|no longer accepting applications).{0,120}(?:job|position|posting|role|opportunity)/.test(text)
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
- if (/(?:job|position|posting|role|opportunity).{0,120}(?:has )?(?:expired|closed|been filled|is no longer available|is no longer accepting applications)|(?:expired|closed|no longer available|no longer accepting applications).{0,120}(?:job|position|posting|role|opportunity)/i.test(page)) {
222
- return { state: "unavailable", jobUrl: parsed.toString(), reason: /expired/i.test(page) ? "expired" : "unavailable" };
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
- const last = [...this.visited].pop();
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.121",
3
+ "version": "0.4.123",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",