Skip to content

vbkatarnaware/careeros

Repository files navigation

CareerOS

CI License: MIT Python 3.11+

An AI-powered, deterministic job discovery and recommendation engine. Not an application bot.

CareerOS finds jobs, scores them against your real experience, and writes a readable digest of the results — resume and cover letter included for every strong match. The KPI is simple: more interviews, for the least amount of AI and compute cost. Runs entirely on your own machine, with no signup and no Google account required to try it.

Quickstart (60 seconds, no Google account needed)

Requires Python 3.11+ and a host coding CLI (Claude Code, Codex, Gemini CLI, OpenCode, …) — CareerOS's AI reasoning steps (scoring, resume tailoring) run inside your coding CLI, not as a separate service. The careeros Python package is the deterministic half; see Architecture.

git clone https://github.com/<you>/careeros.git
cd careeros
pip install -e .
careeros init

Set your discovery credentials — three sources are on by default: the official Fantastic Jobs REST API (needs a free key), plus RemoteOK and We Work Remotely (free, no signup, nothing to configure):

export FANTASTIC_API_KEY=...   # developer.fantastic.jobs — or RAPIDAPI_KEY, see Installation below

Then, inside your host coding CLI:

/careeros start     # guided onboarding — paste your CV (or `skip`), set your goal
/careeros daily      # discover, score, and generate resumes/covers for today's matches

That's it — no Google Sheet, no OAuth, no Drive setup. Every run writes a plain-Markdown digest to .careeros/results/latest/summary.md, linking straight to each match's rendered resume/cover PDF. Want one specific job (any score, not just today's Apply-tier matches) fully generated and published right now? /careeros job <job-id>.

Want a shared, clickable Google Sheet + Drive backup instead of (or in addition to) the local digest? It's fully optional — /careeros start asks, or see docs/google-setup.md any time later.

Example run

$ /careeros daily        # run inside Claude Code / Codex / Gemini CLI / etc.
  [discover] query 1/4 (global_remote): 22 items
  [discover] query 2/4 (india_remote): 9 items
  [discover] query 3/4 (navi_mumbai_onsite): 11 items
  [discover] query 4/4 (mumbai_onsite): 42 items
[discover] fantastic-jobs: 84 raw items across 4 queries (11.4s)
[normalize] 84 raw -> 81 jobs (0.1s)
[dedupe] 81 in -> 47 unique, 34 dropped (in-run: 2, history: 30, sheet: 2)
[constraints] 47 in -> 41 eligible, 6 hard-rejected (0.0s)
[gate:finalize] 41 in -> 19 kept, 22 dropped.
[evaluate:finalize] 19 evaluations valid and cached.
[threshold] 19 evaluated -> 4 APPLY (>= 4.0), 6 CONSIDER ([3.5, 4.0))
[artifacts:finalize] 4 job(s), 8 artifact(s) verified, 8 newly cached.
[summary] wrote .careeros/results/2026-07-14/summary.md (also: .careeros/results/latest/)

4 jobs scored above threshold. Top match: Senior PM at Acme (4.6) — strong
role fit, remote, comp in range. See .careeros/results/latest/summary.md for
all 4 with resumes and cover letters generated, plus 6 near-misses under
Consider for visibility.

Why this exists

Most "AI job search" tools do one of two things badly: they spam applications with generic resumes, or they burn expensive AI calls evaluating and writing long reports for every single job they find, most of which the candidate will never apply to.

CareerOS is built around one rule: use deterministic code wherever possible, and AI only where reasoning genuinely adds value. Discovery, deduplication, thresholding, and the daily report are plain code — zero tokens. AI is spent on exactly two things that need judgment: a cheap triage gate, and one real evaluation per job that survives it.

The governing principle: two sources of truth

Everything CareerOS generates traces back to exactly two files:

  • profile.yaml — your facts. Verbatim experience bullets, targets, constraints. Generated once (via careeros start), edited rarely.
  • eval/<job-id>.json — a job's fit judgment. Generated once, by the evaluate stage. Never recomputed downstream.

Every later artifact is a derivation, never a re-derivation:

Artifact Derives from Never does
Daily Report eval JSON costs an AI call — it's a pure template render
Resume profile facts + eval keywords invent a bullet, or re-score
Cover Letter profile facts + eval's fit paragraph claim something not in your profile
Deep Report eval JSON + profile + new research re-score the job
Application Answers profile facts + eval + the form's real questions fabricate experience

This is "selector, not writer," applied everywhere: facts and judgments are generated once and reused. The Resume goes one step further: it may reword a selected bullet's language to mirror a JD's keywords, and it also selects — per job, by JD relevance — which companies, which of each company's bullets, which skills, and which 2-3 side projects appear, so a resume for a data-heavy role and a resume for a growth role pull genuinely different material from the same profile.yaml. But every number/entity in a reworded bullet must survive unchanged, and every selected company/project name must be a real profile.yaml entry — deterministic checks enforce both, so "selector" never quietly becomes "inventor" and a typo never silently drops real content. It renders through a real Typst layout (single column, real selectable text, no ligature corruption) that auto-fits to exactly one page. The one place this bends further on purpose is the Deep Report, which legitimately needs external research the cheap daily eval was never meant to gather — that research is additive and clearly separated from the inherited, non-recomputed fit judgment.

Architecture

/careeros daily   (a skill in your host coding CLI)
        │
        ▼
┌────────────────────────────────────────────────────────────┐
│  HOST CODING AGENT (Claude Code / Codex / Gemini CLI / …)   │
│  the runtime AND the model — CLI-agnostic by construction.  │
└──────┬─────────────────────────────────┬────────────────────┘
 deterministic (shell out)        reasoning (agent thinks)
       ▼                                  ▼
┌─────────────────────────┐     ┌──────────────────────────┐
│ careeros (Python)        │     │ prompts/*_vN.md           │
│ discover · normalize     │     │ gate · eval · resume ·   │
│ dedupe · threshold ·     │     │ cover · deep_report ·    │
│ sheets · lint · cache    │     │ apply                    │
└──────────┬───────────────┘     └──────────────────────────┘
           │ read/write
           ▼
   ┌─────────────────────────────────────────────┐
   │ .careeros/                                    │
   │  profile.yaml   ← source of truth #1 (facts)  │
   │  cache/         ← fingerprinted reuse          │
   │  runs/<date>/   ← the message bus              │
   │    06_evaluate/<job-id>.json ← source #2       │
   │  results/latest/ ← the stable local digest     │
   └─────────────────────────────────────────────┘
           │
   ┌───────┴───────┬──────────────────┬─────────────┐
   ▼               ▼                  ▼             ▼
 Fantastic Jobs   Local digest     Sheets (opt.)  Drive (opt.)
 [deterministic]  [deterministic]  [deterministic] [deterministic]

CareerOS has no server and no database. The filesystem is the message bus: every pipeline stage reads one JSON file and writes another, under .careeros/runs/<date>/. That makes every run inspectable, resumable, and cheap to re-run (unchanged inputs hit the cache, not the model).

Onboarding a new host CLI? Read AGENT_GUIDE.md — the canonical repo map, the deterministic/reasoning boundary, secrets handling, and the Failure Handling Principle every stage follows. CLAUDE.md/GEMINI.md/AGENTS.md are thin redirects to it for CLIs that auto-load a per-tool file.

Pipeline

  1. Discover — call a provider (Fantastic Jobs REST API by default; a legacy Apify-actor provider is also available — see careeros/providers/README.md), by default as one segmented query per profile work-mode tier rather than a single broad fetch (pipeline/queryplan.py). The REST provider queries both Fantastic sources by default — active-ats (career sites/ATS: Greenhouse, Lever, Ashby…) and active-jb (+ LinkedIn/YC/Wellfound) — merged and deduped. A production acceptance audit (full 107-job population) found the two sources score an equal ~8% ≥4.0 rate but are 92% disjoint, so querying both roughly doubles interview-worthy jobs found at the same quota (the per-tier record allocation is split 50/50, not doubled). Set api.endpoint to active-ats or active-jb to use one source. Deterministic.
  2. Normalize — map to the universal Job schema. Deterministic.
  3. Dedupe — drop jobs already seen this run, in a prior run, or already recorded (Sheet, if enabled). Deterministic.
  4. Constraints — hard-reject on the two objective deal-breakers, location and salary, before any AI is spent. Deterministic (pipeline/constraints.py).
  5. AI Gate — cheap, batched keep/drop triage against your profile's targets and deal-breakers. Bias to keep; recall over precision.
  6. Evaluate — the real reasoning step. Scores against a Career Ops-derived rubric, writes structured JSON only (no long report). This file is the source of truth for everything downstream.
  7. Threshold — two tiers, both configurable. Apply (score ≥ 4.0 AND recommended "apply" AND passing the constraints re-check) gets the full pipeline: resume, cover letter, report, and (if configured) Drive + a Sheet row. Consider (3.5 ≤ score < 4.0) gets a digest/Sheet entry only — score plus a one-line reason it fell short — with no AI artifacts by default, so near-misses stay visible at zero extra AI cost (run /careeros job <job-id> on one you want to pursue anyway — see below). Below 3.5 is omitted. A hard constraint failure (location/salary deal-breaker) is always omitted, never shown as Consider. The score and recommendation can never disagree: score means applyability, not just fit quality, so a job blocked by a deal-breaker or a stated preference (e.g. onsite outside your accepted cities) never shows as a green Apply-tier score — evaluate --finalize deterministically caps the score below threshold whenever the recommendation is "skip", even if the raw fit alone would have cleared it.
  8. Artifacts — resume + cover letter (selected from profile.yaml, never invented, cache-checked) + a daily report (rendered from the eval JSON, zero AI).
  9. Summary — a deterministic digest (funnel, the Apply list, the Consider/near-miss list, cost-per-selected-job), written to BOTH .careeros/runs/<date>/summary.md (the internal pipeline trail) and the stable .careeros/results/<date>/summary.md + .careeros/results/latest/ (what you're actually meant to read — with relative links straight to each Apply job's rendered resume/cover PDF). Zero AI. The KPI is cost per interview-worthy job, supply-aware — a day with 0 selected is a legitimate outcome, not a failure, and CareerOS never lowers the quality bar just to hit a job count. It also includes a Discovery KPI block: Apply conversion (Apply ÷ Discovered — the discovery-quality metric tracked over time against the interviews/week goal), the ATS vs. job-board source split, and requests/records used against your weekly quota. All of it is read from files other stages already wrote — no new API calls. 9a. Application Answers (Apply-tier only) — for every job that just got resume/cover, an invisible background fetch (careeros/apply/browser.py: a lightweight HTTP fetch first, an optional headless-Playwright fallback only if the form genuinely needs JavaScript to render — never your own browser, never a visible window) reads the application form's real questions and drafts answers the same "selector, not writer" way as the resume. A form that isn't automatically readable is marked with a specific status — 🔒 Login Required, ❌ Closed, ⚙️ Playwright Missing, 📄 No Essay Questions, or 🌐 Network Error — not fabricated; see "Application Answers" below.
  10. Drive (optional, off by default) — additive backup of Apply-tier artifacts (Resume/Cover as PDF, Application Answers/Evaluation/Deep Report as Markdown, Deep Report only if present) into one flat Drive folder via your own OAuth desktop grant. Idempotent (re-uploads update in place). Any failure here only warns; it never blocks the rest of the pipeline.
  11. Sheets (optional, off by default) — append one row per Apply job (with per-file Drive links if step 10 ran — Resume/Cover/Evaluation/Deep Report/Application Answers, no shared-folder link) and one row per Consider job (score + reason only). Off by default; the local digest (step 9) is the record when it's off.

Two more commands exist outside the daily loop, deliberately:

  • careeros job <job-id> — give ONE job the full Apply-tier treatment (resume, cover, report, application answers, auto-published) regardless of its actual score — the fastest path from "I found a Consider-tier near-miss I actually want" to a finished application, without waiting for a re-run. See skills/job.md.
  • careeros prep <job-id> — a full interview-prep report, generated only when you ask for it, expanding (never re-deriving) the eval.

careeros apply <job-id> (application-answer drafting for one job, any score, using your own real logged-in browser or pasted questions — the manual counterpart to step 9a's automatic Apply-tier batch) and careeros publish <job-id> (push one job's current artifacts to Drive/Sheet right now, without waiting for the next daily) are the lower-level pieces job <job-id> composes for you automatically — call them directly only if you want just one piece of the full treatment.

Commands

Command Description
careeros init Scaffold .careeros/ (config, profile template)
careeros start Guided onboarding → .careeros/profile.yaml + discovery goal/plan + Sheets/Drive-or-local choice. Opens by asking for your CV (optional — skip to answer questions instead)
careeros doctor First-run checklist: Python version, profile, discovery credentials, and (if enabled) Sheets/Drive — plus your current vs. recommended discovery limit and the last discovery failure, if any (from local state — never a live API call by default). Never modifies anything, never fails just because Sheets/Drive are off. Add --live to actually verify Fantastic Jobs + every configured Apify token against their real APIs right now, instead of trusting local/stored state alone
careeros daily (alias scan) Run the full daily pipeline
careeros job <job-id> Full Apply-tier treatment for ONE job, any score — resume, cover, report, application answers, auto-published
careeros prep <job-id> Level-2 deep interview-prep report
careeros apply <job-id> Detect ATS, draft application answers for one job (any score) using your own real browser or pasted questions
careeros publish <job-id> Upload one job's current artifacts to Drive/Sheet (if configured) and refresh the local digest — use after prep/apply <job-id> so the result shows up without a full daily run
careeros config Show resolved config, incl. the discovery quota-guard's current recommendation
careeros providers List registered discovery providers
careeros --version Print the installed version and exit

careeros --help groups these by purpose (Setup, Daily, Per-job, Advanced) and hides the internal pipeline-stage commands (discover, gate, evaluate, artifacts, threshold, sheets *, and similar) from the top-level listing — they're still fully runnable standalone (e.g. careeros discover --help) for debugging one stage against a run directory without re-running the whole pipeline, they're just not clutter for a first-time user. See AGENT_GUIDE.md for the full stage list.

Folder structure

careeros/
├── careeros/            # the deterministic Python toolkit
│   ├── cli/               # Typer commands, one module per concern
│   ├── config.py  models.py  cache.py  runmeta.py  lint.py  report.py
│   ├── sheets.py  drive.py  pdf.py  budget.py  typst_render.py
│   ├── apply/            # Application Answers: HTTP/Playwright form-reading (browser.py)
│   ├── providers/        # one file per discovery source
│   └── pipeline/         # queryplan, normalize, dedupe, constraints, threshold
├── prompts/              # AI step templates, versioned (gate_v1.md, ...)
├── skills/               # host-CLI playbooks (daily, start, prep, apply, job)
├── schemas/              # JSON Schema — the actual source-of-truth contracts
├── templates/            # example profile/config, safe to commit
└── .careeros/            # your local state (gitignored): profile, cache, runs, results

Installation

Requires Python 3.11+.

git clone https://github.com/<you>/careeros.git
cd careeros
pip install -e .

That installs the careeros CLI plus the default REST provider's dependency (requests). Two things you'll also need, neither installed by pip:

  • A host coding CLI (Claude Code, Codex, Gemini CLI, OpenCode, …) — this is what actually runs /careeros daily and performs the AI Gate/Evaluate reasoning steps. CareerOS's own Python package is the deterministic half; see Architecture.
  • A Fantastic Jobs API key (default provider) — see Quickstart above.

Setting up your profile and discovery providers

  1. Set your discovery providers' credentials. careeros runs three Core sources by default (.careeros/config.yaml's providers: block): the official Fantastic Jobs REST API (your main source — needs a key), plus RemoteOK and We Work Remotely (free, direct, no signup — nothing to configure). For Fantastic Jobs, pick one transport in api: and export the matching key:

    • api.transport: directexport FANTASTIC_API_KEY=... (developer.fantastic.jobs)
    • api.transport: rapidapiexport RAPIDAPI_KEY=... (RapidAPI's "Active Jobs DB")

    (Want more sources? /careeros start offers a handful of Optional paid job boards by name — Naukri, Glassdoor, ZipRecruiter — each with a one-line evidence-based pitch; opt in, set a monthly budget, and they run on a single shared Apify credential behind the scenes (export APIFY_TOKEN=..., or APIFY_TOKENS=tok1,tok2,... for multi-account rotation). See careeros/providers/README.md's "Shipped providers" for the full relevance/cost/reliability evidence behind every source, including the legacy Apify-actor Fantastic Jobs backend (fantastic-jobs-actor) kept as an advanced reference option.)

  2. Set up your profile: /careeros start inside your host coding CLI — opens by asking you to paste your CV (optional; skip to answer questions instead), then extracts your facts into .careeros/profile.yaml, asks your interviews/week goal + Fantastic Jobs plan (Free / Paid / Custom quota) to recommend a daily discovery limit, explained in plain English against your own search preferences (e.g. "CareerOS will run 3 discovery searches every day. On the Free plan, the recommended limit is 23 records per search.") — accept it or enter your own value, and asks whether you want a Google Sheet + Drive or to stay local-only (see Quickstart above; nothing extra to set up either way). If you skip the plan question or never set api.plan, CareerOS assumes the Free plan by default (500 records/week) rather than silently over-fetching — you'll see a one-time note about the assumption on discover/careeros config, and careeros doctor always shows current-vs-recommended. Or hand-edit .careeros/profile.yaml directly — see templates/profile.example.yaml. Change the limit anytime later by editing api.limit/api.plan in .careeros/config.yaml.

  3. Check your setup: careeros doctor — a green/red checklist for Python version, profile, discovery credentials, and (if enabled) Sheets/Drive. Fixes nothing itself; just tells you exactly what's missing — and never fails just because Sheets/Drive are off. If discover ever fails, it now classifies why — an invalid/expired API key, a network or Fantastic Jobs outage, transient rate-limiting, or your request/record quota being exhausted are each reported with a distinct, plain-English next action instead of a generic error; doctor also shows the last failure from local state, with no extra API call. Want to catch a bad or exhausted key before your first daily run instead of finding out mid-discover? Run careeros doctor --live — it actually pings Fantastic Jobs and every configured Apify token right now (a small, bounded amount of real quota: one 1-record fetch, plus a free account-usage check per Apify token, no actor run) and reports their real status instead of only local state.

  4. Run it: /careeros daily inside your host coding CLI.

Local-first results digest

Every daily run writes a plain-Markdown digest to .careeros/results/<date>/summary.md, with .careeros/results/latest/ always pointing at the newest one — funnel counts, the Apply list (with relative links straight to each job's resume.pdf/cover.pdf), the Consider/near-miss list, and cost-per-selected-job. This is the full record of a run with zero Google setup; enabling Sheets/Drive below is an upgrade to a shareable, clickable tracker, not a requirement to get value out of CareerOS.

Google Sheets schema (optional)

Set sheets.enabled: true (see docs/google-setup.md) to also get one Jobs worksheet. New rows are inserted directly below the header, not appended at the bottom — each day's newest run reads at the TOP, so you never have to scroll past a growing history to find today's jobs. Within a single run's batch, rows keep their normal Apply-then-Consider order; across runs, later daily runs stack above earlier ones.

Date · Company · Role · Score · Tier · Recommendation · Confidence · Apply URL · Status · Resume (Drive) · Cover Letter (Drive) · Evaluation (Drive) · Deep Report (Drive) · Application Answers (Drive) · Notes · Source · Company LinkedIn · Hiring Contact · Contact LinkedIn · Contact Email · Job ID

Tier is Apply or Consider (see Pipeline step 7); a Consider row has blank artifact/Drive cells and a Notes reason it scored below 4.0. Status is a dropdown (data validation, not free text) you update by hand as you actually apply: Not Applied (the default on every new row), Applied, Received Call, Interview, After Interview, Ongoing / In Process, Offer, Rejected. It's yours to track — the pipeline only ever sets the default on a NEW row and never overwrites it afterward, exactly like Notes.

Columns are located by header name, not position, and any missing column is added (deprecated ones removed) automatically — so a Sheet created by an earlier version self-migrates on the next sheets append without losing data or breaking dedupe; run careeros sheets migrate to apply that same pass right now instead of waiting for the next daily run — this also sorts an older Sheet's existing rows by Date descending, a one-time fix for history that was written bottom-up. The header row is bold and frozen, Score is conditionally colored — light green at 4.0 and above, light yellow below — and Status shows its dropdown arrow, all applied automatically so you can scan Apply-quality at a glance.

Job ID is the join key prep/apply/publish use to look a row back up. Company LinkedIn is populated for ~100% of postings at zero extra cost. Every ... (Drive) column is a direct, per-job clickable link straight to that job's own file — no shared-folder link (there's only ever one project folder, so a per-row link to it added nothing) and no local filesystem paths (useless outside your own machine — an earlier version removed the old Resume Path/Cover Letter Path/Report Path columns for the same reason). They're populated only once Drive backup (below) is enabled and that specific artifact actually exists — Deep Report (Drive) stays blank until you run prep, and Application Answers (Drive) shows a specific status label (e.g. "🔒 Login Required", "❌ Closed", "⚙️ Playwright Missing") instead of a link for an Apply-tier job whose application form wasn't automatically readable (see below). Got Apply-tier rows from before Drive backup existed? careeros backfill-drive adds Resume/Cover links to them; careeros publish <job-id> adds Evaluation/Deep Report/Application Answers links to one specific row on demand — see below.

Google Drive backup (optional)

Off by default. When drive.enabled: true, careeros drive uploads every Apply-tier job's Resume + Cover Letter (as PDF — the only two artifacts PDF is ever attempted for), Application Answers (Markdown, always — see below), Evaluation, and Deep Report (if you've run prep on it) — plus the day's run.json and summary.md — into one flat folder (drive.root_folder_id) as an additive backup; your local .careeros/runs/ Markdown is never moved, replaced, or read back by any pipeline stage. Files are named Company - Role - Resume.pdf, Company - Role - Cover Letter.pdf, Company - Role - Application Answers.md, Company - Role - Evaluation.md, Company - Role - Deep Report.md — no per-company or per-job subfolders (set drive.date_subfolder: true if you'd rather group each day's uploads under a YYYY-MM-DD/ subfolder instead). Consider-tier jobs never generate artifacts by default, so they never upload anything (unless you ran careeros job <job-id> on one — then its artifacts upload like any other).

Re-running daily (or backfill-drive/publish) for the same job updates its existing files in place rather than duplicating them — uploads are idempotent. Needs one optional extra:

pip install -e ".[drive]"

This installs the Google API client + OAuth deps (required for any upload at all) and typst + pypdf for Resume/Cover PDF rendering — one extra, everything by default, nothing else to install separately. typst (the primary renderer, careeros/typst_render.py) bundles its own compiler binary — pure pip install, no LaTeX/pango/browser system dependency — and renders a densely-filled, one-page, ATS-clean design (single column, ligatures disabled, real selectable text). Rendering happens locally, at careeros artifacts --finalize time, so artifacts/<job-id>/resume.pdf exists on disk whether or not Drive is even enabled. If PDF rendering is ever unavailable anyway (a corrupted install, or an edge-case render failure), it falls back to a plainer legacy renderer, then to Markdown, with a warning printed at each step down — Drive backup still works either way; careeros doctor also flags a missing typst proactively when Drive is enabled.

You'll also need an OAuth Desktop app client secret (Google Cloud Console → Credentials → Create Credentials → OAuth client ID → Desktop app) — not a service account, since uploads land in your own personal Drive quota. The first run opens a one-time browser consent; after that, a cached refresh token (drive.token_path, gitignored) makes every later run silent. Any Drive failure (auth, network, quota) only prints a warning — discovery, evaluation, Sheets, and every other stage run exactly as if Drive were off.

Backfilling jobs from before Drive was enabled

If you already have Apply-tier rows in your Sheet from before you turned Drive on (or before upgrading to this version), careeros backfill-drive adds Drive artifacts + clickable Sheet links to them without touching anything else in those rows:

careeros backfill-drive            # dry run (default) — preview only, writes nothing
careeros backfill-drive --no-dry-run   # actually uploads + updates the Sheet

It's safe to re-run — rows that already have both Drive links are skipped. If a row's local resume.json/cover.md no longer exist on disk (an old run directory was cleaned up), that row is listed as needing regeneration instead of inventing content — nothing is ever fabricated.

Application Answers

For every Apply-tier (score ≥ threshold) job, daily automatically drafts answers to that specific job's real application questions — no waiting until you've manually opened the form. careeros/apply/browser.py reads the form's visible text in the background:

  1. A lightweight HTTP fetch first (the already-core requests dependency — nothing extra to install). Most ATS application pages (Greenhouse, Lever, Ashby, and similar) are viewable, and therefore readable this way, even though submitting usually needs an account. The fetched text is also checked, generically (no per-ATS selectors), for a login wall, a closed-posting notice, or a real page that server-rendered plenty of text but never got past an unclicked "Apply now" button (e.g. some client-side-routed careers sites) — each is a distinct, specific outcome (see below), not a fetch failure.
  2. Only if that isn't enough — the page genuinely needs JavaScript to render — an optional headless-Playwright fallback. Installing it is two steps, not one — pip install alone gets you the Python package but not the actual browser:
    pip install -e ".[apply]"
    playwright install chromium
    
    This launches its own isolated, invisible browser context. It never touches your real browser, never opens a visible window, and never interrupts whatever you're doing on your machine. Run careeros doctor any time to check whether both steps are done — it reports the two independently, so "package installed but browser binary missing" and "package not installed at all" show up as different, specific messages rather than one opaque failure.

Neither tier has any per-ATS scraping logic (no brittle selectors tied to one site's current DOM) — both just return the page's text, and the agent identifies the real questions and drafts grounded answers from it, the same "selector, not writer" rule as the resume (see prompts/apply_v1.md).

A form that isn't automatically readable is never guessed at — that job's digest/Sheet entry shows one of these specific statuses instead of one generic "couldn't read it":

Status Meaning
✅ Generated Answers drafted and ready
🔒 Login Required The fetched page is a login wall, not the form
❌ Closed The posting itself says it's no longer accepting applications
⚙️ Playwright Missing The form needs the JS fallback, and it isn't installed — the cell includes the exact install command
📄 No Essay Questions A real, readable form with no free-text questions to draft
🌐 Network Error The fetch itself failed (DNS, timeout, connection refused)
🛡️ Bot-Blocked The fetch hit a Cloudflare-style bot-detection challenge, not the real form — never bypassed, only named
Manual review required Fallback for any other, less common failure that doesn't match one of the specific cases above

Finish any of these yourself with careeros apply <job-id> — the on-demand skill, which can use your own real, already-logged-in browser (or accept pasted questions) since you're present and chose to run it. It always finishes by running careeros publish <job-id> automatically to upload the result and refresh the local digest — no separate command to remember.

The same on-demand apply skill also works for any job below threshold that you want to pursue anyway — the automatic batch only covers Apply-tier (or use careeros job <job-id> for the full treatment in one shot — see Pipeline above).

Personal/logistics questions — asked once, reused forever

Notice period, work authorization/visa status, salary expectations, earliest start date, employment type — these aren't per-job content, they're the same answer on every application. prompts/apply_v1.md checks profile.yaml's comp and logistics fields for them first; the first time one is genuinely missing, it's asked (on-demand: right there in chat; batch: once per distinct missing fact, after drafting the rest of that pass, never once per job) and written straight into .careeros/profile.yaml — see templates/profile.example.yaml's logistics: block. No profile.version bump needed for these, since they don't affect gate/evaluate/resume/cover. Every later application, batch or on-demand, reuses the saved answer automatically. Voluntary EEO/demographic self-identification questions (race, gender, veteran/disability status) are deliberately excluded from this — always left for you to answer yourself.

Caching and prompt versioning

Every AI-stage output is cached, keyed on a fingerprint of everything that could change the answer (job content hash + profile.yaml version + active prompt version). Because the prompt version is inside the cache key, prompts/eval_v2.md + a one-line config change busts only that stage's cache — a re-run of daily with nothing else changed costs zero AI calls.

What's built today

The full pipeline runs end to end: profile-driven segmented discovery merged across multiple providers (Fantastic Jobs REST plus free RemoteOK/We Work Remotely on by default; Naukri/Glassdoor/ZipRecruiter/Indeed/Foundit and the legacy Apify-actor Fantastic Jobs backend available opt-in — see careeros/providers/README.md for the evidence behind each), deterministic normalize/dedupe/constraints/two-tier threshold, the AI Gate and Evaluate stages with the file-based prepare/finalize contract, resume/cover generation against your profile.yaml, automatic Application Answers for Apply-tier jobs (background HTTP/Playwright form-reading, with a specific status — Login Required, Closed, Playwright Missing, and so on — in place of a generic failure), a zero-cost daily report render, a stable local results digest (.careeros/results/latest/, zero Google setup needed), a per-job job <job-id> command for full on-demand treatment of any single job at any score, optional Google Drive backup (PDF resume/cover/answers, flat layout, idempotent) and optional Google Sheets append with clickable per-job Drive links, a hand-editable Status tracking column, and header/Score formatting. careeros init seeds an example profile.yaml (a Product Manager persona in templates/); replace it with your own facts — via /careeros start (CV-first) or by editing directly — before your first real run.

Roadmap

  • Direct-API providers for Greenhouse, Ashby, Lever, Workday (no Apify actor needed — see careeros/providers/README.md)
  • Incremental (date_created_gte) discovery — deferred out of the REST provider migration to keep it a pure parity swap. (LinkedIn/Wellfound/YC via the active-jb endpoint is now live — the default endpoint: both queries it alongside active-ats; see Pipeline step 1.)
  • careeros config get/set/show — a validated, scriptable config editor so hand-editing .careeros/config.yaml YAML is never required (careeros config today is read-only)
  • Richer profile sections (adaptive framing, negotiation scripts) — kept out of v1 deliberately to stay lean
  • SQLite if Sheets-as-store ever hits real scaling limits
  • Outcome tracking (applied/response/interview/offer) and calibrating scoring/artifacts on real conversion data

Contributing

Adding a provider is one file — see careeros/providers/README.md. The pipeline never imports a provider directly, so new sources never touch pipeline/, careeros/cli/, or any stage.

Read AGENT_GUIDE.md before touching pipeline code or AI prompts — it's the canonical source for the rules that actually govern this codebase: the deterministic/reasoning boundary, and that Gate/Evaluate/ Resume/Cover/Application-Answer output must always come from real per-job reasoning, never a script standing in for it.

Testing

pip install -e ".[dev]"
pytest careeros/tests/

Unit tests cover the deterministic logic that's genuinely subtle: hard constraints (constraints.py), two-tier threshold selection, cache-key stability, dedupe, the resume-truthfulness verbatim check, both Fantastic Jobs providers' source-side-filter/transport/token-rotation wiring, a parity test asserting the REST and legacy-actor providers map identical raw records to an identical Job dict, the Sheets name-keyed read/write and additive header migration, the daily-summary render (incl. the local-first results digest), Drive artifact upload/backfill/idempotency, and PDF rendering — the pure functions most likely to silently regress. They do not (yet) cover normalize.py; contributions there are welcome. CI (.github/workflows/ci.yml) runs the suite on Python 3.11 and 3.12 for every push and PR.

Attribution

The Final Evaluation rubric and matching methodology are adapted from Career Ops. CareerOS deliberately diverges from it on architecture (host-CLI-driven, not a standalone bot), output format (structured JSON, not long markdown reports for every job), and cost model (gate before evaluate, cache everything, resume/cover selection built on a separate philosophy — see prompts/voice-dna.md and the fact-preservation rule embedded in prompts/resume_v2.md).

License

MIT.

About

A deterministic, host-CLI-driven job discovery and recommendation engine. Plain code wherever possible, AI only where reasoning adds value.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors