Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Jobius

The CLI is still called job-agent — see Setup below.

See docs/ARCHITECTURE.md for the pipeline design, the LLM cost-control funnel (gate/shrink/dedupe), and real measured token/cost numbers from a production run.

Personal job aggregator + A-grade resume-tailoring pipeline for one interest cluster: SRE, Platform, Infrastructure, Cloud, DevOps, Forward Deployed Engineer, and AI/ML Infrastructure — treated as one family. Runs entirely on your machine. No hosted service, no auth walls, no auto-apply.

Pulls open roles directly from company ATS boards (Greenhouse, Lever, Ashby), shortlists US-based roles across that whole cluster (keyword net + a cheap classifier for semantic siblings keywords miss), scores every match against your resume (permissive — down to a 10% match), and generates a genuinely differentiated tailored resume + honest gap report per job. You review and apply yourself; this tool never submits anything on your behalf.

Setup

cd job-builder
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -e .          # installs the `job-agent` command
cp -n .env.example .env   # already done if you're reading this after the initial build

Open .env and paste in your Anthropic key:

ANTHROPIC_API_KEY=sk-ant-...

Get one at https://console.anthropic.com/. Everything else in .env has a sensible default — see .env.example for what's overridable (score/tailor model names, embeddings provider, output directory, H-1B data source).

Optional: LaTeX for nicer PDFs

If pdflatex or tectonic is on your PATH, tailored resumes render through LaTeX (cleanest ATS-safe typesetting). Otherwise the pipeline falls back automatically to a pure-Python reportlab renderer — no system dependencies required, works out of the box.

Usage

# One-time: parse your master resume into resume_facts.json (the source of truth for tailoring)
job-agent ingest --resume ~/path/to/master_resume.pdf

# Full pipeline: fetch every seed company's board, filter, score, tailor, export
job-agent run --resume ~/path/to/master_resume.pdf

# Or run stages independently:
job-agent fetch                      # pull all ATS boards, dedupe into jobs.db
job-agent fetch --refresh-companies  # also re-pull the full 15k+ company list (slow; weekly)
job-agent filter                     # shortlist by interest cluster + US-location rules
job-agent score                      # score every in-scope job (reuses resume_facts.json)
job-agent score --cosine-top-pct 30  # only Haiku-score the top 30% of the batch by cosine similarity (default 50)
job-agent tailor                     # auto-tailor: status=pending jobs OR score >= 70 (default gate)
job-agent tailor --min-score 50      # lower/raise the auto-tailor score gate
job-agent tailor --id greenhouse:stripe:1234  # tailor one job on demand, bypassing the gate entirely
job-agent tailor --force             # re-tailor jobs that already have output
job-agent tailor --sponsors-only     # only tailor jobs annotated sponsors_h1b=yes
job-agent tailor --max-level senior  # skip jobs inferred above this level (default: all levels)
job-agent status                     # list scored jobs
job-agent status --filter pending    # list only jobs in one status
job-agent status --new-since 2026-07-01T00:00:00+00:00   # jobs first seen after a timestamp
job-agent status --set pending --id greenhouse:stripe:1234
job-agent status --set applied --id greenhouse:stripe:1234
job-agent serve                      # optional: dashboard status clicks apply instantly (see below)

All commands accept --out DIR. It defaults to your iCloud Drive:

~/Library/Mobile Documents/com~apple~CloudDocs/job-agent/

so jobs.db, jobs.csv, jobs.json, dashboard.html, and every {company}__{role}/ tailored-resume folder sync automatically to Finder, the Mac file-upload picker, and the Files app on your iPhone. Override with --out (or JOB_AGENT_OUT in .env) if you'd rather use Google Drive, Dropbox, or a plain local folder.

Re-running fetch only adds new postings (dedupe on {ats}:{slug}:{job_id}). Re-running tailor skips jobs that already have output unless you pass --force.

Job lifecycle (the state machine)

status is one of: newpendingapplied, or new/pendingignored. The system alone can move anything to closed. Rules, enforced exactly in job_agent/db.py:

  1. A posting seen for the first time is inserted as new.
  2. A posting still live on a refresh has its mutable fields (description, salary, location, last_seen_at) updated — but status is never touched. This is the guardrail: a job you've marked pending/applied/ignored can never silently revert to new just because fetch ran again.
  3. A posting that disappears from its company's board (filled, pulled, or renamed) is set to closedunless it's already applied, which is kept forever so your applied history survives delisting. Rows are never deleted, ever — closed still means "still in jobs.db/jobs.csv/jobs.json, just no longer an option to apply to."

Only new and pending jobs are eligible for job-agent tailor. job-agent score only considers new jobs, and skips ones whose job description hasn't changed since it was last scored (tracked via a content hash) — so re-running score repeatedly doesn't re-spend on unchanged JDs, but a JD that got edited does get rescored even if the job was scored before.

Nothing "resets" on fetch — there's no full-list wipe, it's always an incremental diff against jobs.db: new postings come in as new, filled postings flip to closed, everything else (including your pending/applied/ignored calls) is left alone. Run fetchfilterscoretailor as often as you like, e.g. daily.

"New since last check" (the dashboard's NEW badge, and status --new-since) is measured against the fetch run before the most recent one — so after you run fetch once, jobs that came in on that run are flagged NEW until the next fetch completes.

The apply loop (this is the actual workflow)

  1. Open dashboard.html (from Finder, or the Files app on your phone) — this is your primary interface: ranked highest-match first, California/Remote-US roles pinned to the top, with New / Pending / Applied / Ignored tabs (New + Pending shown by default) and a NEW badge on anything that showed up since your last fetch.
  2. Tap Apply — opens the company's own application form (apply_url), not a resume page.
  3. Triage/review anywhere (phone included). Use the Set status dropdown on a row to queue it (pending) or drop it (ignored).
  4. Do the actual submit on your Mac:
    • Hit the company form's upload-resume button.
    • The file picker opens to iCloud Drive.
    • Pick that row's Resume PDF — named FirstName__Role__Company.pdf (e.g. Kaushik__SRE__Stripe.pdf) so it's identifiable at a glance in the picker.
    • Fill in the remaining fields, submit.
  5. Mark it applied via the dashboard's Set status → Mark applied. An applied job is kept forever, even after the posting is delisted.

There is no phone-side auto-fill or auto-submit by design — you do the actual submission.

Making Set status update instantly (job-agent serve)

dashboard.html is a static file — browser JS can't write to jobs.db (or any local file) on its own, that's a hard browser sandboxing rule, not a choice. By default, clicking a status option copies the equivalent job-agent status --set ... --id ... command to your clipboard so you can paste it into Terminal.

To skip the copy-paste, run this in a terminal on your Mac while you're doing applications:

job-agent serve   # binds to 127.0.0.1:8420 only — never reachable from the network

With it running, the dashboard's status dropdown updates jobs.db directly (and the dashboard re-renders that row immediately) — no clipboard, no Terminal round-trip. It's entirely optional: if the server isn't running, or you're triaging from your phone, the dashboard silently falls back to the copy-command flow. There's no auth on this — the data behind it is your own job-application statuses, not credentials, and it's opt-in/local-only, so the realistic worst case of a stray request is one job's status flipping. --port N to use something other than 8420.

The same server also powers the dashboard's Tailor button. job-agent tailor (no --id) only auto-tailors jobs you've explicitly moved to pending, or that scored ≥ 70 — everything else shows a Tailor button instead of a Resume link. Clicking it hits this server's POST /tailor, tailors that one job on demand (bypassing the score gate, since you clicked it), and opens the PDF when it's done (a minute or two). If the server isn't running, the button shows the equivalent job-agent tailor --id <job_id> command to run manually instead.

If dashboard.html doesn't load jobs.json

Some browsers restrict fetch() of local sibling files when opening file:// URLs directly (notably Safari, and Chrome without extra flags). If the table stays empty:

cd ~/Library/Mobile\ Documents/com~apple~CloudDocs/job-agent/
python3 -m http.server
# then open http://localhost:8000/dashboard.html

Chrome/Firefox desktop usually load it fine directly from file:// without a server.

Pipeline stages

  1. ingest — parses your resume (PDF/DOCX/MD) and has Claude break it into atomic, fact_id-addressable facts (experience/project/skill/metric/education/certification), cached to resume_facts.json. This is the single source of truth every tailored resume traces back to.

  2. fetch — async-pulls every company's public ATS JSON feed (Greenhouse, Lever, Ashby), normalizes to one schema, dedupes into jobs.db. Dead/renamed company slugs are skipped and logged, never abort the run. Concurrency is capped per ATS (Ashby: 5, others: 20), with exponential backoff and a best-effort robots.txt Crawl-delay honor.

  3. filter — shortlists by interest cluster, not a narrow title match: SRE/reliability/ production, platform/infra/cloud engineering, devops/systems, forward-deployed/deployment/ technical-solutions engineering, and AI/ML infrastructure are all treated as one family. Two-stage match: (a) a fast keyword net (data/filters.yaml's title_include minus title_exclude), then (b) titles that don't keyword-match but look plausibly technical (classifier_prefilter_include) go through a cheap Claude Haiku classifier that catches semantic siblings keywords miss (e.g. "Compute Platform Engineer", "Deployment Strategist") — titles that don't even pass the prefilter never reach the classifier, so Haiku isn't spent on obviously-irrelevant postings. Also infers a level (new-grad/mid/ senior/staff) per title via regex heuristic — free, no LLM call — stored but never hard-filters unless you pass --max-level. US locations only, same structured-location logic as before (Lever's ISO country code trusted directly; Greenhouse/Ashby fall back to state-name/abbreviation matching; bare "Remote" kept unless the JD body names a non-US region).

  4. score — cost/quality-aligned: the funnel is strictly cheap → expensive, and nothing spends an LLM call that a free/cheap step can reject first.

    • The job description is trimmed to its requirements/responsibilities substance (job_agent/jd_trim.py) before it goes anywhere near a model — cuts the marketing padding (About Us, benefits, DEI statements) that real JDs are mostly made of, and hard- caps the character count. Falls back to the full text (still capped) if no recognizable section heading is found — under-trimming is a cheaper mistake than losing a real requirement.
    • A cheap offline TF-IDF cosine pre-pass (swap providers via EMBEDDINGS_PROVIDER in .env) ranks every in-scope status=new job against the trimmed JD, no LLM involved.
    • Claude Haiku only scores the top --cosine-top-pct of that batch by cosine similarity (default 50%, --cosine-top-pct 100 disables the gate) — the bottom half a cheap pre-pass already ranks unpromising never gets a Haiku call at all.
    • Cached by (jd_hash, resume_hash): if an identical trimmed JD (same text, e.g. a repost or the same listing syndicated across boards) was already scored against this resume — under this job id or any other — the cached score/matched-skills/missing- requirements/rationale is reused directly, zero new tokens. Also skipped entirely (no cache lookup even needed) if this specific job's JD is byte-identical to what it was scored against last time.
    • Everything ≥ --min-score (default 10) surfaces downstream — deliberately permissive so stretch roles show up.
  5. tailor — lazy, not eager: job-agent tailor with no --id only auto-tailors jobs you've explicitly moved to pending, or that scored ≥ --min-score (default 70) — it will not spend tokens tailoring hundreds of jobs you'll never open. Everything else gets a Tailor button on the dashboard instead of a Resume link, which tailors that one job on demand via job-agent tailor --id <job_id> (or a click, if job-agent serve is running — see above), bypassing the score gate entirely since an explicit click is its own signal of intent.

    For every job selected (auto or on-demand, and ≤ --max-level if set), Claude Sonnet writes an A-grade resume — not a keyword-matched clone — using an exact, fixed system prompt: it extracts the JD's hero requirement, identifies your single strongest differentiator for that specific role, aggressively surfaces relevant real depth (multi-cloud, specific platforms actually used) that might be buried in the master, reorders for a 7-second read, budgets for a strict one page, and self-critiques its own draft before finalizing. Output is Markdown plus a ---FACTMAP----delimited JSON citing fact_ids. The model is explicitly told not to write its own contact header — but that instruction is a nicety, not the guarantee.

    Cost is saved by tailoring fewer jobs, never by tailoring them worse — the model choice (Sonnet) and the exact prompt's self-critique step are never downgraded or skipped to cut tokens. The one place cost is actually saved without touching quality: the full four-call bundle (resume write, gap analysis, verification re-score, and the surfaced-vs-genuine-gap compare) is cached by (jd_hash, resume_hash) exactly like scoring — a repost or a re-run against an identical trimmed JD reuses every one of those calls' outputs outright.

    The real guarantees on a cache miss are what the tool does next, unconditionally:

    • Tool-owned assembly: discard everything in the model's output before the first recognized Summary/Skills heading — wholesale, regardless of what's there (a stray name line, [Full Name], a title line) — then prepend the one true header built from resume_facts.json's contact info. This is what actually prevents a hallucinated/duplicated header from ever reaching a PDF, not pattern-matching what the model might have written.
    • Provenance guardrail: any bullet or skill introducing a proper-noun/tech term absent from your source resume text is dropped before rendering — logged in tailoring_notes.md. (The FACTMAP's own bullet→fact_id shape varies model to model, so it's recorded as a transparency note rather than trusted as the sole gate — the token-level check against your actual resume text is what enforces zero-fabrication.)
    • Resume lint gate (hard fail, never ships a broken PDF): rejects bracketed placeholder tokens ([email], [Full Name], …), literal template strings, the candidate's name appearing anywhere other than exactly once in the header, and a heading glued directly to body text with no line break.
    • One-page hard gate: renders, and if the PDF is more than one page, drops the lowest-priority bullet (from whichever section has the most) and re-renders — up to 3 attempts — before giving up and failing the job rather than shipping an overflowing resume.
    • A separate call (our own prompt, not the exact one above) does gap analysis; a transient hiccup there degrades gracefully (retries once, then ships without it) rather than throwing away an already-successful, already-rendered PDF.

    Then, per job:

    • tailored_resume.pdf — reordered/reworded/surfaced, never fabricated, always ≤ 1 page.
    • gap_report.md — real JD requirements you don't meet, split Learnable/Adjacent vs Hard Blocker, plus a one-line verdict.
    • tailoring_notes.md — emphasized/de-emphasized notes, the FACTMAP's cited fact_ids, and anything the provenance guardrail dropped before rendering.
    • verification.md — the tailored resume is re-scored against the JD; missing requirements are split into keywords you actually have but didn't surface (fix these) vs genuine gaps (real, don't fake them). Flags a warning if the tailored score is < 60.

    The tailoring step can only surface what's in resume_facts.json — if your master resume doesn't capture your full multi-cloud depth or the specific AI/model platforms you've used, the generator can't invent them. The master resume is the ceiling on every tailored resume; if verification.md keeps flagging real skills you have that aren't surfacing, the fix is usually enriching the master, then re-running ingest --force. A job that fails the lint gate or the one-page gate (after 3 tighten attempts) is skipped and logged rather than shipped broken — re-run tailor --force to retry it.

  6. export — writes jobs.csv, jobs.json, and dashboard.html (self-contained, job data embedded directly in the HTML so it works from plain file:// — Finder double-click, Files app on iPhone — no server, no build step).

Company universe

data/seed_companies.json ships ~55 hand-picked, live-verified Greenhouse/Lever/Ashby boards so job-agent run works immediately with zero setup. Pass --refresh-companies to fetch/run to additionally pull the full per-ATS slug lists (~16k companies total) from Feashliaa/job-board-aggregator, cached to data/companies_cache.json — re-run weekly to pick up new boards. That's a much longer run (thousands of HTTP calls even at 20x concurrency); expect it to take a while. Add your own companies any time by editing data/seed_companies.json ({"slug": "...", "ats": "greenhouse|lever|ashby", "name": "..."}).

H-1B sponsorship signal

data/h1b_sponsors.json is a small hand-seeded lookup (well-known tech employers only, illustrative, not authoritative). Every job gets annotated sponsors_h1b: yes/no/unknown during filter — by default this never filters anything out, it's just a dashboard column. job-agent tailor --sponsors-only is the one place it gates behavior (skips tailoring for non-sponsors). For real accuracy, replace data/h1b_sponsors.json (or point H1B_DATA_PATH in .env at your own file) with an export from public LCA/PERM disclosure data such as h1bdata.info or the DOL's own disclosure files — format is {"normalized_company_name": "yes"|"no"} (normalize = lowercase, strip Inc/Corp/LLC, strip non-alphanumerics).

Non-goals

No web UI beyond the static dashboard. No auto-apply, no form-filling, no browser automation. No scraping LinkedIn/Indeed/Glassdoor. No fabricated resume content, ever — anything the tailoring step can't trace to a real fact_id gets dropped, not invented.

License

MIT — see LICENSE.

About

AI-powered job aggregator + resume-tailoring pipeline for SRE/Platform/Infra/Cloud/DevOps/Forward-Deployed/AI-ML-Infra roles

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages