- ensureLocalFile() downloads binary files (resume PDF) from S3 to temp
- Applier downloads resume from S3 before applying
- Cached in /tmp to avoid re-downloading each run
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
storage.mjs is now a single interface: loadJSON() and saveJSON()
route to either local disk or S3 based on settings.storage.type.
The app never touches disk/S3 directly.
- All queue/log functions are now async (saveQueue, appendLog, etc.)
- All callers updated with await
- Data validation prevents saving corrupt types (strings, nulls)
- S3 versioned bucket preserves every write
- Config: storage.type = "local" (disk) or "s3" (S3 primary)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New lib/storage.mjs: async S3 backup on every queue/log save
- Versioned S3 bucket (claw-apply-data) keeps every revision
- Auto-restore from S3 if local file is missing or corrupt
- saveQueue/saveLog now validate data type before writing
(prevents the exact bug that corrupted the queue)
- IAM role attached to EC2 instance for credential-free S3 access
- Config: storage.type = "local" (default) or "s3"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Invisible reCAPTCHA + server round-trip can take 10-30s. Increased
from 6x2.5s (~15s) to 10x3s (~30s) polling window.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Playwright on AWS doesn't have pressSequentially, use type() instead.
Extend submit polling from 4 to 6 iterations (~15s total) to handle
slower reCAPTCHA verification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use .ashby-application-form-field-entry class to find question
containers, then Playwright locators for reliable button clicking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ashby: detect Yes/No button questions for work auth, sponsorship,
and consent. Click appropriate button in beforeSubmit hook.
Greenhouse: use pressSequentially instead of fill() for React
Select comboboxes (Country, Location). Click the dropdown option
after typing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Country inputs on Greenhouse use React Select comboboxes that
need option click after typing. Add 'country' to the autocomplete
trigger list alongside city/location.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prefer resume-specific file inputs (#resume, #_systemfield_resume)
over autofill inputs. Skip autofill class inputs in fallback.
Also fix Ashby beforeSubmit to target #_systemfield_resume directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After clicking submit, poll up to 4 times (2.5s + 3x2s = ~8.5s total)
for success text or form disappearance. Handles invisible reCAPTCHA
verification delay. Stops early on validation errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
External ATS jobs now show their actual apply URL instead of the
LinkedIn listing URL. Also added Ashby-specific "job not found"
text to closed detection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greenhouse wraps phone inputs in a fieldset with legend "Phone"
that gets picked up as an unanswered radio group. Skip these
since the actual phone input is handled separately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip inputs with placeholder="Search" or pre-filled values when
they have a "Phone" label — these are country code pickers, not
the actual phone input.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Form filler now defaults to page root instead of scoping to
[role="dialog"]. LinkedIn Easy Apply passes its modal selector
explicitly. Fixes external ATS forms being scoped to wrong
container. Also improved Greenhouse handler with targeted
resume upload and form detection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ashby wraps file input inside #_systemfield_resume container — search
for the actual input[type="file"] element. Also capture and log
validation errors from the page when submit returns incomplete.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generic handler now accepts options (transformUrl, formDetector,
submitSelector, resumeSelector, beforeSubmit, verifySelector, etc.).
Each ATS handler passes its overrides instead of reimplementing.
Registry resolves handlers by: apply_type -> URL domain -> generic fallback.
New ATS handlers only need to export SUPPORTED_TYPES and an apply() that
calls genericApply with platform-specific options.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Page scraping was grabbing wrong elements (e.g. "Location" instead of
company name on Ashby). Queue already has correct metadata.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Invisible reCAPTCHA (size=invisible) fires on submit and usually passes
automatically. Only block on visible CAPTCHA challenges. Also fix form
detection to work without <form> wrapper elements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Best-effort form filler for any career page with standard HTML forms.
Handles single-page and multi-step flows, resume upload, login wall
and CAPTCHA detection. All ATS stub handlers now delegate to generic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Secondary processes (standalone classifier, ad-hoc scripts) now write
to queue_updates.jsonl via writePendingUpdate() instead of modifying
jobs_queue.json directly. Primary processes pick up updates on next
loadQueue() call using atomic rename-then-read.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
External Apply buttons are <a> tags with LinkedIn redirect URLs, not
<button> elements. Extract the real URL from the redirect's query
parameter instead of clicking and waiting for a new tab.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After LinkedIn search completes, visits each unknown_external job page,
clicks the Apply button, captures the redirect URL, and matches against
known ATS patterns to identify the actual application platform.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LinkedIn shows different text on direct /apply/ URL vs button click:
- Button: "Easy Apply limit"
- Direct URL: "limit daily submissions" / "apply tomorrow"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When LinkedIn shows "Easy Apply limit" message, detect it in the
modal-open check, return rate_limited status, put job back in queue,
send Telegram alert, and stop the entire run. Prevents burning retries
and wasting browser sessions when rate limited.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Some LinkedIn listings have image-only upload fields (JPG/PNG).
Don't attempt to upload a PDF resume to these inputs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- answerFor returning '' (intentionally blank) was treated as falsy,
falling through to AI which fabricated "123 Main Street". Now
empty string skips the field without triggering AI or reporting unknown.
- status.mjs was printing to stdout AND sending via Telegram, causing
OpenClaw to relay a duplicate plain-text copy. Now only prints to
stdout when Telegram isn't configured.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LinkedIn validates number fields even when not marked required in the
DOM. Previously these were skipped (no AI call, no answer). Now number
fields always trigger AI fallback and are reported as unknown if empty.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LinkedIn renders label text twice in nested spans, producing
"Question? Question?" instead of "Question?". The old dedup only
caught exact concatenation (ABCABC); now also handles space-separated
duplicates by comparing left/right halves at the midpoint space.
Applied to all 4 copies: extractLabel, _extractLabel, normalizeLegend,
_normalizeLegend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add street/address pattern to answerFor() — returns profile address or empty string
- Update AI prompt: return "UNKNOWN" instead of guessing facts
- Handle UNKNOWN response by treating it as no answer (triggers Telegram ask)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Number inputs (type="number") were getting answers like "5 years"
instead of "5", causing LinkedIn validation errors. Now:
- AI gets "(must be a number, no text or units)" hint
- Answers are stripped to digits before filling
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Profile name is returned by ensureAuth() from the auth connection
(looked up by domain). No more storing profile names in settings.json
for the applier flow. createBrowser() still supports legacy platform
keys as fallback for searcher/setup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Connection IDs change when re-authing. Instead of storing IDs in
settings.json (which go stale), look up connections by domain at
runtime via kernel.auth.connections.list({ domain }). This keeps
the applier in sync regardless of connection recreation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite session.mjs: check connection status via SDK before creating
browser. If NEEDS_AUTH + can_reauth, auto re-auth with stored creds.
If can't re-auth, send Telegram alert and skip platform.
- Wire ensureAuth() into job_applier.mjs before createBrowser()
- Jobs are returned to queue (not failed) when auth is down
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Captures placeholder from textareas in snapshot. When AI fallback is
used, includes placeholder as a format hint so the AI knows expected
format (dates, phone numbers, URLs, etc).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reads input placeholder in snapshot. If it contains MM/DD/YYYY pattern,
auto-fills with today's date in the correct format. Generic — works
regardless of the field label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Catches 'Date of Application', 'Available Start Date', and other
date fields that expect mm/dd/yyyy format.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Start date fields now return mm/dd/yyyy format instead of "Immediately"
- Cancel any open sub-forms or date pickers before filling (handles
stacked popups with double-cancel check)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LinkedIn sometimes opens an Add Education/Experience sub-form with
Save/Cancel buttons that blocks the main Next button. Detect and
cancel these before attempting to fill the step.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- getModalDebugInfo: one evaluate() for heading, buttons, errors (was N+2 calls)
- selectOptionFuzzy: batch-read option texts in one evaluate (was N calls)
- Tag elements with data-claw-idx during snapshot, query by attribute in fill()
(fixes fragile positional index matching for checkboxes/inputs)
- Log field counts per fill step for debugging
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>