What it does: You send a job posting link — the system reads it, picks the right CV, writes a personalised cover letter, and delivers two ready-to-send PDFs. It also tracks every PhD/job application end-to-end: Gmail sync, a terminal TUI, and a local web dashboard, all backed by a SQLite tracker (see Application Tracker).
Step 1 — Run the installer (zero-touch, run once)
./install.shThis detects your OS, installs uv + LaTeX (+ optional tesseract OCR), runs
uv sync, links the shared agent constitution (a central clone — not a
submodule), registers its skills into ~/.claude/skills/, and creates your
personal-data vault at ~/.local/share/agent-projects/applyforge/ with a
seeded secrets/.env. Personal data never lives in the repo.
Step 2 — Create your personal profile (in the vault)
cp examples/master_cv.example.json \
~/.local/share/agent-projects/applyforge/data/master_cv.jsonEdit it with your real name, contact info, work history, and skills. It lives in the vault, outside the repo — it can never be committed.
Step 3 — Add your API keys (in the vault)
The installer already seeded ~/.local/share/agent-projects/applyforge/secrets/.env
from .env.example. Open it and fill in:
CV_OWNER_SLUG— your name slug used in all output filenames (e.g.Firstname_LASTNAME)LLM_MODEL— LLM model identifier (e.g.deepseek-v4-flashfor DeepSeek, orgpt-4ofor OpenAI)OPENAI_API_KEYorDEEPSEEK_API_KEY— depending on your chosen LLM providerTELEGRAM_BOT_TOKENandTELEGRAM_CHAT_ID— only needed for the Telegram bot (see docs/bot-setup.md)
Step 4 — Verify everything is working
uv run main.py testIf you want to try the tool quickly, run these commands from the repository root:
# 1) Install Python deps
uv sync# 2) Copy env and set your OpenAI key
cp .env.example .env
# Edit .env and set OPENAI_API_KEY (and TELEGRAM_* if using the bot)# 3) Create the internal profile (accepts .tex, .pdf, .jpg/.png)
Examples:
uv run main.py init-profile --cv templates/lato/CV_AI_en.tex
uv run main.py init-profile --cv path/to/my_cv.pdf
uv run main.py init-profile --cv path/to/photo_of_cv.jpg# 4) Generate an application from a job URL (optional: pass --cv to override)
uv run main.py apply https://company.com/jobs/12345
uv run main.py apply https://company.com/jobs/12345 --cv path/to/my_cv.pdf# 5) Or start the Telegram bot and use /apply from your phone
uv run main.py bot# 6) Quick health check
uv run main.py testNotes:
- The
--cvflag forinit-profileandapplyaccepts.tex,.pdf, and common image formats (.jpg,.jpeg,.png,.webp). - PDF extraction and image OCR require additional system/software:
pdfminer.six,pytesseract, andPilloware Python dependencies (declared inpyproject.toml).tesseractOCR engine must be installed on your system for OCR to work. On macOS:brew install tesseract.
- If you only use a LaTeX
.texsource, you do NOT needtesseractor the OCR Python packages — the.texpath works without extra system deps. - The
--templateflag forapplysupportsaltacv(default) andlato. - The
--licenceflag forapplyforces inclusion of the conditional electronics degree. - The pipeline automatically blocks applications that require: Permis B, fonctionnaire status, French nationality, or Secret Défense clearance.
- For Technicien-tier postings (catégorie B, Bac+2/3 keywords), the pipeline automatically drops the DU degree, filters Master honors to Réseaux/Systèmes modules only, and normalizes experience titles from "Ingénieur" to "Technicien" — no LLM involved.
- Bilingual master CV:
data/master_cv_en.json(English source of truth) anddata/master_cv_fr.json(French). When--lang fris used, the pipeline loadsCV_<Role>_fr_source.jsonand generates it frommaster_cv_fr.jsonif absent — experience titles, degrees, and certifications are French from the start, no manual translation needed.
Start the bot:
uv run main.py botThen in Telegram, send:
/apply https://company.com/jobs/your-job-link
The bot will:
- Read the job posting
- Generate a tailored CV + cover letter (takes ~30 seconds)
- Send you both PDFs to review
- Ask Approve or Reject
- Approve → saves both files to your private archive channel
- Reject → nothing is saved
uv run main.py apply https://company.com/jobs/your-job-linkThe two PDFs are saved in the Applied/ folder.
| What | Where |
|---|---|
| Generated applications (CV + cover letter per job) | Applied/YYYY-MM-DD_Company_Role/ |
| Spontaneous applications (no company) | Applied/YYYY-MM-DD_Spontannee_Role_lang/ |
Output Filenames: All generated CVs and cover letters follow the standardized naming pattern:
{CV_OWNER_SLUG}-{DocumentType}_{Role}_{Language}.{ext}
The slug is set via CV_OWNER_SLUG in your .env file. Examples:
Firstname_LASTNAME-CV_IT_fr.pdf— IT infrastructure CV (French)Firstname_LASTNAME-LettreMotivation_AI_fr.pdf— AI cover letter (French)Firstname_LASTNAME-CV_PhD_en.pdf— PhD application CV (English)Firstname_LASTNAME-LettreMotivation_Courte_AI_fr.txt— plain-text short cover letter (≤1500 chars), generated alongside every PDF letter for pasting into character-limited application portals (e.g. France Travail)
This naming convention makes it easy to identify document type and role at a glance.
Edit one file only:
templates/shared/personal_data.tex
All CVs pull from this file — you never need to update the same detail in multiple places.
Templates are organized by style family:
Filenames follow CV_<Label>_<lang>.tex, matching the role labels in
config/roles.yaml and the profile JSON files (…-CV_<Label>_source.json).
templates/altacv/ — AltaCV style (xelatex), used for spontaneous applications:
| File | Role | Use for |
|---|---|---|
CV_AI_fr.tex / CV_AI_en.tex |
ai |
AI / MLOps roles |
CV_IngenieurEtude_fr.tex |
ingenieur_etude |
Ingénieur d'études / IGE (placeholder copy of AI) |
CV_DevOpsAlternance_fr.tex |
devops, devops_alternance |
DevOps (alternance layout) |
CV_Polyvalent_fr.tex |
polyvalent |
Polyvalent / interim agency (French) |
CV_Python_fr.tex |
python |
Generalist Python (placeholder copy of Polyvalent) |
templates/lato/ — Lato/article style (pdflatex):
| File | Role | Use for |
|---|---|---|
CV_AI_en.tex |
ai |
AI / Data Science / Python roles (English) |
CV_Support_fr.tex |
support |
IT Support / Network technicien (French) |
CV_Technicien_fr.tex |
technicien |
Intérim / technicien-tier — pre-down-levelled (French) |
CV_PhD_en.tex |
phd |
PhD / Research applications (English) |
templates/classic/ — ModernCV banking style (pdflatex), 16 role variants.
After editing a lato or classic template, rebuild the PDF:
./compile.sh ai # CV_AI_en
./compile.sh support # CV_Support_fr
./compile.sh phd # CV_PhD_en
./compile.sh all # rebuild all CV_*.tex across all template foldersGenerate a pre-written CV without LLM — no job URL needed:
uv run main.py spontaneous python # Generalist Python (French)
uv run main.py spontaneous ai # AI / MLOps (French)
uv run main.py spontaneous ai-en # AI / MLOps (English)
uv run main.py spontaneous devops # DevOps (French)
uv run main.py spontaneous devops_alternance # DevOps alternance / work-study (French)
uv run main.py spontaneous support # Network / support technicien (French)
uv run main.py spontaneous phd # PhD / Research (English)
uv run main.py spontaneous ingenieur_etude # Ingénieur d'études / IGE (French)
uv run main.py spontaneous polyvalent # Polyvalent / interim (French)
uv run main.py spontaneous technicien # Intérim technicien-tier, no DU/honours (French)
# Add --city to select Montpellier vs City Name automatically:
uv run main.py spontaneous ai --city montpellierOutput goes to Applied/YYYY-MM-DD_Spontannee_{role}_{lang}/.
All CV tracks are defined in one file: config/roles.yaml. The role
classifier, filename labels, CV + spontaneous template maps, cover-letter
variant, and per-role skill ordering are all derived from it — nothing is
hardcoded elsewhere.
Canonical roles: general, devops, ai, phd. Each entry
lists aliases that route fuzzy or legacy inputs to a canonical role (e.g.
it, network → devops; python, mlops → ai; polyvalent → general).
Append -en/-fr to a spontaneous role to override its language (ai-en).
Add a new role by adding an entry under roles: — or auto-scaffold one
(clones a base template stub + registers the entry):
from src.core import roles
roles.scaffold_role("cloud", base="devops", lang="fr")Run the registry contract tests with:
python tests/test_roles.py # or: pytest tests/test_roles.pyOpen the relevant template in cover_letters/:
| File | Language |
|---|---|
Cover_Letter_Template_Fr.tex |
French |
Cover_Letter_Template_En.tex |
English |
Only edit the stock paragraphs (the text that describes your experience).
The personalisation variables at the top (\CompanyName, \PositionTitle, etc.)
are filled automatically for each application — do not touch them.
Cover letter body is now fully LLM-generated. \CLIntro and \CLBody are
written by the LLM based on the actual job posting — no hardcoded variant blocks.
The LLM adapts tone and content: technical for IT/AI roles, transferable-skills focused
for maintenance or industrial roles.
node scripts/data_jobs_scraper.mjsReads ROME codes from docs/it_rome_codes.json, visits
candidat.francetravail.fr/metierscope/fiche-metier/{CODE}/ for each role,
extracts national offer count and candidate count, saves to docs/data_jobs_stats.json.
Key ROME codes for data jobs:
| Code | Métier |
|---|---|
| M1405 | Data Scientist |
| M1419 | Data Analyst |
| M1811 | Data Engineer |
| M1889 | Ingénieur IA |
| M1872 | Consultant BI |
Note: France Travail uses non-breaking spaces as thousands separators ("3 830").
The scraper strips all non-digit characters before parsing numbers.
src/apply_tracker/ tracks every PhD/job application from first sighting to reply,
in a SQLite DB (tracker.db) mirrored to per-track tracking.json files. It's
CLI-agnostic — the same src/apply_tracker/service.py business layer backs three
interfaces, invoked here directly with uv run python -m:
# Web dashboard (localhost:8765, auto-reload)
uv run python -m src.apply_tracker.web ~/@-Amir/Apply/2026-2027 8765
# Terminal TUI (arrow-key navigation)
uv run python -m src.apply_tracker.tui ~/@-Amir/Apply/2026-2027 phd
# Gmail sync — pulls [AMIR-SYNC] tagged position emails into the tracker
uv run python -c "
from pathlib import Path
from src.apply_tracker.gmail_sync import fetch_and_process
print(fetch_and_process(Path.home() / '@-Amir/Apply/2026-2027'))
"
# Quick stats
uv run python -m src.apply_tracker.stats_cli ~/@-Amir/Apply/2026-2027The amir CLI (separate repo, amir-cli) wraps these as amir apply web|tui|sync|stats|alert
and amir phd/amir job — same wrap pattern as amir apply <url> forwarding to
main.py apply in this repo. The application data itself (tracking.json, tracker.db,
found/applied position files) lives outside this repo, in the personal-data vault
(~/@-Amir/Apply/2026-2027 by default, override with APPLY_BASE_DIR) — never committed.
A generated application is a set of documents that must tell one story: the CV,
the letter PDF and the two .txt letters all restate the same degrees, the same
figures and the same identity. Editing one of them by hand — which is the normal
workflow — silently desynchronises the rest. The gate audits a finished folder for
exactly that:
uv run main.py check "$APPLY_BASE_DIR/Job-Search/applied/2026-08-03_Acme_Technicien_fr"It reads whatever is on disk right now (not the pipeline's in-memory bundle), so
hand-edits are covered. Layer 1 is eight deterministic rules — R1 stale artifact
(mtime + SHA sidecar), R2/R3 degree support and contradiction, R4 figures backed by
the CV on the same subject, R5 identity, R6 completeness and letter format, R7 one
language per document, R8 leftover placeholders. Layer 2 is a single free agy
flash call that catches wording contradictions the rules cannot see; it runs by
default, is non-blocking, and no-ops when agy is not installed.
uv run main.py check <dir> --no-semantic # Layer 1 only, fully offline
uv run main.py check <dir> --report /tmp/out.md # write the report elsewhereCOHERENCE.md is written into the folder only when the target sits inside this repo;
for folders in the personal vault the report goes to stdout unless --report is given.
For developers, AI agents, or anyone who wants to understand the internals:
| Topic | File |
|---|---|
| How the pipeline works (scraping, LLM, compilation) | docs/architecture.md |
| LaTeX macros, template structure, adding new styles | docs/latex-templates.md |
| Telegram bot setup, AUTO_APPLY, source files | docs/bot-setup.md |
| Git workflow, commit conventions, tracked files | docs/git-workflow.md |
Contact details live in the personal-data vault outside the repo
(~/.local/share/agent-projects/applyforge/shared/personal_data.tex, override with
APPLYFORGE_DATA_DIR) — private, never committed.
