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>
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>
Prevents OpenClaw from relaying full report as plain-text duplicate.
sendTelegram() handles formatted delivery, stdout just says "sent".
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>
When a batch completes but scores aren't written back (collection
error), jobs get stuck with filter_batch_id set and never re-submitted.
Now checks: if no filter_state.json exists (no batch in flight) but
jobs have batch markers without scores, clear them so they get
re-submitted on the next run.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously status.mjs relied on OpenClaw relaying stdout, which sent
as plain text. Now sends via sendTelegram() directly when Telegram
config is present, matching how all other scripts send notifications.
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 are no longer stored in settings.json. The applier
finds auth connections by domain (linkedin.com, wellfound.com) at
runtime via the Kernel SDK. Updated SKILL.md, README.md, and bumped
to 0.1.4.
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>
The ClawHub scanner flags systemPrompt variables in lib/*.mjs as
potential prompt injection. These are legitimate Claude API prompts
for job scoring, answer generation, and keyword generation. Added
explicit note clarifying their purpose.
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>
The stuck/incomplete retry logic referenced maxRetries which was only
defined in main() scope, not in handleResult(). Compute it locally.
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>
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>