This system was built and used by santifer to evaluate 740+ job offers, generate 100+ tailored CVs, and land a Head of Applied AI role. The archetypes, scoring logic, negotiation scripts, and proof point structure all reflect his specific career search in AI/automation roles.
The portfolio that goes with this system is also open source: cv-santiago.
It will work out of the box, but it's designed to be made yours. If the archetypes don't match your career, the modes are in the wrong language, or the scoring doesn't fit your priorities -- just ask. You (AI Agent) can edit the user's files. The user says "change the archetypes to data engineering roles" and you do it. That's the whole point.
There are two layers. Read DATA_CONTRACT.md for the full list.
User Layer (NEVER auto-updated, personalization goes HERE):
cv.md,config/profile.yml,modes/_profile.md,article-digest.md,portals.ymldata/*,reports/*,output/*,interview-prep/*
System Layer (auto-updatable, DON'T put user data here):
modes/_shared.md,modes/oferta.md, all other modesCLAUDE.md,*.mjsscripts,dashboard/*,templates/*,batch/*
THE RULE: When the user asks to customize anything (archetypes, narrative, negotiation scripts, proof points, location policy, comp targets), ALWAYS write to modes/_profile.md or config/profile.yml. NEVER edit modes/_shared.md for user-specific content. This ensures system updates don't overwrite their customizations.
On the first message of each session, run the update checker silently:
node update-system.mjs checkParse the JSON output:
{"status": "update-available", "local": "1.0.0", "remote": "1.1.0", "changelog": "..."}→ tell the user:"career-ops update available (v{local} → v{remote}). Your data (CV, profile, tracker, reports) will NOT be touched. Want me to update?" If yes → run
node update-system.mjs apply. If no → runnode update-system.mjs dismiss.{"status": "up-to-date"}→ say nothing{"status": "dismissed"}→ say nothing{"status": "offline"}→ say nothing
The user can also say "check for updates" or "update career-ops" at any time to force a check.
To rollback: node update-system.mjs rollback
AI-powered job search automation built on Claude Code: pipeline tracking, offer evaluation, CV generation, portal scanning, batch processing.
| File | Function |
|---|---|
data/applications.md |
Application tracker |
data/pipeline.md |
Inbox of pending URLs |
data/scan-history.tsv |
Scanner dedup history |
portals.yml |
Query and company config |
templates/cv-template.html |
HTML template for CVs |
generate-pdf.mjs |
Playwright: HTML to PDF |
article-digest.md |
Compact proof points from portfolio (optional) |
interview-prep/story-bank.md |
Accumulated STAR+R stories across evaluations |
interview-prep/{company}-{role}.md |
Company-specific interview intel reports |
analyze-patterns.mjs |
Pattern analysis script (JSON output) |
reports/ |
Evaluation reports (format: {###}-{company-slug}-{YYYY-MM-DD}.md) |
When using OpenCode, the following slash commands are available (defined in .opencode/commands/):
| Command | Claude Code Equivalent | Description |
|---|---|---|
/career-ops |
/career-ops |
Show menu or evaluate JD with args |
/career-ops-pipeline |
/career-ops pipeline |
Process pending URLs from inbox |
/career-ops-evaluate |
/career-ops oferta |
Evaluate job offer (A-F scoring) |
/career-ops-compare |
/career-ops ofertas |
Compare and rank multiple offers |
/career-ops-contact |
/career-ops contacto |
LinkedIn outreach (find contacts + draft) |
/career-ops-deep |
/career-ops deep |
Deep company research |
/career-ops-pdf |
/career-ops pdf |
Generate ATS-optimized CV |
/career-ops-training |
/career-ops training |
Evaluate course/cert against goals |
/career-ops-project |
/career-ops project |
Evaluate portfolio project idea |
/career-ops-tracker |
/career-ops tracker |
Application status overview |
/career-ops-apply |
/career-ops apply |
Live application assistant |
/career-ops-scan |
/career-ops scan |
Scan portals for new offers |
/career-ops-batch |
/career-ops batch |
Batch processing with parallel workers |
/career-ops-patterns |
/career-ops patterns |
Analyze rejection patterns and improve targeting |
Note: OpenCode commands invoke the same .claude/skills/career-ops/SKILL.md skill used by Claude Code. The modes/* files are shared between both platforms.
Before doing ANYTHING else, check if the system is set up. Run these checks silently every time a session starts:
- Does
cv.mdexist? - Does
config/profile.ymlexist (not just profile.example.yml)? - Does
modes/_profile.mdexist (not just _profile.template.md)? - Does
portals.ymlexist (not just templates/portals.example.yml)?
If modes/_profile.md is missing, copy from modes/_profile.template.md silently. This is the user's customization file — it will never be overwritten by updates.
If ANY of these is missing, enter onboarding mode. Do NOT proceed with evaluations, scans, or any other mode until the basics are in place. Guide the user step by step:
If cv.md is missing, ask:
"I don't have your CV yet. You can either:
- Paste your CV here and I'll convert it to markdown
- Paste your LinkedIn URL and I'll extract the key info
- Tell me about your experience and I'll draft a CV for you
Which do you prefer?"
Create cv.md from whatever they provide. Make it clean markdown with standard sections (Summary, Experience, Projects, Education, Skills).
If config/profile.yml is missing, copy from config/profile.example.yml and then ask:
"I need a few details to personalize the system:
- Your full name and email
- Your location and timezone
- What roles are you targeting? (e.g., 'Senior Backend Engineer', 'AI Product Manager')
- Your salary target range
I'll set everything up for you."
Fill in config/profile.yml with their answers. For archetypes and targeting narrative, store the user-specific mapping in modes/_profile.md or config/profile.yml rather than editing modes/_shared.md.
If portals.yml is missing:
"I'll set up the job scanner with 45+ pre-configured companies. Want me to customize the search keywords for your target roles?"
Copy templates/portals.example.yml → portals.yml. If they gave target roles in Step 2, update title_filter.positive to match.
If data/applications.md doesn't exist, create it:
# Applications Tracker
| # | Date | Company | Role | Score | Status | PDF | Report | Notes |
|---|------|---------|------|-------|--------|-----|--------|-------|After the basics are set up, proactively ask for more context. The more you know, the better your evaluations will be:
"The basics are ready. But the system works much better when it knows you well. Can you tell me more about:
- What makes you unique? What's your 'superpower' that other candidates don't have?
- What kind of work excites you? What drains you?
- Any deal-breakers? (e.g., no on-site, no startups under 20 people, no Java shops)
- Your best professional achievement — the one you'd lead with in an interview
- Any projects, articles, or case studies you've published?
The more context you give me, the better I filter. Think of it as onboarding a recruiter — the first week I need to learn about you, then I become invaluable."
Store any insights the user shares in config/profile.yml (under narrative), modes/_profile.md, or in article-digest.md if they share proof points. Do not put user-specific archetypes or framing into modes/_shared.md.
After every evaluation, learn. If the user says "this score is too high, I wouldn't apply here" or "you missed that I have experience in X", update your understanding in modes/_profile.md, config/profile.yml, or article-digest.md. The system should get smarter with every interaction without putting personalization into system-layer files.
Once all files exist, confirm:
"You're all set! You can now:
- Paste a job URL to evaluate it
- Run
/career-ops scan(or/career-ops-scanif using OpenCode) to search portals- Run
/career-opsto see all commandsEverything is customizable — just ask me to change anything.
Tip: Having a personal portfolio dramatically improves your job search. If you don't have one yet, the author's portfolio is also open source: github.com/santifer/cv-santiago — feel free to fork it and make it yours."
Then suggest automation:
"Want me to scan for new offers automatically? I can set up a recurring scan every few days so you don't miss anything. Just say 'scan every 3 days' and I'll configure it."
If the user accepts, use the /loop or /schedule skill (if available) to set up a recurring /career-ops scan (or /career-ops-scan if using OpenCode). If those aren't available, suggest adding a cron job or remind them to run /career-ops scan (or /career-ops-scan if using OpenCode) periodically.
This system is designed to be customized by YOU (AI Agent). When the user asks you to change archetypes, translate modes, adjust scoring, add companies, or modify negotiation scripts -- do it directly. You read the same files you use, so you know exactly what to edit.
Common customization requests:
- "Change the archetypes to [backend/frontend/data/devops] roles" → edit
modes/_profile.mdorconfig/profile.yml - "Translate the modes to English" → edit all files in
modes/ - "Add these companies to my portals" → edit
portals.yml - "Update my profile" → edit
config/profile.yml - "Change the CV template design" → edit
templates/cv-template.html - "Adjust the scoring weights" → edit
modes/_profile.mdfor user-specific weighting, or editmodes/_shared.mdandbatch/batch-prompt.mdonly when changing the shared system defaults for everyone
Default modes are in modes/ (English). Additional language-specific modes are available:
- German (DACH market):
modes/de/— native German translations with DACH-specific vocabulary (13. Monatsgehalt, Probezeit, Kündigungsfrist, AGG, Tarifvertrag, etc.). Includes_shared.md,angebot.md(evaluation),bewerben.md(apply),pipeline.md. - French (Francophone market):
modes/fr/— native French translations with France/Belgium/Switzerland/Luxembourg-specific vocabulary (CDI/CDD, convention collective SYNTEC, RTT, mutuelle, prévoyance, 13e mois, intéressement/participation, titres-restaurant, CSE, portage salarial, etc.). Includes_shared.md,offre.md(evaluation),postuler.md(apply),pipeline.md.
When to use German modes: If the user is targeting German-language job postings, lives in DACH, or asks for German output. Either:
- User says "use German modes" → read from
modes/de/instead ofmodes/ - User sets
language.modes_dir: modes/deinconfig/profile.yml→ always use German modes - You detect a German JD → suggest switching to German modes
When to use French modes: If the user is targeting French-language job postings, lives in France/Belgium/Switzerland/Luxembourg/Quebec, or asks for French output. Either:
- User says "use French modes" → read from
modes/fr/instead ofmodes/ - User sets
language.modes_dir: modes/frinconfig/profile.yml→ always use French modes - You detect a French JD → suggest switching to French modes
When NOT to: If the user applies to English-language roles, even at French or German companies, use the default English modes.
| If the user... | Mode |
|---|---|
| Pastes JD or URL | auto-pipeline (evaluate + report + PDF + tracker) |
| Asks to evaluate offer | oferta |
| Asks to compare offers | ofertas |
| Wants LinkedIn outreach | contacto |
| Asks for company research | deep |
| Preps for interview at specific company | interview-prep |
| Wants to generate CV/PDF | pdf |
| Evaluates a course/cert | training |
| Evaluates portfolio project | project |
| Asks about application status | tracker |
| Fills out application form | apply |
| Searches for new offers | scan |
| Processes pending URLs | pipeline |
| Batch processes offers | batch |
| Asks about rejection patterns or wants to improve targeting | patterns |
cv.mdin project root is the canonical CVarticle-digest.mdhas detailed proof points (optional)- NEVER hardcode metrics -- read them from these files at evaluation time
This system is designed for quality, not quantity. The goal is to help the user find and apply to roles where there is a genuine match -- not to spam companies with mass applications.
- NEVER submit an application without the user reviewing it first. Fill forms, draft answers, generate PDFs -- but always STOP before clicking Submit/Send/Apply. The user makes the final call.
- Strongly discourage low-fit applications. If a score is below 4.0/5, explicitly recommend against applying. The user's time and the recruiter's time are both valuable. Only proceed if the user has a specific reason to override the score.
- Quality over speed. A well-targeted application to 5 companies beats a generic blast to 50. Guide the user toward fewer, better applications.
- Respect recruiters' time. Every application a human reads costs someone's attention. Only send what's worth reading.
NEVER trust WebSearch/WebFetch to verify if an offer is still active. ALWAYS use Playwright:
browser_navigateto the URLbrowser_snapshotto read content- Only footer/navbar without JD = closed. Title + description + Apply = active.
Exception for batch workers (claude -p): Playwright is not available in headless pipe mode. Use WebFetch as fallback and mark the report header with **Verification:** unconfirmed (batch mode). The user can verify manually later.
- Node.js (mjs modules), Playwright (PDF + scraping), YAML (config), HTML/CSS (template), Markdown (data), Canva MCP (optional visual CV)
- Scripts in
.mjs, configuration in YAML - Output in
output/(gitignored), Reports inreports/ - JDs in
jds/(referenced aslocal:jds/{file}in pipeline.md) - Batch in
batch/(gitignored except scripts and prompt) - Report numbering: sequential 3-digit zero-padded, max existing + 1
- RULE: After each batch of evaluations, run
node merge-tracker.mjsto merge tracker additions and avoid duplications. - RULE: NEVER create new entries in applications.md if company+role already exists. Update the existing entry.
Write one TSV file per evaluation to batch/tracker-additions/{num}-{company-slug}.tsv. Single line, 9 tab-separated columns:
{num}\t{date}\t{company}\t{role}\t{status}\t{score}/5\t{pdf_emoji}\t[{num}](reports/{num}-{slug}-{date}.md)\t{note}
Column order (IMPORTANT -- status BEFORE score):
num-- sequential number (integer)date-- YYYY-MM-DDcompany-- short company namerole-- job titlestatus-- canonical status (e.g.,Evaluated)score-- formatX.X/5(e.g.,4.2/5)pdf--✅or❌report-- markdown link[num](reports/...)notes-- one-line summary
Note: In applications.md, score comes BEFORE status. The merge script handles this column swap automatically.
- NEVER edit applications.md to ADD new entries -- Write TSV in
batch/tracker-additions/andmerge-tracker.mjshandles the merge. - YES you can edit applications.md to UPDATE status/notes of existing entries.
- All reports MUST include
**URL:**in the header (between Score and PDF). - All statuses MUST be canonical (see
templates/states.yml). - Health check:
node verify-pipeline.mjs - Normalize statuses:
node normalize-statuses.mjs - Dedup:
node dedup-tracker.mjs
Source of truth: templates/states.yml
| State | When to use |
|---|---|
Evaluated |
Report completed, pending decision |
Applied |
Application sent |
Responded |
Company responded |
Interview |
In interview process |
Offer |
Offer received |
Rejected |
Rejected by company |
Discarded |
Discarded by candidate or offer closed |
SKIP |
Doesn't fit, don't apply |
RULES:
- No markdown bold (
**) in status field - No dates in status field (use the date column)
- No extra text (use the notes column)