- 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>
Stuck and incomplete jobs now get retried up to max_retries (default 2)
before being permanently marked. Honeypots are still permanent.
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>
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>
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>
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>
When --telegram, send directly and don't print. Otherwise keep * in
console output so agents capturing stdout can relay with formatting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
status.mjs --telegram sends the report to Telegram with proper bold formatting.
Console output strips markdown. Queue total now accounts for duplicates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>
- Removed Telegram notification on batch submit (only notify on collect
when results are ready)
- After collecting, immediately submit remaining unscored jobs in the
same run instead of waiting for next cron cycle
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
logStream.end() callback wasn't firing reliably, leaving processes hanging.
process.exit() is synchronous and forces exit regardless of open handles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
- 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>
Writes all console output to a log file from inside the process so
logs are always available regardless of how the process is launched.
Fixes invisible output when claw redirects stdout to a file that
gets overwritten by a second lock-blocked attempt.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
- 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>