Commit Graph

98 Commits

Author SHA1 Message Date
029360b118 Dismiss LinkedIn education/experience sub-forms before filling
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>
2026-03-06 14:54:40 -08:00
d0a40e4654 Batch CDP calls: single-evaluate debug info, data-claw-idx tagging, field count logging
- 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>
2026-03-06 14:50:47 -08:00
a956b98941 Stop auto-checking 'top choice' and 'interested' checkboxes
Only auto-check confirm/agree/consent checkboxes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:46:03 -08:00
23eb5284fa Fix snapshot evaluate: scope queries to container element, handle Page fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:42:41 -08:00
aeb41b42c2 Optimize form_filler: batch DOM reads into single evaluate() call
Instead of 60+ sequential CDP round-trips per step (isVisible, getLabel,
inputValue, isRequired for each element), snapshot all form state in one
evaluate() call, do answer matching locally, then only make CDP calls to
fill/click elements that need action.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:39:18 -08:00
dc7113907b Fix Anthropic API 404 — correct model ID to claude-sonnet-4-6
The dated model ID claude-sonnet-4-6-20251101 doesn't exist, causing
keyword generation to fall back to static keywords.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:24:38 -08:00
c4e5dbc32a Fix Telegram code block rendering — remove leading spaces from messages
Telegram Markdown treats lines starting with spaces as preformatted code
blocks. Replaced leading spaces with bullet points and arrows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:14:47 -08:00
cd454f8cc2 Clean up status report and apply summary
Status: only show non-zero queue counts, compact breakdown line,
remove emoji clutter
Apply summary: remove misleading green check on header, show applied/total

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:05:45 -08:00
b0a2eb3746 Fix fieldset radios: validate answers against options, fix checkbox skip, fix authorization match
Tested against Webflow (4311307116) and Scout Global (4378934058):
- Webflow: reaches Review page with all radios checked and checkbox group selected
- Scout Global: already applied successfully with select fix

Three fixes:
1. Fieldset radio answers validated against available options - prevents
   experience pattern returning "7" when options are Yes/No
2. Checkbox groups no longer skipped when one checkbox already checked -
   was preventing multi-select from working on retry
3. "authoriz" pattern matches both "authorized" and "authorization"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:05:02 -08:00
053c0b1242 Fix select answer mismatch: validate answer against available options
Built-in answerFor() could return a number (e.g. "7" for years experience)
when the select only has Yes/No options, causing selectOptionFuzzy to fail
silently. Now checks if the answer matches any available option before using
it, falling through to AI with actual options if not.

Also added "right to work" to work authorization pattern matching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:44:49 -08:00
c99ea10585 Richer search and filter summaries
Search: show per-track breakdown (found/added per track name)
Filter: show top 5 scoring jobs with score, title, company and cost

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:34:21 -08:00
82fa2b3697 Rich apply summary with per-job details (title, company, link)
Each job now shows title @ company with a clickable link, grouped by
status category. Only non-empty categories are shown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:30:58 -08:00
615899e9a9 Normalize fieldset legend text same as getLabel (dedup, strip Required)
Legend text was saved to answers.json with duplicated text and "Required"
suffix, causing pattern mismatches on future runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:27:45 -08:00
094824abb2 Add dedicated 'closed' status for listings no longer accepting applications
Distinguishes closed listings from missing apply buttons. Shows in summary
as a separate line item.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:23:39 -08:00
97aa8472aa Fix radio selection: verify click worked, fallback to input/select
LinkedIn radio clicks via label can silently fail. Now verifies input:checked
after label click, falls back to clicking radio input directly by value match,
and tries <select> within fieldset as last resort.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:16:30 -08:00
bb0c96dd3d Enable Wellfound apply: fix missing apply_type, add submit verification
- Search now sets apply_type: 'wellfound' on discovered jobs (was missing,
  so applier silently skipped all Wellfound jobs)
- Add Wellfound to DEFAULT_ENABLED_APPLY_TYPES (LinkedIn first, then Wellfound)
- Sort platform processing order: linkedin → wellfound → external
- Apply handler: add closed listing detection, submit verification,
  error handling on meta extraction

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:48:03 -08:00
3e367687b2 Fix checkbox group (multi-select) handling in form filler
LinkedIn forms with "Select all that apply" questions use checkbox groups
inside fieldsets. The fieldset handler only clicked one label (radio behavior).
Now detects checkbox vs radio fieldsets and splits comma-separated AI answers
to click multiple labels. Also added "consent" to auto-check keyword list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:43:58 -08:00
26afc803a5 Navigate directly to apply URL instead of finding/clicking Easy Apply button
LinkedIn's apply URL pattern is {jobUrl}/apply/?openSDUIApplyFlow=true
which opens the modal directly. This eliminates:
- Button finding with waitForSelector (flaky on slow loads)
- Click retry logic
- "Continue" link fallback for draft applications
- Shadow DOM piercing for button detection

Tested: modal opens reliably, meta readable from background page,
form + progress bar present, 3.4s total navigation time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:32:34 -08:00
7a730f689e Use networkidle for job page navigation to fix intermittent button miss
domcontentloaded fires before JS renders the Easy Apply button,
causing flaky "No Easy Apply button found" failures on valid listings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:28:50 -08:00
c5ebdc9362 Fix answer saving: normalize answers format, improve label dedup
- normalizeAnswers() handles both object {"q":"a"} and array [{pattern,answer}]
  formats — prevents silent failures when answers.json format varies
- getLabel() now strips "Required" suffix before dedup, uses smarter
  prefix-repeat detection instead of simple half-split
- telegram_answers.mjs also normalizes on load
- Cleaned existing answers.json on AWS: removed duplicated text in patterns,
  fixed bad AI answer for "Current company", generalized Geotab-specific patterns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:24:10 -08:00
d43e2025b2 Fix process not exiting after run, detect closed job listings
- All entry points with log tee now call logStream.end() + process.exit()
  (log stream kept event loop alive, blocking next cron run)
- easy_apply: detect "no longer accepting applications" and similar closed
  listing text before reporting as unsupported

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:19:00 -08:00
a498f49b95 Fix critical import bugs in linkedin.mjs and wellfound.mjs, clean up status/setup
- linkedin.mjs: LINKEDIN_LINKEDIN_MAX_SEARCH_PAGES → LINKEDIN_MAX_SEARCH_PAGES
  (typo would crash searcher at module load — constant doesn't exist)
- wellfound.mjs: WELLFOUND_WELLFOUND_MAX_INFINITE_SCROLL → WELLFOUND_MAX_INFINITE_SCROLL
  (same — doubled prefix crashes at import)
- status.mjs: remove double-counting ATS from both queue and log
- setup.mjs: replace PM2 instructions with OpenClaw crons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:15:52 -08:00
51ca354c52 Audit fixes: remove dead code, fix run timeout bug, add log tee to all entry points
- Remove unused APPLY_PRIORITY array (replaced by score-based sort)
- Fix run timeout only breaking inner loop — now breaks outer platform loop too
- Remove dead lastProgress variable in easy_apply step loop
- Add stdout/stderr log tee to job_searcher, job_filter, telegram_poller

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:13:01 -08:00
8db00d94a5 Fix summary showing all zeros: count unhandled statuses, clean up format
- Default case in handleResult now increments skipped_other
- Summary only shows non-zero categories (cleaner output)
- Applied count always shown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:54:06 -08:00
7c9de1af4a AI fallback for unknown form fields: ask Claude before Telegram
Every unknown required field now goes through AI before falling back to
Telegram. Claude sees the question + all saved answers + profile, and
either recognizes it as a variation of a saved answer or generates a new
one. AI answers are auto-saved to answers.json so the same question is
a free pattern match next time. Telegram is now last resort (no API key).

Flow: pattern match (free) → AI (smart) → auto-save → Telegram (human)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:52:39 -08:00
3fc5c38df7 Add clear status logging for needs_answer and honeypot exits
Log lines now explicitly say STOPPING with reason when the modal is
dismissed due to unknown questions or honeypots. handleResult logs
the full flow: paused → generating AI answer → sent to Telegram →
will retry after reply. Prevents claw from misreading as a hang/timeout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:47:40 -08:00
0920554dad Add Telegram answer learning: poller + applier safety net
- New lib/telegram_answers.mjs: shared module that polls Telegram getUpdates,
  matches replies to needs_answer jobs, saves to answers.json, flips job to new
- telegram_poller.mjs: lightweight cron script (every minute via OpenClaw)
- Applier also processes replies at start of each run as safety net
- sendTelegram now returns message_id, stored on job for reply matching
- User replies "ACCEPT" to use AI answer, or types their own
- Answers persist in answers.json and apply to ALL future jobs
- Also includes: selectOptionFuzzy, multiple dialog handling, browser
  recovery, answers reload, per-job timeout bump to 10min

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:38:37 -08:00
14cf9a12c1 Reliability improvements: click retry, resume selection, answer loop, browser recovery
- Easy Apply click: click found element directly, retry with force if modal doesn't open
- Resume: select first radio if none checked, fall back to file upload
- AI answers: inject stored answers into formFiller on needs_answer retry
- Answers persistence: reload answers.json before each job for Telegram replies
- Browser recovery: detect dead page, create fresh browser session
- Multiple dialogs: findApplyModal() tags the right dialog among cookie banners etc.
- Select matching: case-insensitive fuzzy match with substring fallback
- dismissModal: scope Discard scan to dialog elements only
- Label dedup: normalize whitespace, fix odd-length edge case
- no_modal status: explicit handleResult case
- Per-job timeout: 10 minutes (was 3)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:30:09 -08:00
7f8cc3658e Fix submit verification: wait for modal detach instead of fixed sleep
Replace fixed 2.5s sleep + single check with event-driven waitForSelector
(state: 'detached', timeout: 8s). If modal persists, check for success
indicators (success text, Submit button gone) before marking incomplete.
Fixes false 'incomplete' status when LinkedIn shows post-submit dialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:23:03 -08:00
51a4231b5d fix: stuck detection uses progress bar value, not heading
Heading is always "Apply to <Company>" on every step — comparing it
caused false stuck detection on step 2. Now:
- Progress bar selector finds <progress> elements (no explicit role)
- Stuck detection re-reads progress AFTER clicking Next to see if it changed
- Threshold raised to 3 same-progress clicks before exiting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:08:35 -08:00
00bcfaca53 feat: include select options in unknown field reports and Telegram messages
When a required select has no answer, the unknown field now includes
the available options. Telegram notification shows them so the user
knows exactly what to reply with.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:06:38 -08:00
2c04504ad8 fix: report unfilled required selects as unknown fields
Non-EEO required selects with no answer were silently ignored, causing
infinite loops when LinkedIn blocked page advancement. Now reported as
unknown fields so the applier can exit with needs_answer status.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:05:54 -08:00
5aed8eb404 fix: use evaluateHandle to find Continue link in frames
page.$() and frame.$() can't find the <a> in LinkedIn's shadow DOM,
but frame.evaluateHandle with document.querySelectorAll can.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:00:46 -08:00
1e67c930db fix: search frames for Continue /apply/ link when page.$() misses it
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:59:41 -08:00
46c84bf28a fix: click parent <a> link for Continue, not child <span>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:57:35 -08:00
4c85a88902 fix: stuck loop on unfilled selects, Continue button detection
- Stuck detection after clicking Next: if heading+progress unchanged 2x,
  exit with 'stuck' status instead of looping forever
- Select dropdowns: treat "Select an option" as unfilled (LinkedIn's
  placeholder has a truthy value that was skipping the fill logic)
- Continue button fallback: detect draft "Continue" span via apply URL
  pattern when Easy Apply button not found

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:51:26 -08:00
5e4a0c6599 fix: always discard on modal dismiss, support "Continue applying" button
- dismissModal now always waits for Discard confirmation after closing
  (previously returned early after Dismiss click, leaving draft saved)
- LINKEDIN_APPLY_BUTTON_SELECTOR matches both "Easy Apply" and
  "Continue applying" (shown when a previous draft exists)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:34:19 -08:00
3c46de1358 fix: shadow DOM support — LinkedIn modal is inside shadow root
LinkedIn renders Easy Apply modal inside shadow DOM. document.querySelector()
in evaluate() cannot pierce shadow DOM, but Playwright's page.$() can.

easy_apply.mjs:
- Replaced all frame.evaluate(document.querySelector) with ElementHandle ops
- findModalButton uses modal.$$() + btn.evaluate() instead of evaluateHandle
- getModalDebugInfo uses modal.$eval and modal.$$() for all queries
- dismissModal scans buttons via page.$$() instead of evaluateHandle
- Removed findModalFrame (no longer needed)

form_filler.mjs:
- getLabel() walks up ancestor DOM to find labels (LinkedIn doesn't use label[for])
- Deduplicates repeated label text ("Phone country codePhone country code")
- isRequired() walks ancestors to find labels with * or required indicators

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:25:51 -08:00
df31019fd0 fix: modal frame detection — LinkedIn renders Easy Apply in /preload/ frame
LinkedIn renders the Easy Apply modal inside a /preload/ iframe, not the
main document. page.$() searches cross-frame but evaluate() only runs in
the main frame context, causing blank headings/buttons and broken navigation.

- Added findModalFrame() to locate the frame owning the modal dialog
- All evaluate/evaluateHandle calls now use modalFrame instead of page
- findModalButton() and dismissModal() updated to accept frame parameter
- formFiller.fill() receives modalFrame so container scoping works correctly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:20:58 -08:00
0df5eb1b63 debug: add modal structure diagnostics — iframe detection, child tags, html snippet
Logs iframe count, child element tags, and first 500 chars of modal innerHTML
on step 0 to diagnose why buttons/heading aren't being found.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:13:22 -08:00
e6fb380e1c fix: cover letter silent drop, submit verification, autocomplete scoping
- Text inputs matched to cover letter now report as unknown if required,
  instead of silently leaving the field empty
- Submit click now verifies modal closed before reporting success;
  returns 'incomplete' with actionable log if modal stays open
- selectAutocomplete scoped to container (modal) to avoid clicking
  wrong dropdowns from the underlying page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:11:44 -08:00
e62756c6ca fix: robustness improvements — atomic writes, timeouts, shell injection, validation errors
- Atomic JSON writes (write-to-tmp + rename) prevent queue/log corruption
- Per-job (3min) and overall run (45min) timeouts prevent hangs
- execFileSync in ai_answer.mjs prevents shell injection with resume paths
- Validation error detection after form fill in Easy Apply modal
- Config-driven enabled_apply_types (from settings.json)
- isRequired() detects required/aria-required/label * patterns
- getLabel() strips trailing * from required field labels
- Actionable logging on failures ("Action: ..." messages)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:01:53 -08:00
a7ce119bde fix: resilient modal button detection and form filler robustness
easy_apply.mjs:
- findModalButton() uses 3-strategy detection: aria-label exact/substring,
  then exact button text match — survives LinkedIn aria-label changes
- Check order fixed: Next → Review → Submit (submit only when no forward nav)
- All queries scoped to modal + :not([disabled])
- dismissModal() with fallback chain: Dismiss → Close/X → Escape → Discard
- Uses innerText for button text (ignores hidden children)

form_filler.mjs:
- All queries scoped to container (modal when present, page otherwise)
- Radio labels use $$('label') + textContent instead of broken :has-text()
- Autocomplete uses waitForSelector instead of blind 800ms sleep
- EEO selects iterate options directly (selectOption doesn't accept regex)
- Country code check ordered before country to prevent fragile match order

constants.mjs:
- Add AUTOCOMPLETE_WAIT, AUTOCOMPLETE_TIMEOUT
- Remove unused button selectors (now handled inline by findModalButton)

ai_answer.mjs + keywords.mjs:
- Use ANTHROPIC_API_URL constant, claude-sonnet-4-6 model

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:54:44 -08:00
33c50178f4 debug: full-page button scan, modal heading, real progress, validation errors; bump max steps to 20 2026-03-06 17:43:03 +00:00
b79e9d0b9c feat: city autocomplete dropdown, phone country code, checkboxes, EEO selects, location/heard-about answers 2026-03-06 17:39:05 +00:00
8556208405 debug: verbose step-by-step logging in easy_apply modal flow 2026-03-06 17:27:39 +00:00
093b349aad fix: waitForSelector on modal after click instead of fixed 1.5s delay 2026-03-06 17:13:56 +00:00
8c626b5147 debug: log apply elements on skip; use state=attached for waitForSelector 2026-03-06 17:05:57 +00:00
318da35e01 fix: scroll page before waiting for Easy Apply button; extend timeout to 12s 2026-03-06 17:04:27 +00:00
45ad5d1ec9 fix: Easy Apply is an <a> not <button> — update selector; wait for element instead of fixed timeout 2026-03-06 17:01:02 +00:00