job-forge 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/main.mdc +20 -0
- package/AGENTS.md +20 -0
- package/CLAUDE.md +20 -0
- package/iso/instructions.md +20 -0
- package/merge-tracker.mjs +54 -7
- package/modes/apply.md +17 -0
- package/modes/scan.md +4 -4
- package/package.json +4 -3
package/.cursor/rules/main.mdc
CHANGED
|
@@ -307,6 +307,7 @@ When a form says "enter the code we sent to your email", you MUST retrieve the c
|
|
|
307
307
|
| Lever | `from:lever newer_than:10m` |
|
|
308
308
|
| Ashby | `from:ashby newer_than:10m` |
|
|
309
309
|
| SmartRecruiters | `from:smartrecruiters newer_than:10m` |
|
|
310
|
+
| Toast (via ClinchTalent) | `from:toast.mail.clinchtalent.com newer_than:15m` OR `subject:"verify your login at Toast" newer_than:15m` |
|
|
310
311
|
| Aggregator redirect (WeWorkRemotely / RemoteOK) | Detect the underlying ATS from the post-redirect URL, then use that row's sender query |
|
|
311
312
|
| Unknown | `newer_than:10m subject:(verify OR code OR confirm)` |
|
|
312
313
|
|
|
@@ -314,6 +315,7 @@ When a form says "enter the code we sent to your email", you MUST retrieve the c
|
|
|
314
315
|
- ALWAYS check Gmail before reporting a submission as failed.
|
|
315
316
|
- If "submit button did nothing", it usually means an OTP step appeared. Check Gmail.
|
|
316
317
|
- If no email after 10 seconds, retry `gmail_list_messages` once more with `newer_than:5m`.
|
|
318
|
+
- **Some Greenhouse tenants route OTP through third-party verification (Toast uses ClinchTalent).** If `from:greenhouse` returns empty after a Greenhouse submit, check the tenant-specific sender row above. Confirmed 2026-04-19: Toast Principal SWE #807 and Toast Senior FE #808.
|
|
317
319
|
|
|
318
320
|
---
|
|
319
321
|
|
|
@@ -369,6 +371,24 @@ These blocks come from two distinct root causes and require different responses:
|
|
|
369
371
|
|
|
370
372
|
**Rule — do NOT loop retrying a class B block.** One retry with `imeFriendly: true` is the correct test for class A. If the same spam message fires after a clean `imeFriendly` refill, stop, mark Failed, move on. Repeated retries waste subagent time and do not change the outcome.
|
|
371
373
|
|
|
374
|
+
**Known-block Ashby tenants (2026-04-19 empirical observations).** These tenants fired class B on every attempted submit from a headless datacenter-IP proxy. Orchestrators planning apply dispatches should assume these tenants will Fail in headless — prioritize other portals, or skip same-tenant siblings after a confirmed class B to avoid burning subagent slots:
|
|
375
|
+
|
|
376
|
+
- Vellum, Linear, Vanta, River Financial, Higharc, Trace Labs, Solace Health, Unstructured, ClickUp, Zapier, Deepgram, Ramp, WorkOS
|
|
377
|
+
|
|
378
|
+
**Known class-A-compatible Ashby tenants (same observations).** These tenants accepted headless submits cleanly, often with `imeFriendly: true` making the difference on the text-field subset:
|
|
379
|
+
|
|
380
|
+
- Supabase, LangChain, Poolside, Runway Financial
|
|
381
|
+
|
|
382
|
+
The pattern is tenant configuration, not role or company size. Lists drift as tenants tune their anti-bot — treat as probabilistic priors, not hard rules.
|
|
383
|
+
|
|
384
|
+
### Greenhouse Bot-Detection Honeypots
|
|
385
|
+
|
|
386
|
+
Some Greenhouse tenants (Grafana Labs confirmed, 2026-04-19) inject a honeypot-style single-pick question on the application form, rendered as a listbox labeled something like "Which of the following best describes you?" with options resembling "I am a human being / I am a bot / I am a robot".
|
|
387
|
+
|
|
388
|
+
**Rule:** pick the "I am a human being" option (or whichever option is the obvious human-authentic choice). Bots that pick other options are filtered before submit. This is NOT a validation check — the field will always read back clean — but the submit will be silently discarded if the wrong option is selected.
|
|
389
|
+
|
|
390
|
+
If the honeypot question is absent, skip. If present, always pick the human option.
|
|
391
|
+
|
|
372
392
|
### Nested Scroll Containers (Greenhouse / Ashby)
|
|
373
393
|
|
|
374
394
|
The major ATS portals (Greenhouse, Workday, Lever, Ashby) use nested scrollable regions. A field's `visibleBounds` may show it as off-screen even when it is actually visible within a child scroll container. Geometra's `scroll_to` operates on the outermost page scroll, so it cannot reach fields in inner scroll regions.
|
package/AGENTS.md
CHANGED
|
@@ -302,6 +302,7 @@ When a form says "enter the code we sent to your email", you MUST retrieve the c
|
|
|
302
302
|
| Lever | `from:lever newer_than:10m` |
|
|
303
303
|
| Ashby | `from:ashby newer_than:10m` |
|
|
304
304
|
| SmartRecruiters | `from:smartrecruiters newer_than:10m` |
|
|
305
|
+
| Toast (via ClinchTalent) | `from:toast.mail.clinchtalent.com newer_than:15m` OR `subject:"verify your login at Toast" newer_than:15m` |
|
|
305
306
|
| Aggregator redirect (WeWorkRemotely / RemoteOK) | Detect the underlying ATS from the post-redirect URL, then use that row's sender query |
|
|
306
307
|
| Unknown | `newer_than:10m subject:(verify OR code OR confirm)` |
|
|
307
308
|
|
|
@@ -309,6 +310,7 @@ When a form says "enter the code we sent to your email", you MUST retrieve the c
|
|
|
309
310
|
- ALWAYS check Gmail before reporting a submission as failed.
|
|
310
311
|
- If "submit button did nothing", it usually means an OTP step appeared. Check Gmail.
|
|
311
312
|
- If no email after 10 seconds, retry `gmail_list_messages` once more with `newer_than:5m`.
|
|
313
|
+
- **Some Greenhouse tenants route OTP through third-party verification (Toast uses ClinchTalent).** If `from:greenhouse` returns empty after a Greenhouse submit, check the tenant-specific sender row above. Confirmed 2026-04-19: Toast Principal SWE #807 and Toast Senior FE #808.
|
|
312
314
|
|
|
313
315
|
---
|
|
314
316
|
|
|
@@ -364,6 +366,24 @@ These blocks come from two distinct root causes and require different responses:
|
|
|
364
366
|
|
|
365
367
|
**Rule — do NOT loop retrying a class B block.** One retry with `imeFriendly: true` is the correct test for class A. If the same spam message fires after a clean `imeFriendly` refill, stop, mark Failed, move on. Repeated retries waste subagent time and do not change the outcome.
|
|
366
368
|
|
|
369
|
+
**Known-block Ashby tenants (2026-04-19 empirical observations).** These tenants fired class B on every attempted submit from a headless datacenter-IP proxy. Orchestrators planning apply dispatches should assume these tenants will Fail in headless — prioritize other portals, or skip same-tenant siblings after a confirmed class B to avoid burning subagent slots:
|
|
370
|
+
|
|
371
|
+
- Vellum, Linear, Vanta, River Financial, Higharc, Trace Labs, Solace Health, Unstructured, ClickUp, Zapier, Deepgram, Ramp, WorkOS
|
|
372
|
+
|
|
373
|
+
**Known class-A-compatible Ashby tenants (same observations).** These tenants accepted headless submits cleanly, often with `imeFriendly: true` making the difference on the text-field subset:
|
|
374
|
+
|
|
375
|
+
- Supabase, LangChain, Poolside, Runway Financial
|
|
376
|
+
|
|
377
|
+
The pattern is tenant configuration, not role or company size. Lists drift as tenants tune their anti-bot — treat as probabilistic priors, not hard rules.
|
|
378
|
+
|
|
379
|
+
### Greenhouse Bot-Detection Honeypots
|
|
380
|
+
|
|
381
|
+
Some Greenhouse tenants (Grafana Labs confirmed, 2026-04-19) inject a honeypot-style single-pick question on the application form, rendered as a listbox labeled something like "Which of the following best describes you?" with options resembling "I am a human being / I am a bot / I am a robot".
|
|
382
|
+
|
|
383
|
+
**Rule:** pick the "I am a human being" option (or whichever option is the obvious human-authentic choice). Bots that pick other options are filtered before submit. This is NOT a validation check — the field will always read back clean — but the submit will be silently discarded if the wrong option is selected.
|
|
384
|
+
|
|
385
|
+
If the honeypot question is absent, skip. If present, always pick the human option.
|
|
386
|
+
|
|
367
387
|
### Nested Scroll Containers (Greenhouse / Ashby)
|
|
368
388
|
|
|
369
389
|
The major ATS portals (Greenhouse, Workday, Lever, Ashby) use nested scrollable regions. A field's `visibleBounds` may show it as off-screen even when it is actually visible within a child scroll container. Geometra's `scroll_to` operates on the outermost page scroll, so it cannot reach fields in inner scroll regions.
|
package/CLAUDE.md
CHANGED
|
@@ -302,6 +302,7 @@ When a form says "enter the code we sent to your email", you MUST retrieve the c
|
|
|
302
302
|
| Lever | `from:lever newer_than:10m` |
|
|
303
303
|
| Ashby | `from:ashby newer_than:10m` |
|
|
304
304
|
| SmartRecruiters | `from:smartrecruiters newer_than:10m` |
|
|
305
|
+
| Toast (via ClinchTalent) | `from:toast.mail.clinchtalent.com newer_than:15m` OR `subject:"verify your login at Toast" newer_than:15m` |
|
|
305
306
|
| Aggregator redirect (WeWorkRemotely / RemoteOK) | Detect the underlying ATS from the post-redirect URL, then use that row's sender query |
|
|
306
307
|
| Unknown | `newer_than:10m subject:(verify OR code OR confirm)` |
|
|
307
308
|
|
|
@@ -309,6 +310,7 @@ When a form says "enter the code we sent to your email", you MUST retrieve the c
|
|
|
309
310
|
- ALWAYS check Gmail before reporting a submission as failed.
|
|
310
311
|
- If "submit button did nothing", it usually means an OTP step appeared. Check Gmail.
|
|
311
312
|
- If no email after 10 seconds, retry `gmail_list_messages` once more with `newer_than:5m`.
|
|
313
|
+
- **Some Greenhouse tenants route OTP through third-party verification (Toast uses ClinchTalent).** If `from:greenhouse` returns empty after a Greenhouse submit, check the tenant-specific sender row above. Confirmed 2026-04-19: Toast Principal SWE #807 and Toast Senior FE #808.
|
|
312
314
|
|
|
313
315
|
---
|
|
314
316
|
|
|
@@ -364,6 +366,24 @@ These blocks come from two distinct root causes and require different responses:
|
|
|
364
366
|
|
|
365
367
|
**Rule — do NOT loop retrying a class B block.** One retry with `imeFriendly: true` is the correct test for class A. If the same spam message fires after a clean `imeFriendly` refill, stop, mark Failed, move on. Repeated retries waste subagent time and do not change the outcome.
|
|
366
368
|
|
|
369
|
+
**Known-block Ashby tenants (2026-04-19 empirical observations).** These tenants fired class B on every attempted submit from a headless datacenter-IP proxy. Orchestrators planning apply dispatches should assume these tenants will Fail in headless — prioritize other portals, or skip same-tenant siblings after a confirmed class B to avoid burning subagent slots:
|
|
370
|
+
|
|
371
|
+
- Vellum, Linear, Vanta, River Financial, Higharc, Trace Labs, Solace Health, Unstructured, ClickUp, Zapier, Deepgram, Ramp, WorkOS
|
|
372
|
+
|
|
373
|
+
**Known class-A-compatible Ashby tenants (same observations).** These tenants accepted headless submits cleanly, often with `imeFriendly: true` making the difference on the text-field subset:
|
|
374
|
+
|
|
375
|
+
- Supabase, LangChain, Poolside, Runway Financial
|
|
376
|
+
|
|
377
|
+
The pattern is tenant configuration, not role or company size. Lists drift as tenants tune their anti-bot — treat as probabilistic priors, not hard rules.
|
|
378
|
+
|
|
379
|
+
### Greenhouse Bot-Detection Honeypots
|
|
380
|
+
|
|
381
|
+
Some Greenhouse tenants (Grafana Labs confirmed, 2026-04-19) inject a honeypot-style single-pick question on the application form, rendered as a listbox labeled something like "Which of the following best describes you?" with options resembling "I am a human being / I am a bot / I am a robot".
|
|
382
|
+
|
|
383
|
+
**Rule:** pick the "I am a human being" option (or whichever option is the obvious human-authentic choice). Bots that pick other options are filtered before submit. This is NOT a validation check — the field will always read back clean — but the submit will be silently discarded if the wrong option is selected.
|
|
384
|
+
|
|
385
|
+
If the honeypot question is absent, skip. If present, always pick the human option.
|
|
386
|
+
|
|
367
387
|
### Nested Scroll Containers (Greenhouse / Ashby)
|
|
368
388
|
|
|
369
389
|
The major ATS portals (Greenhouse, Workday, Lever, Ashby) use nested scrollable regions. A field's `visibleBounds` may show it as off-screen even when it is actually visible within a child scroll container. Geometra's `scroll_to` operates on the outermost page scroll, so it cannot reach fields in inner scroll regions.
|
package/iso/instructions.md
CHANGED
|
@@ -302,6 +302,7 @@ When a form says "enter the code we sent to your email", you MUST retrieve the c
|
|
|
302
302
|
| Lever | `from:lever newer_than:10m` |
|
|
303
303
|
| Ashby | `from:ashby newer_than:10m` |
|
|
304
304
|
| SmartRecruiters | `from:smartrecruiters newer_than:10m` |
|
|
305
|
+
| Toast (via ClinchTalent) | `from:toast.mail.clinchtalent.com newer_than:15m` OR `subject:"verify your login at Toast" newer_than:15m` |
|
|
305
306
|
| Aggregator redirect (WeWorkRemotely / RemoteOK) | Detect the underlying ATS from the post-redirect URL, then use that row's sender query |
|
|
306
307
|
| Unknown | `newer_than:10m subject:(verify OR code OR confirm)` |
|
|
307
308
|
|
|
@@ -309,6 +310,7 @@ When a form says "enter the code we sent to your email", you MUST retrieve the c
|
|
|
309
310
|
- ALWAYS check Gmail before reporting a submission as failed.
|
|
310
311
|
- If "submit button did nothing", it usually means an OTP step appeared. Check Gmail.
|
|
311
312
|
- If no email after 10 seconds, retry `gmail_list_messages` once more with `newer_than:5m`.
|
|
313
|
+
- **Some Greenhouse tenants route OTP through third-party verification (Toast uses ClinchTalent).** If `from:greenhouse` returns empty after a Greenhouse submit, check the tenant-specific sender row above. Confirmed 2026-04-19: Toast Principal SWE #807 and Toast Senior FE #808.
|
|
312
314
|
|
|
313
315
|
---
|
|
314
316
|
|
|
@@ -364,6 +366,24 @@ These blocks come from two distinct root causes and require different responses:
|
|
|
364
366
|
|
|
365
367
|
**Rule — do NOT loop retrying a class B block.** One retry with `imeFriendly: true` is the correct test for class A. If the same spam message fires after a clean `imeFriendly` refill, stop, mark Failed, move on. Repeated retries waste subagent time and do not change the outcome.
|
|
366
368
|
|
|
369
|
+
**Known-block Ashby tenants (2026-04-19 empirical observations).** These tenants fired class B on every attempted submit from a headless datacenter-IP proxy. Orchestrators planning apply dispatches should assume these tenants will Fail in headless — prioritize other portals, or skip same-tenant siblings after a confirmed class B to avoid burning subagent slots:
|
|
370
|
+
|
|
371
|
+
- Vellum, Linear, Vanta, River Financial, Higharc, Trace Labs, Solace Health, Unstructured, ClickUp, Zapier, Deepgram, Ramp, WorkOS
|
|
372
|
+
|
|
373
|
+
**Known class-A-compatible Ashby tenants (same observations).** These tenants accepted headless submits cleanly, often with `imeFriendly: true` making the difference on the text-field subset:
|
|
374
|
+
|
|
375
|
+
- Supabase, LangChain, Poolside, Runway Financial
|
|
376
|
+
|
|
377
|
+
The pattern is tenant configuration, not role or company size. Lists drift as tenants tune their anti-bot — treat as probabilistic priors, not hard rules.
|
|
378
|
+
|
|
379
|
+
### Greenhouse Bot-Detection Honeypots
|
|
380
|
+
|
|
381
|
+
Some Greenhouse tenants (Grafana Labs confirmed, 2026-04-19) inject a honeypot-style single-pick question on the application form, rendered as a listbox labeled something like "Which of the following best describes you?" with options resembling "I am a human being / I am a bot / I am a robot".
|
|
382
|
+
|
|
383
|
+
**Rule:** pick the "I am a human being" option (or whichever option is the obvious human-authentic choice). Bots that pick other options are filtered before submit. This is NOT a validation check — the field will always read back clean — but the submit will be silently discarded if the wrong option is selected.
|
|
384
|
+
|
|
385
|
+
If the honeypot question is absent, skip. If present, always pick the human option.
|
|
386
|
+
|
|
367
387
|
### Nested Scroll Containers (Greenhouse / Ashby)
|
|
368
388
|
|
|
369
389
|
The major ATS portals (Greenhouse, Workday, Lever, Ashby) use nested scrollable regions. A field's `visibleBounds` may show it as off-screen even when it is actually visible within a child scroll container. Geometra's `scroll_to` operates on the outermost page scroll, so it cannot reach fields in inner scroll regions.
|
package/merge-tracker.mjs
CHANGED
|
@@ -60,6 +60,29 @@ Run from the repository root.`);
|
|
|
60
60
|
const CANONICAL_STATES = loadCanonicalStates(PROJECT_DIR) || DEFAULT_STATES;
|
|
61
61
|
const STATUS_DETECT_RE = buildStatusDetectionRegex(CANONICAL_STATES);
|
|
62
62
|
|
|
63
|
+
// Lifecycle precedence — higher value means the status represents a later
|
|
64
|
+
// stage of the application and should override an earlier stage on merge,
|
|
65
|
+
// independent of score. Evaluated (pure eval, no action) is the baseline;
|
|
66
|
+
// any action state outranks it. This fixes a historical bug where a higher-
|
|
67
|
+
// score Evaluated row would silently block an Applied/Failed/SKIP outcome
|
|
68
|
+
// from propagating because the merge considered score alone.
|
|
69
|
+
const STATUS_PRECEDENCE = {
|
|
70
|
+
'Evaluated': 0,
|
|
71
|
+
'SKIP': 1,
|
|
72
|
+
'Discarded': 1,
|
|
73
|
+
'Contacted': 2,
|
|
74
|
+
'Failed': 2,
|
|
75
|
+
'Applied': 3,
|
|
76
|
+
'Responded': 4,
|
|
77
|
+
'Rejected': 4,
|
|
78
|
+
'Interview': 5,
|
|
79
|
+
'Offer': 6,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function statusRank(s) {
|
|
83
|
+
return STATUS_PRECEDENCE[s] ?? 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
63
86
|
function validateStatus(status) {
|
|
64
87
|
const clean = status.replace(/\*\*/g, '').replace(/\s+\d{4}-\d{2}-\d{2}.*$/, '').trim();
|
|
65
88
|
const lower = clean.toLowerCase();
|
|
@@ -274,25 +297,49 @@ for (const file of tsvFiles) {
|
|
|
274
297
|
if (duplicate) {
|
|
275
298
|
const newScore = parseScore(addition.score);
|
|
276
299
|
const oldScore = parseScore(duplicate.score);
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
300
|
+
const newRank = statusRank(addition.status);
|
|
301
|
+
const oldRank = statusRank(duplicate.status);
|
|
302
|
+
|
|
303
|
+
// Update if EITHER the lifecycle status advances (e.g. Evaluated → Applied)
|
|
304
|
+
// OR the score improves. Never regress the status (Applied → Evaluated is
|
|
305
|
+
// ignored). Same-rank same-score updates are skipped as no-op.
|
|
306
|
+
const statusAdvances = newRank > oldRank;
|
|
307
|
+
const statusRegresses = newRank < oldRank;
|
|
308
|
+
const scoreImproves = newScore > oldScore;
|
|
309
|
+
|
|
310
|
+
if (statusAdvances || (!statusRegresses && scoreImproves)) {
|
|
311
|
+
const newStatus = statusAdvances ? addition.status : duplicate.status;
|
|
312
|
+
const newPdf = statusAdvances ? addition.pdf : duplicate.pdf;
|
|
313
|
+
const reason = statusAdvances
|
|
314
|
+
? `${duplicate.status}→${newStatus}`
|
|
315
|
+
: `${oldScore}→${newScore}`;
|
|
316
|
+
console.log(`🔄 Update: #${duplicate.num} ${addition.company} — ${addition.role} (${reason})`);
|
|
280
317
|
|
|
281
318
|
if (layout === 'day') {
|
|
282
|
-
// Update in existing entries list for later write
|
|
283
319
|
duplicate.date = addition.date;
|
|
284
320
|
duplicate.company = addition.company;
|
|
285
321
|
duplicate.role = addition.role;
|
|
286
|
-
duplicate.score = addition.score;
|
|
322
|
+
duplicate.score = scoreImproves ? addition.score : duplicate.score;
|
|
323
|
+
duplicate.status = newStatus;
|
|
324
|
+
duplicate.pdf = newPdf;
|
|
287
325
|
duplicate.report = addition.report;
|
|
288
|
-
duplicate.notes =
|
|
326
|
+
duplicate.notes = statusAdvances
|
|
327
|
+
? addition.notes
|
|
328
|
+
: `Re-eval ${addition.date} (${oldScore}→${newScore}). ${addition.notes}`;
|
|
289
329
|
} else {
|
|
290
330
|
const lineIdx = appLines.indexOf(duplicate.raw);
|
|
331
|
+
const outScore = scoreImproves ? addition.score : duplicate.score;
|
|
332
|
+
const noteText = statusAdvances
|
|
333
|
+
? addition.notes
|
|
334
|
+
: `Re-eval ${addition.date} (${oldScore}→${newScore}). ${addition.notes}`;
|
|
291
335
|
if (lineIdx >= 0) {
|
|
292
|
-
appLines[lineIdx] = `| ${duplicate.num} | ${addition.date} | ${addition.company} | ${addition.role} | ${
|
|
336
|
+
appLines[lineIdx] = `| ${duplicate.num} | ${addition.date} | ${addition.company} | ${addition.role} | ${outScore} | ${newStatus} | ${newPdf} | ${addition.report} | ${noteText} |`;
|
|
293
337
|
}
|
|
294
338
|
}
|
|
295
339
|
updated++;
|
|
340
|
+
} else if (statusRegresses) {
|
|
341
|
+
console.log(`⏭️ Skip: ${addition.company} — ${addition.role} (existing #${duplicate.num} status ${duplicate.status} outranks new ${addition.status})`);
|
|
342
|
+
skipped++;
|
|
296
343
|
} else {
|
|
297
344
|
console.log(`⏭️ Skip: ${addition.company} — ${addition.role} (existing #${duplicate.num} ${oldScore} >= new ${newScore})`);
|
|
298
345
|
skipped++;
|
package/modes/apply.md
CHANGED
|
@@ -260,6 +260,22 @@ If you've uploaded a file with a dedicated `geometra_run_actions` call (e.g., th
|
|
|
260
260
|
|
|
261
261
|
Specific portals — Workday "parse my resume", iCIMS multi-step, SAP SuccessFactors — reveal additional fields ONLY after a file upload. In that case, use exactly two `run_actions` calls: (1) upload + wait_for, (2) fill+submit. After the first call, call `geometra_form_schema` **once** to discover the newly-revealed labels, then run the second call using labels. Never more than two phases.
|
|
262
262
|
|
|
263
|
+
### Resume-upload silent-fail → chooser-strategy fallback (Greenhouse)
|
|
264
|
+
|
|
265
|
+
Some Greenhouse tenants (Grafana Labs confirmed, 2026-04-19) render the resume upload as a file input where the default `upload_files` action readback succeeds but the field stays empty — Submit returns "Resume/CV is required." only after submit is clicked.
|
|
266
|
+
|
|
267
|
+
**Fix:** if the resume field shows empty after an `upload_files` action (either by explicit readback or by a "Resume/CV is required" error post-submit), re-upload using `strategy: chooser` with x,y coordinates pulled from the upload button's `visibleBounds` center. Example:
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
{ type: "upload_files",
|
|
271
|
+
fieldLabel: "Resume/CV",
|
|
272
|
+
paths: ["/abs/path/cv.pdf"],
|
|
273
|
+
strategy: "chooser",
|
|
274
|
+
x: 314, y: 474 }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The `chooser` strategy triggers the native file picker via click-at-coordinates, which bypasses the React-controlled input that silently drops programmatic assignments on some Greenhouse tenants. One retry is enough; if it still fails, mark Failed.
|
|
278
|
+
|
|
263
279
|
## Step 6 — Resolve OTP verification (if prompted)
|
|
264
280
|
|
|
265
281
|
Check for an OTP gate after the candidate (or Geometra) submits — the major portals (Greenhouse, Workday, Lever, Ashby) gate submission behind an email verification code. When an OTP step appears, do this.
|
|
@@ -282,6 +298,7 @@ Check for an OTP gate after the candidate (or Geometra) submits — the major po
|
|
|
282
298
|
| `smartrecruiters` | `from:smartrecruiters newer_than:10m` |
|
|
283
299
|
| `wwr` / `remoteok` | Follow the apply redirect to the underlying ATS, re-detect the host, then use that row's query. Aggregators do not send OTP emails themselves. |
|
|
284
300
|
| `builtin` | `from:builtin newer_than:10m` |
|
|
301
|
+
| Toast (via Greenhouse + ClinchTalent) | `from:toast.mail.clinchtalent.com newer_than:15m` OR `subject:"verify your login at Toast" newer_than:15m`. Default `from:greenhouse` returns null — Toast routes OTP through ClinchTalent. |
|
|
285
302
|
| `custom` / `unknown` / missing | `newer_than:10m subject:(verify OR code OR confirm)` |
|
|
286
303
|
|
|
287
304
|
**Fallback when `ats` is missing** (legacy pipeline entries with no `| ats=` suffix, or scan-output without an `ats` column): infer from the URL host — `*.greenhouse.io` → `greenhouse`; `jobs.ashbyhq.com` → `ashby`; `jobs.lever.co` → `lever`; `*.myworkdayjobs.com` → `workday`; `apply.workable.com` / `jobs.workable.com` → `workable`; `api.smartrecruiters.com` / `jobs.smartrecruiters.com` → `smartrecruiters`; `weworkremotely.com` → `wwr`; `remoteok.com` → `remoteok`; `builtin.com` → `builtin`; otherwise use the generic `verify OR code OR confirm` subject query.
|
package/modes/scan.md
CHANGED
|
@@ -45,7 +45,7 @@ Supported API shapes:
|
|
|
45
45
|
- **Endpoint**: `https://boards-api.greenhouse.io/v1/boards/{slug}/jobs`
|
|
46
46
|
- **Method**: `GET` (plain, no auth)
|
|
47
47
|
- **Shape**: `{ jobs: [{ id, title, absolute_url, updated_at, location: { name } }, ...] }`
|
|
48
|
-
- **Canonical URL to record**: `https://job-boards.greenhouse.io/{slug}/jobs/{id}` — do NOT use `absolute_url` when it points to a customer-skinned front-end (see
|
|
48
|
+
- **Canonical URL to record**: `https://job-boards.greenhouse.io/{slug}/jobs/{id}` — do NOT use `absolute_url` when it points to a customer-skinned front-end (see **Verify Before Marking CLOSED** below).
|
|
49
49
|
- **ats**: `greenhouse`
|
|
50
50
|
|
|
51
51
|
#### Ashby (JSON, per-company board)
|
|
@@ -75,7 +75,7 @@ Supported API shapes:
|
|
|
75
75
|
```json
|
|
76
76
|
{"appliedFacets": {}, "limit": 20, "offset": 0, "searchText": ""}
|
|
77
77
|
```
|
|
78
|
-
- **Required headers**: `Content-Type: application/json`, `Accept: application/json`.
|
|
78
|
+
- **Required headers**: `Content-Type: application/json`, `Accept: application/json`. If the response is 403, set a realistic `User-Agent` header and retry — Workday tenants selectively block data-center UAs.
|
|
79
79
|
- **Shape**: `{ jobPostings: [{ title, externalPath, postedOn, locationsText, bulletFields }, ...], total }`
|
|
80
80
|
- **Canonical URL to record**: `https://{subdomain}.{pod}.myworkdayjobs.com/{site}{externalPath}` (note: `externalPath` already starts with `/job/...` — do NOT prepend an extra `/`).
|
|
81
81
|
- **Pagination**: increment `offset` by `limit` (20) until `jobPostings.length < limit` or `offset >= total`.
|
|
@@ -287,7 +287,7 @@ NEXT STEP RECOMMENDATION:
|
|
|
287
287
|
|
|
288
288
|
## Verify Before Marking CLOSED (downstream rule)
|
|
289
289
|
|
|
290
|
-
**DO NOT mark a Greenhouse offer CLOSED based on a WebFetch/Geometra result alone.** Customer-skinned careers pages
|
|
290
|
+
**DO NOT mark a Greenhouse offer CLOSED based on a WebFetch/Geometra result alone.** Customer-skinned careers pages serve bot-hostile shells — a 403, a navbar-only response, or a client-side-only render — and WebFetch sees "no JD" and mis-classifies as CLOSED. Known customer-skinned hosts: `pinterestcareers.com`, `okta.com`, `samsara.com`, `zoominfo.com`, `collibra.com`, `careers.toasttab.com`, `careers.airbnb.com`, `coinbase.com`, `instacart.careers`. Treat any host that is NOT `greenhouse.io` / `job-boards.greenhouse.io` / `boards-api.greenhouse.io` as customer-skinned.
|
|
291
291
|
|
|
292
292
|
**Correct verification order for any Greenhouse-sourced URL** (identified by a `| gh={slug}/{id}` suffix in `pipeline.md` or a `boards-api.greenhouse.io` / `job-boards.greenhouse.io` / `boards.greenhouse.io` host):
|
|
293
293
|
|
|
@@ -298,7 +298,7 @@ NEXT STEP RECOMMENDATION:
|
|
|
298
298
|
2. Only then fall back to WebFetch of the canonical `job-boards.greenhouse.io/{slug}/jobs/{id}` URL.
|
|
299
299
|
3. Only then fall back to Geometra on the same canonical URL.
|
|
300
300
|
|
|
301
|
-
**Rule
|
|
301
|
+
**Rule:** Greenhouse postings with valid `gh_slug`/`gh_id` MUST be verified via the API first. A WebFetch failure on a customer-skinned domain is NOT evidence the role is closed.
|
|
302
302
|
|
|
303
303
|
## Update careers_url
|
|
304
304
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-forge",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "AI-powered job search pipeline built on opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"tokens": "node scripts/token-usage-report.mjs",
|
|
19
19
|
"tokens:today": "node scripts/token-usage-report.mjs --days 1",
|
|
20
20
|
"tokens:log": "node scripts/token-usage-report.mjs --days 1 --append",
|
|
21
|
-
"build:config": "iso
|
|
22
|
-
"prepack": "iso
|
|
21
|
+
"build:config": "iso build .",
|
|
22
|
+
"prepack": "iso build .",
|
|
23
23
|
"release:check-source": "node ./scripts/release/check-source.mjs",
|
|
24
24
|
"postinstall": "node bin/sync.mjs"
|
|
25
25
|
},
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"playwright": "^1.58.1"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
+
"@razroo/iso": "^0.1.1",
|
|
77
78
|
"@razroo/iso-harness": "^0.1.3"
|
|
78
79
|
}
|
|
79
80
|
}
|