From job-matcher
Scheduled pipeline: ingest LinkedIn job alert emails from Gmail, verify each job against a canonical source (employer ATS first), score against the career brief, deliver a report and apply queue to the Obsidian vault, and generate tailored IDML CV drafts for Tier 1 roles. Designed to run headless via claude -p on a launchd schedule, but can be invoked interactively. Trigger phrases: "screen alerts", "screen my job alerts", "run the job alert pipeline", "process linkedin alerts".
How this skill is triggered — by the user, by Claude, or both
Slash command
/job-matcher:screen-alertsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are running the job alert screening pipeline (PRD: docs/PRD-job-alert-pipeline.md).
You are running the job alert screening pipeline (PRD: docs/PRD-job-alert-pipeline.md). Work from the repo root (the directory containing this plugin). Be quietly mechanical: this runs unattended. Never ask the user questions; degrade gracefully and record problems in the run report instead.
bin/run-pipeline.sh splits this pipeline into three phases so the slow,
mechanical resolve step never sits inside an LLM turn (the agent twice
backgrounded it and abandoned the batch, 2026-06-11 and 2026-06-13):
scripts/pipeline-mechanical.sh runs
dedup, ledger upsert, and resolution to a temp resolved.json. No agent.resolved.json and does the
judgment work: scoring, report, ledger updates, apply queue, CV drafts.When invoked for scoring, resolution is already done: read the resolved file, do NOT re-ingest or re-resolve. The phase docs below remain the reference for what each step must accomplish. If this skill is ever run end-to-end in one turn, never background a command: run everything synchronously (rule 8).
data/pipeline.local.json (fall back to data/pipeline.example.json
only to learn the shape; a missing local config is a failure, see rule 7).career_brief_path. If its mtime is older than
brief_stale_days, add a STALE BRIEF warning to the report header.
Also read brief_overrides_path if it exists: private exclusions and
preferences that carry the same authority as the brief and win where they
conflict. The overrides file is local-only; never copy its contents into
committed files, and cite it in reports only as "private exclusion:
".
Also read experience_bank_path if it exists: the candidate's full career
history (including jobs too old or minor for the printed CV) plus academic
CV. It is confirmed-true source material. Use it in scoring (credit
experience the 2-page CV omits) and in CV tailoring (draw real details
from it when rewriting editable sections, especially earlier-career;
surface a role-relevant old job in the candidate's true words). Never
invent beyond what it states; never copy it into committed files.master_cv_path mtime against the generated_from_mtime field in
story_map_path. If the map is missing or stale, regenerate:
python3 scripts/idml-story-map.py --cv <master_cv_path> --out <story_map_path>
and flag the new map for confirmation in the report.gmail_senders: search threads with
from:<sender> newer_than:<gmail_lookback_days>d, paging until exhausted
(pageSize 50)..messages[].plaintextBody with jq into a temp file and run:
python3 scripts/parse-alert-email.py --text < body.txt
Collect all parsed jobs into one array. Count emails_reviewed and
jobs_extracted.python3 scripts/deduplicate-jobs.py.python3 scripts/ledger.py upsert (reads/writes the job array).ledger_status == "new". Jobs previously seen are not
re-reported; count them as deduped.python3 scripts/ledger.py expire --days <ledger_expire_days> to age out
stale rows.python3 scripts/resolve-job.py annotates each job with source_type
"ats" (canonical URL + real posted date) or "unverified".
For each still-unverified job, try the free APIs with company or title
keywords: scripts/search-remotive.sh, search-themuse.sh,
search-jobicy.sh, search-himalayas.sh, search-remoteok.sh, piping
through normalize-jobs.py --source <api>. Fuzzy-match titles (same
spirit as resolve-job.py). On a match: source_type "api", take the API's
URL and posted date, verification_status API_ACTIVE.
Last resort, only for roles that would plausibly score Tier 1 or 2: one
WebSearch per job for "<title>" "<company>" careers OR jobs, looking for
the employer's own careers page or ATS posting. If found, run
scripts/verify-url.sh <url>: VERIFIED -> source_type "web";
EXPIRED -> mark skipped with reason; otherwise source_type stays
"unverified".
Manual evidence folder (done in the SCORE phase, since reading PDFs needs
the model): read any PDFs directly in data/job-ads/ (ignore the
processed/ subfolder). For each, extract company, title, work mode, posted date,
and description from the ad, then match to a ledger row (same fuzzy
company+title spirit as resolve-job.py; check both this batch and
existing rows via ledger.py check). On a match: treat the ad as the
job's full text for scoring, set source_type "manual",
verification_status MANUAL_EVIDENCE, and use any posted date from the ad
for freshness. The user supplied the ad deliberately, so a manual-
evidence job skips the unverified penalty, but it is still labeled
"manual evidence (user-supplied ad)" in the report with the citation.
If a PDF matches no known job, score it as a new lead anyway and note
where it came from. After processing, move the file into data/job-ads/processed/.
Precedence when the matched job already has a disposition (a duplicate drop is a user mistake to absorb, not an override):
data/job-ads/processed/ so it is consumed once.Freshness rule: jobs whose canonical posted_date is older than
freshness_days are skipped with reason "stale (posted )".
Jobs with no canonical date stay unverified: labeled, ranked below
verified jobs, never dropped. Manual-evidence jobs without a date in
the ad count as fresh: the user just printed them.
Use the Phase 6 rubric from the match-jobs skill, unchanged: skills 0-30, seniority 0-20, sector 0-20, work mode 0-15, culture/values 0-10, recency 0-5. Tiers: 80-100 Tier 1 (strong), 60-79 Tier 2 (good), 40-59 Tier 3 (growth).
Score on the full job description, not the title. Each resolved job
carries description_text (the resolver fetches the complete ad: Greenhouse
via content=true, Workday via its detail endpoint, Ashby/Lever/Workable from
the listing). Read it and score skills, sector, culture, and the
product-mandate guard against what the ad actually says. A job flagged
thin_description: true (under 200 characters resolved) could not have its
full ad fetched: score it conservatively on title and metadata, and say so
in the report ("scored on title only, full ad not retrievable"). Do not let a
thin description silently depress a verified role without noting why.
Apply, in this order:
brief_overrides_path are
dealbreakers; quiet preferences adjust scores. In reports, cite only
"private exclusion: ".For every scored job, update the ledger:
python3 scripts/ledger.py set-status --key <ledger_key> --status reported --score N --tier N --canonical-url URL --source-type TYPE --posted-date DATE --location "<city/country>" --work-mode <remote|hybrid|onsite>
(location and work-mode come from the resolved job; they surface in the apply queue.)
(or --status skipped --note "<reason>" for dealbreakers/stale).
<reports_dir>/YYYY-MM-DD-job-screen.md and copy
it to <vault_reports_dir>/ (create directories as needed).python3 scripts/render-apply-queue.py. It reads Tier 1 reported rows from the ledger and writes <vault_apply_queue> in the exact ## {Title}: {Company} format the Recall daily note parses. Do NOT hand-write the queue; the script owns its format. When the user marks a role applied or skipped, update the ledger with ledger.py set-status and re-run the renderer.Only for tiers in cv_tiers (default: Tier 1 only). Drafts only; never
submit anything.
Read the story map (story_map_path) and the master CV. Editable stories
and their roles come from the map; never touch stories outside the
whitelist.
Edit for the role's vibe, not just keywords. Before writing anything, read the full ad and write yourself a short register map: its vocabulary (the actual nouns it uses for people, work, and outcomes), its themes, what it values, its tone. A museum's register is audience, visitors, cultural sensitivity, public engagement, accessibility; a fintech's is customers, risk, compliance, scale. Then rewrite the candidate's TRUE experience to speak in that register. Tailoring is editorial, not literal: the timid failure mode (observed 2026-06-13) is hugging the master CV's wording and changing almost nothing.
Concretely, across profile, core strengths, methods/tools, AND the most role-relevant experience bullets (the page 3 letter gets the v1 placeholder, not a tailored letter), do all of:
Content rules: every claim is TRUE (traceable to the master CV, the career
brief, or the candidate's confirmed experience), expressed in the role's
language;
British or American spelling follows the posting; tone matches the master
CV; no em dashes. Growth budget: framing lines +cv_length_budget_pct%
(default 10); experience bullets up to +20% (pass --budget-pct 20),
since the ad's vocabulary sometimes needs the room.
Apply: python3 scripts/idml-apply.py --cv <master_cv_path> --map <story_map_path> --edits <edits.json> --out <cv_drafts_dir>/CV-rabourn-<company-slug>-<YYYY-MM-DD>.idml
The script enforces the whitelist, length budget, XML well-formedness,
and IDML packaging rules; treat its failure as a per-job failure, note it
in the report, and continue with other jobs.
Write a plain text diff summary next to each draft (same name, .changes.txt): what changed, why, and a reminder to check for overset text in InDesign.
Write a job-ad companion file next to each draft, named
CV-rabourn-<company-slug>-<date>.job-ad.md, with TWO clearly separated,
clearly labelled parts:
"<ad term>" -> <where it is true in her background> (drawing on the
brief, master CV, and experience bank). Follow it with a short
"### Not claimed / gaps" list of notable ad requirements she does
not meet, so the picture is honest.Update the ledger with --cv-path and add the draft to the apply queue
entry.
python3 scripts/ledger.py log-run (JSON on stdin).logs/pipeline.log:
<ISO date> | emails N | extracted N | new N | verified N | tier1 N | cvs N | <outcome>.npx claudepluginhub rabourn/job-matcher --plugin job-matcherCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.