For the past four months I've been quietly obsessed with a single question: what actually makes a resume win? So I built a machine to answer it, and I've run more than 300 head-to-head A/B tests since. Most resume tools are a thesaurus with a subscription. They swap verbs, sprinkle keywords, and hand you something that reads well until an actual recruiter, or an actual applicant-tracking system, gets to it. I wanted the opposite: a system that is adversarial to itself. It writes a draft, then tries hard to reject it, and only ships when the draft survives a tournament and it self-learns on every run from previous rejections.
A/B testing your resume produces one of your best resumes. Not because any single rewrite is magic, but because scoring many variants against a consistent panel and keeping the winner is a search, and searching beats guessing.
The pipeline
The whole thing is a directed graph. Each node does one job, and the edges carry a typed state object from stage to stage. The back-edges are the interesting part. When a draft fails a gate (bullets too long, an identity field got mutated, the LaTeX won't compile, or the panel score is too low), the state routes back to the writer with structured notes about what to fix.
StateGraph. The dashed edge is the revision loop: the Aggregator sends failing drafts back to the Writer, up to four rounds. Nodes marked ↩ (length, identity, compile) have their own tighter retry budgets.The front half is an extraction spine that runs once: parse the resume into a locked identity ledger plus editable work history, analyze the job description into a weighted skill vector, find honest gaps to reframe, and generate a JD-tailored skill block. Then the revision loop begins: the writer rewrites bullets, the draft is rendered into the original LaTeX template, compiled with Tectonic, and handed to the recruiter panel.
A design rule I kept coming back to: the writer never touches your name, contact details, employers, titles, or dates. Those are locked at parse time and injected verbatim by the renderer. An identity check verifies nothing leaked. The model gets to be creative with framing, never with facts about who you are.
Models are chosen per role. By default the writer is Claude Sonnet 5 and the gap analyzer is Claude Opus 5 (both with medium reasoning effort), while parsing and scoring use a fast, cheap model. The whole pipeline streams its progress (parse, write, compile, score), so you can watch a draft climb toward the bar in real time.
Four recruiters who disagree on purpose
A single scorer is easy to game. So the panel is four personas, run in parallel, each with a different lens and a different failure mode they're paranoid about:
- ATS Matcher - keyword coverage and parseability. Does the resume mirror the JD's weighted vocabulary?
- Hiring Manager - outcomes, quantification, seniority fit, narrative coherence.
- Technical Screener - stack coherence and whether the claims would survive a phone screen.
- Skeptic - the fabrication guard. It's the only persona that also sees your source resume, so it can call out claims the draft can't defend.
Each persona scores five rubric dimensions (0–100): keyword match, impact quality, coherence, plausibility, and formatting. Those collapse into a per-persona composite, and the four composites average into the aggregate. A draft passes only if the aggregate clears the threshold and the Skeptic's plausibility clears a floor, so you can't buy a pass with keyword stuffing the Skeptic would veto.
When the writer plateaus and produces a byte-identical draft, the panel is skipped and the previous scores are reused. Re-scoring an unchanged resume is four wasted model calls, and the loop is cheap where it can afford to be.
What 'good' looks like
The revision loop is the benchmark that matters. A first draft rarely clears the bar; each round folds the panel's ranked notes back into the writer, and the composite climbs. The curve below traces a representative run from a weak opener to a comfortable pass.
The rubric weights decide what the climb optimizes for. Keyword match carries the most weight because it's the gate an ATS actually enforces; plausibility and formatting are deliberately lighter so the writer isn't punished for confident, well-supported reframing.
A tournament picks your best resume
Scoring one resume tells you whether it passes. Scoring many and keeping the winner tells you which one to send. So the A/B mode seeds a bracket of 4, 8, or 16 resumes and plays single-elimination matches judged by a configurable panel. It's fully deterministic. The same seed produces the same bracket and the same result, every time.
Each match is scored per judge, per read. "Best of 3" takes three reads and averages them. More reads means less variance, the same reason you don't decide a chess match on one blitz game. An upset factor dials how much noise the judges tolerate: at zero it's pure chalk (the top seed always wins, which the tests verify); crank it up and lower seeds start pulling upsets.
Here's a full 16-resume bracket. My actual resume (labeled "Vestwell") enters mid-seed and climbs the right half (82 → 85.9 → 89.5) to take the final over a strong platform-engineer variant. The tournament didn't just call my resume "good." It told me it was the best of sixteen, and exactly which matchups it won.
Because it's deterministic, A/B mode is useful rather than theatrical. Change one bullet, keep the seed, and re-run: if the champion changes, that bullet mattered. It turns "does this edit help?" from a vibe into a measurement.
Every knob, explained
Two forms drive everything. The first tunes how the writer loop behaves; the second tunes how tournaments are judged. Sensible defaults ship out of the box, but every value is exposed because different jobs want different trade-offs.
The upload form
Loop budgets and scoring thresholds for a single resume.
When scoring is enabled, you also get the rubric weights and bullet shapes. Weights are live-balanced, so moving one rebalances the rest to keep the sum at 100%.
The A/B form
How a tournament is seeded and judged.
A loop that makes the next one better
Every run so far is stateless: good resume in, good resume out, no memory. The last piece closes the loop. When a run finishes, it writes a note to an Obsidian vault: the winning bullets, the full score breakdown, and tags for the role and domains. The vault is just markdown files, so it's greppable, portable, and yours.
On the next job, before the writer runs, a retrieval step loads past notes, keeps only the ones that actually did well (outcome: interview or offer) for the same role, and ranks them by how much their domains overlap with the new job. The top few winning bullets are handed to the writer as proven examples: framing that already cleared a panel, now available as a starting point instead of a blank page.
There's also a tuning layer in the vault: per-tag overrides that can nudge thresholds and rubric weights for particular role/domain combinations, learned from what those applications needed. Fintech backend roles and ML research roles don't want the same resume, and over time the system stops pretending they do.
Search, not magic
None of the individual pieces are exotic: a staged graph, a panel of judges, a bracket, a folder of markdown notes. What makes it work is the shape: draft, reject, revise, and keep only what survives, then remember it. A resume isn't a document you write once; it's the current best solution to a search problem, and this system just runs the search honestly.
A note on the numbers here: the scores, thresholds, and the bracket are from real runs. The revision-climb curve is illustrative, grounded in a real final score but drawn to show the shape of a typical run, not a logged transcript. I'd rather label that clearly than dress an estimate up as measurement. That, more than anything, is the point of the project.
The winning bullets
Enough about the machinery. Here are the five bullets from the resume that won its tournament, generated, scored, and shipped by the pipeline, shown verbatim. The highlighted figures are the quantified outcomes the panel rewards most.
- 01Action + stack
Built strongly-typed TypeScript and Node.js service layers for a real-time module serving 3,000 clients, using typed interfaces across WebSocket and GraphQL contracts to cut data delivery time 25%
- 02Result-first
70% cut in SQL latency achieved by tuning SQL-based databases (PostgreSQL) - rewriting queries, adding composite indexes, and optimizing Hibernate/JPA persistence via Redis caching layer under high concurrency
- 03PAR
Practiced test-driven development (TDD), writing failing unit tests before implementation across microservices with JUnit, Mockito and Jasmine/Jest suites gated in Jenkins CI/CD, sustaining 99% coverage
- 04Context-PAR
Across a 3,000-client real-time module, built responsive interfaces in the React framework using TypeScript hooks, semantic HTML5 markup, and CSS3 Flexbox/Grid styling for consistent cross-device rendering
- 05Action + stack
Owned CI/CD delivery by building automation pipelines using Jenkins with Groovy pipelines-as-code, running unit tests, SonarQube quality gates, and Docker/Kubernetes rollouts across 10+ services