Skip to main content

VITA — CV as Code

One base CV. Infinite targeted variants. Zero duplicates.

vita-cv is a CLI tool that treats your resume like source code — versioned with git, branched per company, built from LaTeX, and sharpened with AI assistance.

VITA can generate targeted AI prompts or run an autonomous AI workflow to analyze job descriptions, adapt your CV, and write a matching cover letter. Customize each task with your own Markdown skills.

uv tool install vita-cv

How It Works

VITA manages your CV using git branches:

Branch Purpose
master Your canonical base — the CV you're always proud of
gen-<role> Domain-specific base (e.g. gen-swe, gen-ml)
etp-<company>-<role> Tailored CV for a specific application (e.g. etp-google-swe)

Every time you apply somewhere, you create a branch. VITA tracks that in a registry so you never accidentally send the wrong version or apply twice.


Prerequisites

  • uv — install (pip install uv or curl -LsSf https://astral.sh/uv/install.sh | sh)
  • Python 3.10+ — managed automatically by uv
  • Git (must be in PATH)
  • LaTeX — TeX Live or MiKTeX, with latexmk

Quick Start

# 1. Install vita from PyPI as a persistent tool
uv tool install vita-cv
# → vita is now available globally in your terminal

# 2. Create your CV project directory
mkdir my-cv && cd my-cv

# 3. Initialize VITA (creates .vita/ and git repo)
vita init

# 4. Edit .vita/config.json — set your name
#    "author": "yourname"

# 5. Create your LaTeX CV (see below)
# ... write main.tex, sections/, etc.

# 6. Commit your base CV
git add .
git commit -m "base: initial CV"

# 7. Apply to a company
vita new etp google "software engineer"
# → creates branch etp-google-swe, updates registry

# 8. Tailor your CV for this job, then build
vita build

Your CV Project Structure

After running vita init, your project should look like this:

my-cv/
├── main.tex              ← LaTeX entry point (you create this)
├── sections/             ← Modular CV sections (you create these)
│   ├── experience.tex
│   ├── education.tex
│   ├── skills.tex
│   └── ...
├── job.md                ← Paste job descriptions here (for AI prompts)
├── cover-letter.tex      ← Cover letter source (generated by vita cover)
├── out/                  ← PDF output (auto-created on build)
│
└── .vita/                ← Auto-created by `vita init`
    ├── config.json       ← Your settings (author, output paths, etc.)
    ├── companies.json    ← Registry of all companies you've applied to
    ├── extensions.json   ← Your custom role aliases, languages, and LLM providers
    ├── .env              ← Local API keys for --auto mode
    ├── .env.example      ← Example API key file
    ├── skills/           ← Custom skills in analyze/, adapt/, review/, cover/
    ├── prompts/          ← Generated prompts for each task
    └── logs/             ← CLI activity log

.vita/ is gitignored — your registry and config stay local.


Files You Need to Create

VITA manages the .vita/ directory for you. You are responsible for the LaTeX files.

main.tex — Entry Point

Your main LaTeX file. VITA will compile this with latexmk. Example minimal structure:

\documentclass{article}

\begin{document}

\input{sections/experience}
\input{sections/education}
\input{sections/skills}

\end{document}

You can use any LaTeX CV class (e.g. moderncv, altacv, awesome-cv).

sections/ — Modular Content

Split your CV into files so each branch only touches relevant sections:

sections/
├── experience.tex   ← Work history (most frequently changed per company)
├── education.tex    ← Education
├── skills.tex       ← Skills (adjust keywords per role)
├── summary.tex      ← Optional: tailored headline
└── projects.tex     ← Optional

job.md — Job Description (for AI prompts)

When you run vita adapt or vita analyze, VITA looks for job.md in your project root. Paste the job posting there:

Company: Google
Role: Senior Software Engineer

[Paste full job description here]

Configuration Reference

vita init creates .vita/config.json with these defaults:

{
  "author": "",
  "output_dir": "out",
  "output_filename": "cv-{author}.pdf",
  "tex_entry": "main.tex",
  "cover_tex_entry": "cover-letter.tex",
  "cover_output_filename": "cover-letter-{author}.pdf",
  "build_mode": "auto",
  "default_base_branch": "master",
  "strict_mode": false,
  "allow_multiple_per_company": true,
  "warn_on_duplicate": true,
  "auto_add_codex_provider": false
}
Key What it does
author Your name — used in the PDF filename
output_dir Where the compiled PDF goes (out/ by default)
output_filename PDF name template. {author} is substituted automatically
tex_entry Your LaTeX entry file (default: main.tex)
cover_tex_entry Cover letter LaTeX source (default: cover-letter.tex)
cover_output_filename Cover letter PDF name (default: cover-letter-{author}.pdf)
build_mode Reserved build mode setting (auto by default)
default_base_branch The branch vita new checks out from if no base exists
strict_mode Reserved stricter workflow setting (false by default)
allow_multiple_per_company Reserved duplicate-policy setting
warn_on_duplicate If true, warns (but doesn't block) on duplicate company
auto_add_codex_provider If true, automatically adds a missing Codex provider to extensions.json without prompting

CLI Commands

vita init

Initialize VITA in the current directory. Creates .vita/, scaffolds config files, and runs git init.

vita init          # first-time setup
vita init --force  # wipe and re-initialize

vita new etp <company> <role>

Create a new tailored CV branch for a job application.

vita new etp google "software engineer"
vita new etp openai ml
vita new etp stripe "backend engineer" --commit  # commit pending changes first
  • Normalizes the role (software engineer → swe)
  • Creates branch etp-<company>-<role> off your base branch
  • Registers the company in .vita/companies.json
  • Warns if you've already applied to this company

vita status

See all companies, branches, and where you currently are.

vita status
VITA Status — yourname
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Current branch : etp-google-swe

Companies (2):
  google  [2 CVs]
    ├── etp-google-swe   base: gen-swe   ← you are here
    └── etp-google-ml    base: gen-ml

  meta  [1 CV] [LOCKED]
    └── etp-meta-swe     base: gen-swe

Base branches:
  gen-swe · gen-ml
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

vita sync

Reconcile the .vita directory and registry with your actual git branches. Useful if you created or deleted branches manually, or if local .vita files such as config.json, extensions.json, .env, or generated artifact directories are missing.

vita sync              # interactive
vita sync --dry-run    # preview only, no changes written
vita sync --auto       # apply all changes without prompting

vita save

Stage all changes, commit them, and push to the remote repository.

vita save
vita save -m "update Google CV"
vita save -r
  • -m/--message sets a custom commit message.
  • Without -m, VITA uses a generic commit message.
  • -r/--reconcile saves the current branch, checks out master, runs vita sync --auto, then saves master.

vita build

Compile your LaTeX CV, cover letter, or both to PDF using latexmk.

vita build                 # CV only (existing default)
vita build both            # CV and cover letter
vita build -r              # CV only; also --resume
vita build -c              # Cover letter only; also --cover

Both PDFs use the same configured output_dir: by default out/cv-<author>.pdf and out/cover-letter-<author>.pdf. Generate the cover letter first with vita cover --auto or vita run --auto. Building compiles existing sources; it does not call an AI provider. A missing source or compiler failure returns a nonzero exit status. both cannot be combined with --resume or --cover.


vita diff <company>

Show what changed in a company branch versus its base.

vita diff google

vita lock / unlock <company>

Lock a company to prevent creating new CVs for it (e.g. you've accepted an offer).

vita lock amazon
vita unlock amazon

vita extract

Extract structured job metadata without sending the CV to the extraction provider.

vita extract                           # Generate an extraction prompt
vita extract --auto                    # Refresh job.json and job.toon
vita extract --auto --provider openai

Extraction is optional and off by default. Enable it with ENABLE_JD_EXTRACTION=true in .vita/.env to prepend extraction to vita run. Configure a lightweight extraction model independently from the evaluator. See pipeline configuration and data contracts for routing, cache behavior, schemas, and score comparisons.

vita analyze

Generate an AI prompt to analyze your CV against the job in job.md.

vita analyze
vita analyze --auto
vita analyze --auto --provider codex

Without --auto, VITA saves the prompt to .vita/current_prompt.md; open it and give it to your AI assistant.

With --auto, VITA sends the prompt and local context to your configured LLM provider and saves validated .vita/results/analysis.json plus a rendered analysis.md. analyze.md remains a compatibility alias. Analysis uses validated TOON metadata when available, otherwise raw job.md.

job.md can contain one role, or multiple roles from the same company using # Job N sections:

# Job 1
Software Engineer, Infrastructure

# Job 2
Research Engineer, GenAI

When multiple jobs are detected, vita analyze --auto and vita run --auto produce one unified cross-role analysis with criteria identified by job and an overall documented-fit score. This score describes evidence in the CV, not hiring probability.


vita adapt [--language <code>]

Generate an AI prompt to tailor your CV to the job in job.md.

vita adapt               # English (default)
vita adapt --language fr # French
vita adapt --language ar # Arabic
vita adapt --auto --provider gemini

VITA adapts the CV title/headline, professional summary, experience, and skills based on the job description. For multi-job job.md files, it creates one coherent CV optimized for shared requirements across all listed roles instead of separate CV versions.

The CV title uses one role name, such as Applied AI Engineer, Data Engineer, Software Engineer, or ML Engineer. Specializations and keywords belong in the summary or skills, without a tagline appended to the title. For multiple jobs, VITA selects the single role that best fits the shared requirements and your experience.


vita review

Evaluate the updated CV against job.md. Review saves review.json and review.md using the same schema and scoring rubric as analysis. A matching initial analysis locks the criteria and produces delta.json and delta.md with before/after score changes. Review recommends improvements without editing the CV.

vita review
vita review --auto

vita run

Run the full AI workflow: (optional extract) → analyze → adapt → review → cover.

vita run
vita run --auto
vita run --auto --provider codex
vita run --auto --provider gemini --language fr

Without --auto, this saves each task to .vita/prompts/<task>.md and writes an ordered workflow to .vita/current_prompt.md for your assistant to execute. Each prompt includes its bundled and custom skills.

With --auto, VITA writes reports into .vita/results/, updates the configured CV source during adaptation, and creates the configured cover letter source after review. Each stage supports its own provider/model settings; review inherits analysis settings by default. Existing provider configuration remains the fallback. --language applies to both documents. A failed step stops the pipeline. Run vita build both to compile the PDFs.

vita cover [--language <code>]

Generate a cover letter (lettre de motivation) from the current CV and job.md, using the bundled cover-letter-generator.md skill and any .vita/skills/cover/*.md customizations.

vita cover --language fr                         # Generate a prompt
vita cover --auto --provider codex --language fr # Write cover-letter.tex
vita build --cover                              # Compile its PDF

The letter uses supported CV facts, optional .vita/cv.yaml knowledge, and the job's requirements. It uses the same model settings as the CV commands in .vita/extensions.json. The CV and a nonempty job.md must exist. Multiple jobs from one company produce one coherent letter. The generated letter is standalone LaTeX; it does not require the CV's custom class or assets.


vita keys

Manage API keys for LLM providers.

vita keys list
vita keys set gemini <your-key>
vita keys remove gemini

Keys are stored locally in .vita/.env. Codex CLI support uses your local Codex login instead of an API key.


Typical Workflow

# Day 1 — Set up your CV project
uv tool install vita-cv
mkdir my-cv && cd my-cv
vita init
# → edit .vita/config.json (set author)
# → write main.tex and sections/

git add .
git commit -m "base: initial CV"

# Applying to a company
git checkout -b gen-swe         # create your domain baseline
# → tailor the CV for SWE roles generally
git add . && git commit -m "gen-swe: baseline"

vita new etp google "software engineer"
# → now on branch etp-google-swe

# Paste job description
echo "..." > job.md
vita analyze    # get analysis prompt
vita adapt      # get tailoring prompt
# → run prompts with AI, apply suggestions to sections/

# Or run the autonomous pipeline with a configured provider
vita run --auto --provider codex

vita build      # compile to PDF
# → out/cv-yourname.pdf

vita status     # review all companies

Building the PDF

VITA uses latexmk automatically. If you need to build manually:

# Standard
latexmk -pdf -outdir=out main.tex

# With bibliography (BibTeX/Biber)
pdflatex -output-directory=out main.tex
biber --input-directory=out --output-directory=out main
pdflatex -output-directory=out main.tex
pdflatex -output-directory=out main.tex

Extensibility

VITA ships with built-in role aliases, language codes, and LLM provider support. You can add your own in .vita/extensions.json (created by vita init):

{
  "llm_providers": {
    "codex": {
      "model": ""
    },
    "gemini": {
      "model": "gemini-2.0-flash"
    }
  },
  "role_aliases": {
    "research engineer": "re",
    "applied scientist": "as",
    "solutions architect": "sa"
  },
  "language_map": {
    "zh": "Chinese (Simplified)",
    "ja": "Japanese"
  }
}

Your entries are merged on top of the built-ins — your values win on conflict. Use --provider with --auto commands to choose a configured LLM provider.

See EXTENSIONS.md for the full list of built-in aliases and all format details.


CI/CD and automated releases

GitHub Actions runs linting, dependency auditing, package validation, CodeQL, regression tests on Python 3.10 through 3.14, and an isolated installed-package smoke test. The full pipeline also runs every Monday at 05:23 UTC. Dependabot checks Python and GitHub Actions dependencies every Monday.

Releases require two separate PR merges. Release Please maintains a TestPyPI release PR, including vita.__version__ and CHANGELOG.md. Merging it creates the version tag and a draft GitHub release, runs the release checks, builds once, and publishes to TestPyPI only. After verifying the uploaded package, automation publishes the GitHub prerelease and opens a PyPI promotion PR with the exact file hashes and testing instructions. Test the package yourself, then merge that second PR when ready. Only that merge publishes the same files to PyPI and marks the GitHub release stable.

Use these pull request title prefixes to select the next version automatically:

  • fix: ... creates a patch release, such as 2.3.0 to 2.3.1.
  • feat: ... creates a minor release, such as 2.3.0 to 2.4.0.
  • feat!: ..., fix!: ..., or a BREAKING CHANGE: footer creates a major release.
  • chore: ..., ci: ..., and test: ... normally do not create a release.

Use a scope when useful, for example fix(cli): handle a missing config file. CI rejects pull request titles that are not Conventional Commits. To select an exact exceptional version, add Release-As: X.Y.Z to the squash commit body.

Add the publishing tokens to GitHub

Never commit either token to this repository. Store each token as a GitHub environment secret:

  1. Open the GitHub repository and go to Settings → Environments.
  2. Create an environment named testpypi. Under Environment secrets, add TEST_PYPI_API_TOKEN with a project-scoped token created at TestPyPI account settings.
  3. Create an environment named pypi. Under Environment secrets, add PYPI_API_TOKEN with a project-scoped token created at PyPI account settings.
  4. Restrict both environments to tags matching v*. Merging the promotion PR dispatches PyPI publication on its tested version tag, preserving existing environment restrictions. Environment reviewers are optional extra protection; merging the promotion PR is already the explicit production approval.

The token value includes the pypi- prefix. The workflow supplies the required __token__ username automatically.

Add the release automation token to GitHub

Release Please needs a fine-grained GitHub personal access token so that its pull requests run the required CI checks and its version tags start the publishing workflow. The same token creates the separate promotion PR. In GitHub, open Settings → Secrets and variables → Actions, create a repository secret named RELEASE_PLEASE_TOKEN, and paste a fine-grained token with access only to this repository. Grant it Contents: Read and write, Pull requests: Read and write, and Issues: Read and write. Keep the token owner authorized to create branches, tags, and pull requests in this repository.

After the three secrets are configured, normal development and releases use this flow:

  1. Merge a pull request with a Conventional Commit title into master.
  2. Release Please creates or updates chore(testpypi): release X.Y.Z with the calculated version and release notes.
  3. Review and merge that PR after its checks pass. The vX.Y.Z tag triggers TestPyPI publication and verification.
  4. Automation opens chore(pypi): release X.Y.Z, adding only .github/releases/pypi/vX.Y.Z.json. Its body includes a TestPyPI install command and SHA-256 hashes for the wheel and source distribution.
  5. Install from TestPyPI in a separate environment and test your workflow. Leave the promotion PR open while testing; closing it without merging does not publish anything to PyPI.
  6. Merge the promotion PR when satisfied. Automation dispatches publication on the tested version tag, verifies the merged approval, successful TestPyPI run, tag/commit provenance, and hashes, downloads the exact tested files, smoke-tests them, and uploads them to PyPI without rebuilding. It verifies PyPI's files before marking the GitHub release stable. Manually dispatching this workflow also requires a valid, already-merged promotion PR; selecting an unapproved PR or a different tag fails validation.

Promotion uses TestPyPI as the durable source of the files, so it does not depend on an expiring Actions artifact. A newer master commit or a newer pending release cannot alter an older promotion's package. Keep these promotion PRs limited to their generated JSON record; adding other changes makes the production check fail.

If publication fails, rerun the failed jobs in the original workflow. Already uploaded files are accepted only when their hashes match. Retrying the TestPyPI workflow reuses its existing promotion PR and refreshes the run provenance only when the package bytes are unchanged. Closed promotion PRs are not automatically reopened. If testing finds a package problem, close the promotion PR, merge a fix, and release a new version: an uploaded version's files must not be replaced. A full rebuild can produce different bytes and will be rejected for an already uploaded version.


AI Skills

Add project-specific Markdown skills under the folder for their task:

.vita/skills/
├── extract/                 # Optional JD extraction guidance
├── analyze/                 # Job analysis
├── adapt/                   # CV adaptation
│   ├── writing-style.md     # Additional guidance
│   └── resume-tailor.md     # Replaces the bundled skill with this filename
├── review/                  # CV review
└── cover/                   # Cover letter construction
    └── tone.md

vita init creates these folders; existing projects can use vita sync --auto. Nonempty .md files directly inside a task folder apply only to that task, in both manual and automatic modes. A file matching a bundled skill's filename replaces that skill for the task. Other files are appended in filename order, with project writing/style guidance taking precedence over bundled guidance. Empty files are ignored. No frontmatter is required. See custom task skills for the default skill mapping and an example.

VITA includes a library of AI agent skills for specialized tasks. Reference them when prompting your AI assistant:

Category Skills
Writing resume-writer, resume-bullet-writer, resume-quantifier
Reviewing resume-reviewer, resume-ats-optimizer
Targeting resume-tailor, job-description-analyzer, resume-section-builder
Documents cover-letter-generator, linkedin-profile-optimizer
Prep interview-prep-generator, salary-negotiation-prep, offer-comparison-analyzer
Specialized tech-resume-optimizer, executive-resume-writer, academic-cv-builder
Transitions career-changer-translator, creative-portfolio-resume

Example usage:

"Use the resume-tailor skill to update sections/experience.tex based on the job description I pasted in job.md."

"Run interview-prep-generator on my CV and generate 5 STAR stories for a Google interview."


Design Philosophy

  • Warn, don't block — duplicate CVs trigger a warning, not a hard stop (--force overrides, locked: true blocks permanently)
  • CLI enforces naming — roles are normalized automatically (data engineer → de, machine learning engineer → ml)
  • Registry is the truth — .vita/companies.json is the authoritative record; use vita sync if it drifts from git
  • User-extensible — role aliases and language codes are fully overridable via .vita/extensions.json
  • Local-first — prompt generation, git workflows, and LaTeX builds are local. --auto sends CV/job context to the LLM provider you configure.

Metadata

Release files for vita-cv 2.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vita-cv 2.5.0
File Size Uploaded Provenance
vita_cv-2.5.0.tar.gz 156.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for vita-cv 2.5.0
File Interpreter ABI Platform
vita_cv-2.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 307.4 kB

Release files / vita_cv-2.5.0.tar.gz

Download URL vita_cv-2.5.0.tar.gz
Size 156.3 kB
Tags Source
SHA-256 checksum
How to use checksums
07d7197268449020c5d3b314d626015f6e1a12dd4348f03e1268536c08d47dc8
BLAKE2b-256 checksum
How to use checksums
7dbe60747e53653231b812ae940e5b271e3ff62bcc11e3054c7394695903826f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / vita_cv-2.5.0-py3-none-any.whl

Download URL vita_cv-2.5.0-py3-none-any.whl
Size 151.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b71b6ba8b15a606f712db7b61e73a4ebcf99a1c9dd3f48db42460c7239d21d4d
BLAKE2b-256 checksum
How to use checksums
b844a570cddb804dd8a57dafa1db3ba6e575f07fc73cd85e065a9d7a1177cf18
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for vita-cv 2.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page