Skip to main content
Glama

job-resume-agent

An evidence-grounded resume tailoring system. Given a job description detected in the browser and a structured experience bank the user has curated, it produces an ATS-readable LaTeX/PDF resume where every bullet is traceable to a piece of user-provided evidence. Any wording the model inferred, generalized, or reworded is visibly flagged and must be approved by the user before export.

The product refuses to fabricate. If a required skill or achievement is not backed by evidence, it says so instead of inventing one.

Status: working proof of concept. The repository includes an MCP server, evidence linker, safe LaTeX renderer, CLI, and 65 tests. It does not yet include the Chrome extension.

How it works: the model judges, the server enforces

There is no keyword extractor in this project, by design. An MCP server is called by a language model, so the reasoning should happen in the model — which can tell that "Redis, Kafka, DynamoDB, low-latency" means distributed systems, and that "barista-made espresso" is a perk, not a requirement. A token matcher can do neither, and an earlier version of this repo proved it: it read a Verkada posting as having 40 "requirements", 17 of which were hyphenated boilerplate like well-being and team-building. Coverage numbers computed against that denominator were meaningless.

So the split is:

The calling LLM decides

The server enforces

Which requirements the posting actually states

That cited bullet_ids exist in the bank

Which experience speaks to them

That coverage claims have surviving citations

What order content appears in

That rephrasings introduce no new numbers or proper nouns

How a bullet is worded for this posting

That the PDF really is one page (by compiling it)

Which skills to emphasise first

That Education and every bank skill are always present

That a half-empty page is reported, not shipped silently

The model gets full latitude over selection and wording, and zero latitude over facts. That is what makes "let the model polish it" safe.

Related MCP server: mcp-overleaf-server

MCP tool workflow

  1. load_bank(path) — returns the full bank catalog: every entry, every bullet, every bullet_id. The agent reads this; it is the only source of resume content.

  2. set_job_description(raw_text, ...) — accepts raw scraped HTML and returns clean plain text plus a job_id. It deliberately does not extract requirements — that is the agent's job.

  3. analyze_fit(requirements, job_id) — the agent submits the requirements it found and the bullet_ids/skills backing each verdict. The server validates: unresolvable citations are stripped, and any covered/partial verdict left with no evidence is downgraded to a gap and reported in corrections. Returns flat coverage, must-have-weighted coverage, and a recommendation.

  4. tailor_resume(selection, job_id) — the agent picks entries, bullets, and order. Optional rewritten_text per bullet is classified by the evidence linker; new numbers or new proper nouns make it unsupported and block export. Unknown ids are errors, not silent skips.

  5. get_draft(draft_id) — retrieve a draft.

  6. export_draft(draft_id, output_dir) — renders .tex and compiles with pdflatex, counting real pages. On overflow the least-important trailing bullets are dropped (never reworded) and it recompiles. Returns page_count, pdf_path, dropped_bullet_ids, and fill_ratio.

One page is a ceiling, not a target

Enforcing "at most one page" alone let thin drafts ship as half-empty pages, which reads to a recruiter as a thin candidate rather than a focused one. Three of the five test resumes were using only ~57% of the page. Two changes fixed it:

  • Skills are additive. The agent's skills selection now sets emphasis order, not membership — every bank skill is listed regardless. All of them are evidence-backed, and a posting that doesn't mention Julia is not a posting that penalises knowing Julia.

  • Sparseness is reported. tailor_resume returns bank_usage and warns when the selection uses under 60% of available bullets; export_draft measures the compiled PDF's real ink extent and returns fill_ratio, warning below 0.75. The agent can then add content and re-tailor.

Result across the five test postings: 57–83% fill → 78–85%, still one page, with no trimming required.

What the guarantees look like in practice

Validated against five real 2027 internship postings (GlossGenius, Verkada ×2, Compeer, BTI360) using a real experience bank:

  • All five export to genuine one-page PDFs with Education and Skills present.

  • Verkada Mobile correctly reports 3 must-have gaps (Swift, Kotlin, mobile architecture) and refuses to claim them — the bank has no iOS evidence.

  • An agent asserting verdict: "covered" for Swift with no citation gets coverage 0.0 and an explicit correction.

  • An agent rewriting a bullet to "Shipped a SwiftUI iOS app to 50,000 users" gets unsupported, and export_draft refuses.

  • Inflating a real metric (50+500+) is caught the same way.

  • accept_inferred: true cannot launder an unsupported bullet.

Quickstart

Requires Python 3.11+ and a TeX distribution with pdflatex on PATH (MiKTeX, TeX Live) for the one-page gate. Without pdflatex, exports still work but skip page-count enforcement (a warning is returned).

python -m pip install -e ".[dev]"

# Inspect the bank the way an agent sees it (bullet_ids and all)
resume-agent catalog --bank docs/example-experience-bank.yaml

# Apply a selection JSON (normally authored by the agent) and compile
resume-agent tailor --bank docs/example-experience-bank.yaml \
  --jd path/to/job.txt --selection selection.json --export --out ./out

Run the MCP server over stdio:

resume-agent serve

Register it with Claude Code:

claude mcp add resume-agent -- resume-agent serve

Set RESUME_AGENT_BANK_PATH to a local bank with your own information. Keep your private bank and outputs under .private/ — that directory is git-ignored. The committed example bank uses placeholder contact details and is safe to publish.


Why this exists

Most "AI resume tailoring" tools optimize for keyword match and paragraph fluency at the cost of factual integrity. They will happily add "led a team of 10" or "improved latency by 40%" when the user never claimed either. That is a career risk for the user and a trust problem for the recipient.

This project takes the opposite stance: the model is a selector and rephraser over the user's own evidence, not a generator of achievements. Tailoring means selecting, reordering, and rephrasing — never inventing. Crucially, that constraint is enforced mechanically by the server rather than requested politely in a prompt. See docs/product-requirements.md for the acceptance criteria that operationalize this.

Product shape

Three components, loosely coupled:

  1. Chrome extension. Detects when the active tab is a job description on a supported site (LinkedIn, Greenhouse, Lever, Ashby, Workday, plain text). Offers a Generate Tailored Resume action. Sends the JD to the local backend; opens a review UI when the draft is ready.

  2. Local backend + web app. Owns the experience bank, the tailoring pipeline, the evidence matrix, and the LaTeX/PDF renderer. Runs on localhost by default. Ships a review UI: side-by-side diff vs. base resume, evidence links per bullet, keyword coverage heatmap, unsupported-claim warnings, approve/reject/edit per bullet.

  3. Provider adapters (BYO account). The user connects their own OpenAI, Anthropic, or local (Ollama) account. We do not proxy through our servers and, in local-first mode, we do not store provider keys in a hosted DB — see docs/security-and-privacy.md.

The public reference LaTeX template lives at templates/resume.template.tex and is used as the visual/structural target for the renderer. Personal identifiers (name, email, links, phone, citizenship) are declared as macros in the template so they can be overridden by a private overlay. The maintainer's own resume source, which contains real PII, lives at .private/alan-resume-source.tex and is git-ignored — treat it as the canonical worked example of what a filled-in template looks like, not as a file that ships with the repository. A minimal experience bank derived from that private source is at docs/example-experience-bank.yaml.

Non-goals (v1)

  • No fabrication assist. No "suggest a bullet you might have done." No "fill in a plausible metric."

  • No hosted multi-tenant SaaS. MVP is local-first. Cloud sync is deferred (see roadmap).

  • No auto-apply. We do not submit applications, message recruiters, or write cover letters. Out of scope.

  • No scraping of gated job boards. The extension reads the currently open page in the user's own browser session; it does not crawl.

  • No storage of provider API keys server-side in v1. Keys live in OS keychain or extension local storage.

  • No résumé "score." Numeric ATS scores are pseudoscience without the ATS in the loop. We show keyword coverage and evidence coverage, not a grade.

  • No LinkedIn scraping to seed the experience bank. Import is manual or from user-uploaded PDF/LaTeX only.

Documentation map

File

Purpose

docs/product-requirements.md

User stories, acceptance criteria, non-goals, UX principles

docs/technical-architecture.md

System diagram, components, provider adapter interface, LaTeX validation

docs/data-model.md

Experience bank schema, evidence links, tailored-resume artifact schema

docs/security-and-privacy.md

Threat model, extension permissions rationale, key storage, data flows

docs/roadmap.md

Phased build plan from M0 (skeleton) through M5 (multi-user cloud)

docs/adr/001-local-first-provider-agnostic.md

Foundational architecture decision record

docs/example-experience-bank.yaml

Sample bank derived only from the maintainer's private .private/alan-resume-source.tex

CONTRIBUTING.md

Development principles, coding standards, review posture

Repository layout

job-resume-agent/
├── src/resume_agent/
│   ├── mcp_server.py             # the six MCP tools; agent-facing contract
│   ├── catalog.py                # renders the bank for the agent to read
│   ├── jd_clean.py               # HTML -> clean posting text
│   ├── fit.py                    # validates the agent's coverage claims
│   ├── tailor.py                 # assembles a draft from the agent's selection
│   ├── evidence_linker.py        # classifies rephrasings against evidence
│   ├── latex_renderer.py         # safe LaTeX rendering (escaping, sections)
│   ├── export.py                 # export gate + real pdflatex one-page loop
│   ├── models.py                 # Pydantic schemas
│   ├── state.py                  # process-local session store
│   └── cli.py                    # serve / catalog / tailor
├── templates/resume.template.tex # public, PII-free reference template
├── tests/                        # 65 tests, incl. adversarial fabrication tests
├── docs/
└── .private/                     # git-ignored: real bank, resumes, outputs

The Chrome extension is not built yet; the MCP tool boundary is designed so it can call the same tools the agent does.

Assumptions on record

These are the reasonable assumptions this design makes without asking the user. Change them by opening an ADR.

  1. Local-first, single-user MVP. Cloud/multi-user is a v2 concern. See docs/adr/001-local-first-provider-agnostic.md.

  2. BYO provider account. We do not front an LLM as a service. The user picks OpenAI, Anthropic, or Ollama; we call it with their credentials.

  3. LaTeX is the source of truth for the tailored artifact. PDF is a build product. The user's base template (templates/resume.template.tex, optionally overlaid by a private file such as .private/alan-resume-source.tex) defines the layout envelope.

  4. Python backend, TypeScript extension + web UI. Matches the ML/LaTeX ecosystem on the backend and the browser platform on the frontend.

  5. Chrome first. Firefox/Safari can follow via MV3 compatibility once the surface is stable.

  6. The bank is user-owned. The user can export the full bank as YAML at any time and delete the local DB with one command.

  7. Every generated bullet carries provenance. No bullet reaches the PDF without at least one evidence_id reference in the tailored-resume artifact.

Open questions

Tracked in docs/roadmap.md#open-questions.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    Tailors LaTeX résumés, CVs, and cover letters to job descriptions by injecting truthfully-selected content from a master CV, compiling PDFs, and logging applications.
    11
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables users to tailor a one-page LaTeX CV to a job posting by fetching relevant facts from a YAML file, rendering a LaTeX document from id-based selections, and compiling it to PDF, without wasting tokens on repetitive CV reads or compiler logs.
    MIT