- filter.mjs: loadProfile now async, uses loadJSON
- telegram_answers.mjs: answers read/write through storage layer
- status.mjs: uses initQueue + loadQueue for S3 support
- setup.mjs: await all loadConfig calls
- storage.mjs: more robust getS3Key using URL parsing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- loadConfig now uses loadJSON when storage is initialized
- Fix getS3Key to handle config/ and data/ paths (not just data/)
- All loadConfig calls updated to await
- settings.json still bootstraps from disk (needed to know storage type)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents spending hours classifying unknown_external jobs at the
end of a long search run. Remaining get classified on next run.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Failed keywords log the error and continue to the next one.
Not marked complete, so they'll be retried on next run.
Also await async onPage callback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Browser crash recovery: per-keyword error handling, auto-recreate
browser and re-login if page dies mid-search
- Platform-level retry: if browser creation or login fails entirely,
retry up to 3 times with escalating waits (5/10/15 min)
- Progress saved after each search track (not just at end)
- Unhandled rejection handler to prevent silent process death
- Fixed async callback in classifyExternalJobs
- Added ensureLoggedIn to session.mjs for searcher flow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Searcher creates browser first then verifies login, unlike applier
which checks auth before browser creation. Both paths now work.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
Only classify jobs without an apply_url — those with one have already
been visited and don't need re-classification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ashby and unknown_external work. Greenhouse, Lever, Jobvite have
visible CAPTCHAs. Workday requires login. Keep those disabled until
proper handlers are built.
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>
Iterate over the full queue array instead of getJobsByStatus() results,
and pass it to saveQueue(). The previous code passed no argument, which
would corrupt or silently fail the save.
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>
Replaces flat "Breakdown: ..." line with indented sub-items showing
Easy Apply, Wellfound, Greenhouse etc. counts under Ready to apply.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When LinkedIn is rate-limited, its jobs were filling the maxApps quota
but then getting skipped, leaving 0 applied. Now excludes easy_apply
jobs from selection during cooldown so Wellfound jobs get picked up.
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>
When rate limited, writes timestamp to data/linkedin_rate_limited_at.json.
Subsequent runs skip LinkedIn until 6 hours have passed. Other platforms
(Wellfound) continue unaffected. Cooldown file auto-deleted on expiry.
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>
Don't break the entire run when LinkedIn rate limits — skip LinkedIn
jobs and continue to Wellfound and other platforms.
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>