career-ops · system explainer

How career-ops works — surfaces, pipeline, and data construct

career-ops is an AI-powered job-search pipeline that lives in a git repo and runs on top of an AI coding CLI. There is no server and no database: markdown and YAML files are the database, AI "modes" are the application logic, and Node scripts are the deterministic engine. You talk to it through /career-ops in Claude Code (or OpenCode / Gemini CLI), watch it through a Go terminal dashboard, and automate it through zero-token scripts. Everything it produces — evaluations, tailored ATS CVs, tracker rows, interview prep — is a file you own, split by a strict data contract into a user layer (never auto-updated) and a system layer (upgradeable via update-system.mjs).

System map

ACCESS SURFACES Claude Code /career-ops {mode} OpenCode · Gemini /career-ops-* commands Go TUI dashboard dashboard/ (read-only) cron / shell node *.mjs · batch skill router .claude/skills/career-ops/SKILL.md → 17 modes ENGINE AI mode playbooks modes/*.md — oferta, pdf, scan, apply, deep… _shared.md rules + _profile.md user overrides deterministic scripts scan.mjs · generate-pdf.mjs · merge-tracker.mjs verify-pipeline · liveness · patterns · followup ATS job boards Greenhouse · Ashby · Lever · Workable APIs DATA (files = the database) USER LAYER — never auto-updated cv.md · config/profile.yml · portals.yml modes/_profile.md · article-digest.md data/applications.md (tracker) data/pipeline.md (inbox) · scan-history.tsv reports/###-company-date.md (A–G evals) output/*.pdf (tailored CVs) · interview-prep/ SYSTEM LAYER — upgradeable modes/_shared.md · templates/ · *.mjs · dashboard/ update-system.mjs check / apply / rollback loads playbook writes scans jobs found → inbox runs via Bash reads tracker

How a job flows through the system

1
.claude/skills/career-ops/SKILL.md — the router

Every interaction starts at one skill file. /career-ops with no args shows the command menu; /career-ops scan routes to the scan mode; pasting a raw JD or URL triggers the auto-pipeline (evaluate → report → PDF → tracker, end to end). The router then loads modes/_shared.md (system rules, scoring logic) plus the specific mode playbook, and — critically — modes/_profile.md last, so your personal overrides always win.

The same modes are exposed as /career-ops-* commands in OpenCode (.opencode/commands/) and Gemini CLI (.gemini/commands/*.toml) — different CLI, identical playbooks, identical files.

2
scan.mjs + portals.yml — zero-token discovery

The scanner costs zero LLM tokens: it hits Greenhouse/Ashby/Lever/Workable/SmartRecruiters/Recruitee JSON APIs directly for every company in portals.yml (~111 tracked), then applies your title_filter and location_filter keywords locally. Survivors land in data/pipeline.md (the inbox); data/scan-history.tsv dedups across runs so you never see the same posting twice. Schedule it on cron — it's pure Node.

show the filter (scan.mjs:121)
// scan.mjs — case-insensitive SUBSTRING match
function buildTitleFilter(titleFilter) {
  const positive = (titleFilter?.positive || []).map(k => k.toLowerCase());
  const negative = (titleFilter?.negative || []).map(k => k.toLowerCase());
  return (title) => {
    const lower = title.toLowerCase();
    const hasPositive = positive.length === 0 || positive.some(k => lower.includes(k));
    const hasNegative = negative.some(k => lower.includes(k));
    // substring! "CTO" matches "direCTOr" — use " CTO" with a leading space
3
modes/oferta.md — the A–G evaluation core

This is the heart. For each offer, the AI runs a seven-block evaluation and writes it to reports/###-company-date.md:

  • A — Role Summary: archetype, seniority, remote policy, TL;DR
  • B — Match with CV: every JD requirement mapped to exact lines in cv.md, plus gaps with mitigation plans
  • C — Level & Strategy: detected level vs yours, "sell senior without lying" plan, downlevel playbook
  • D — Comp & Demand: live WebSearch salary data, cited sources
  • E — Customization Plan: top-5 CV changes + top-5 LinkedIn changes for this role
  • F — Interview Plan: 6–10 STAR+R stories mapped to JD reqs, accumulated into interview-prep/story-bank.md
  • G — Posting Legitimacy: ghost-job detection (posting age, repost patterns, layoff news, JD specificity) → High Confidence / Caution / Suspicious

Output is a global 1–5 score. The system is ethically opinionated: below 4.0 it explicitly recommends against applying — quality over spray-and-pray. Liveness is verified against the source (Playwright snapshot or ATS API status) before any work happens.

4
generate-pdf.mjs + templates/cv-template.html — ATS CV generation

For offers scoring ≥ auto_pdf_score_threshold (default 3.0), the pdf mode tailors your CV to that JD: 15–20 keywords extracted, summary rewritten with them, bullets reordered by relevance, competency grid built from requirements — with a hard rule: never invent skills, only reword real experience using JD vocabulary. The HTML is rendered to PDF via Playwright, single-column and ATS-clean (selectable text, standard headers, unicode normalized to ASCII, A4 or US-letter by company geography). A LaTeX/Overleaf export (generate-latex.mjs) and optional Canva visual flow exist too.

show the run command
node generate-pdf.mjs /tmp/cv-steven-gonsalvez-{company}.html \
  output/cv-steven-gonsalvez-{company}-{date}.pdf --format=a4
# → output/ is gitignored; PDFs are yours, named per company per day
5
data/applications.md + merge-tracker.mjs — the tracker (your ATS)

Every evaluated offer becomes a row: number, date, company, role, score, status, PDF flag, report link, note. Statuses are canonical (templates/states.yml): Evaluated → Applied → Responded → Interview → Offer, or Rejected / Discarded / SKIP. Writes are protected by a merge protocol — evaluations write single-line TSVs to batch/tracker-additions/ and merge-tracker.mjs folds them in, preventing duplicate rows when parallel workers finish at once. Health tooling: verify-pipeline.mjs (lint everything), normalize-statuses.mjs, dedup-tracker.mjs, check-liveness.mjs (are my applied-to jobs still open?).

show the TSV contract
# batch/tracker-additions/{num}-{company}.tsv — 9 tab-separated cols
{num} {date} {company} {role} {status} {score}/5 {✅|❌} [{num}](reports/…) {note}
# status BEFORE score in TSV; merge script swaps for the tracker table
6
dashboard/ — Go TUI (Bubble Tea)

The dashboard is a terminal app, not a webpage. It parses data/applications.md, computes funnel metrics, and gives you three screens: pipeline table, full report viewer (enter on a row), and progress metrics. q quits, esc goes back. It is read-only — a lens over the same files, never a writer.

how to run it
cd ~/d/git/career-ops/dashboard
go run . --path ..          # run against the repo root
# or build a binary once:
go build -o ~/bin/career-dash . && career-dash --path ~/d/git/career-ops
7
modes/ — everything beyond the core loop
  • apply — live application assistant: reads the form, drafts answers in your voice, fills fields — always stops before Submit (you click)
  • contacto — LinkedIn outreach: finds likely hiring contacts, drafts the message
  • deep — deep company research before an interview or offer call
  • interview-prep — company-specific prep doc + growing STAR story bank
  • patternsanalyze-patterns.mjs mines your rejections to fix targeting
  • followupfollowup-cadence.mjs flags overdue follow-ups, drafts the nudge
  • ofertas — compare and rank multiple offers side by side
  • training / project — evaluate a course or portfolio idea against your goals
  • batchbatch/batch-runner.sh spawns parallel headless workers (claude -p) for bulk evaluation
  • Language packs: full native mode sets for DACH (modes/de), French (fr), Japanese (ja), Turkish (tr), plus pt, ru, ua
8
update-system.mjs + DATA_CONTRACT.md — why upgrades never eat your data

The whole construct rests on a two-layer data contract. User layer (cv, profile, portals, tracker, reports, output, interview-prep) is never touched by updates. System layer (shared mode logic, scripts, templates, dashboard) is upgradeable: node update-system.mjs check runs at session start, apply upgrades with a backup, rollback restores. Your customizations live in modes/_profile.md and config/profile.yml precisely so that pulling v1.9 never overwrites who you are.