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.
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.
$ /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.
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.
Everything CareerOS generates traces back to exactly two files:
profile.yaml— your facts. Verbatim experience bullets, targets, constraints. Generated once (viacareeros start), edited rarely.eval/<job-id>.json— a job's fit judgment. Generated once, by theevaluatestage. 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.
/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.
- 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…) andactive-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). Setapi.endpointtoactive-atsoractive-jbto use one source. Deterministic. - Normalize — map to the universal
Jobschema. Deterministic. - Dedupe — drop jobs already seen this run, in a prior run, or already recorded (Sheet, if enabled). Deterministic.
- Constraints — hard-reject on the two objective deal-breakers, location
and salary, before any AI is spent. Deterministic (
pipeline/constraints.py). - AI Gate — cheap, batched keep/drop triage against your profile's targets and deal-breakers. Bias to keep; recall over precision.
- 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.
- 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:scoremeans 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 --finalizedeterministically caps the score below threshold whenever the recommendation is "skip", even if the raw fit alone would have cleared it. - Artifacts — resume + cover letter (selected from
profile.yaml, never invented, cache-checked) + a daily report (rendered from the eval JSON, zero AI). - 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. - 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.
- 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. Seeskills/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.
| 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.
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
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 dailyand 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.
-
Set your discovery providers' credentials.
careerosruns three Core sources by default (.careeros/config.yaml'sproviders: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 inapi:and export the matching key:api.transport: direct→export FANTASTIC_API_KEY=...(developer.fantastic.jobs)api.transport: rapidapi→export RAPIDAPI_KEY=...(RapidAPI's "Active Jobs DB")
(Want more sources?
/careeros startoffers 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=..., orAPIFY_TOKENS=tok1,tok2,...for multi-account rotation). Seecareeros/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.) -
Set up your profile:
/careeros startinside your host coding CLI — opens by asking you to paste your CV (optional;skipto 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 setapi.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 ondiscover/careeros config, andcareeros doctoralways shows current-vs-recommended. Or hand-edit.careeros/profile.yamldirectly — seetemplates/profile.example.yaml. Change the limit anytime later by editingapi.limit/api.planin.careeros/config.yaml. -
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. Ifdiscoverever 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;doctoralso shows the last failure from local state, with no extra API call. Want to catch a bad or exhausted key before your firstdailyrun instead of finding out mid-discover? Runcareeros 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. -
Run it:
/careeros dailyinside your host coding CLI.
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.
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.
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.
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.
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:
- A lightweight HTTP fetch first (the already-core
requestsdependency — 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. - 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 installalone gets you the Python package but not the actual browser: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. Runpip install -e ".[apply]" playwright install chromiumcareeros doctorany 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).
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.
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.
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.
- 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 theactive-jbendpoint is now live — the defaultendpoint: bothqueries it alongsideactive-ats; see Pipeline step 1.) careeros config get/set/show— a validated, scriptable config editor so hand-editing.careeros/config.yamlYAML is never required (careeros configtoday 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
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.
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.
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).
MIT.