Skip to content

Releases: vbkatarnaware/careeros

v1.3.2 — PDF-by-default for Resume/Cover, fix silent Markdown fallback

Choose a tag to compare

@vbkatarnaware vbkatarnaware released this 13 Jul 06:05

[1.3.2] - 2026-07-12

Fixed

  • Resume/Cover Letter silently uploaded to Drive as Markdown instead of
    PDF.
    The [pdf] extra (fpdf2) was a separate, easy-to-forget install
    step from [drive] — a missing fpdf2 degraded every Resume/Cover
    upload to .md with only a per-file warning, easy to miss. fpdf2 is
    now bundled into the [drive] extra, so pip install -e ".[drive]"
    alone gets PDF rendering by default; careeros doctor (with
    drive.enabled: true) now also proactively checks for it and FAILs with
    a clear fix if it's ever missing, instead of only surfacing a buried
    warning during daily.

  • Only Resume and Cover Letter ever attempt PDF now. Application
    Answers, Evaluation, and Deep Report always upload as Markdown — not
    "PDF with a fallback," simply never attempted, since only Resume/Cover
    are ever attached to a job application.

  • A Resume/Cover previously uploaded as .md (before PDF rendering was
    available) is no longer left orphaned in Drive once PDF rendering
    works.
    Drive matches existing files by exact filename, so a new
    .pdf upload didn't replace an old .md — both used to sit side by
    side in the flat Drive folder. Re-uploading now explicitly deletes the
    stale .md when a .pdf supersedes it for the same job.

  • Edge case, found during this release's own verification: the stale-.md cleanup above now also handles two different jobs that happen to share the exact same Company/Role string (a genuine name collision, disambiguated with a numeric suffix like ... (2).md) — the cleanup previously only checked the un-suffixed base filename.

v1.3.1 — Applyability scoring contract, live provider quota, anti-scripting guardrail

Choose a tag to compare

@vbkatarnaware vbkatarnaware released this 12 Jul 11:52

[1.3.1] - 2026-07-12

Added

  • careeros doctor --live. Opt-in flag that actually verifies
    Fantastic Jobs and every configured Apify token against their real APIs
    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) instead of
    only reporting local/stored state — catches a bad or exhausted key before
    your first daily run, not mid-discover.
  • Live provider-quota surfacing. The discovery summary table now shows
    each provider's real, provider-reported remaining quota (e.g. Fantastic
    Jobs' x-ratelimit-* headers) when available, next to its record count
    and timing.

Fixed

  • Score/recommendation could disagree. evaluate's score is meant to
    mean applyability — a green (≥ threshold) score should always mean the
    candidate can actually apply — but a job with a real deal-breaker (an
    onsite location outside your accepted cities, a stated preference
    violation) could still show a high score alongside
    recommendation: "skip". careeros evaluate --finalize now
    deterministically caps a skip-recommendation eval's stored score below
    threshold, so the two signals can never disagree, regardless of which
    agent/model produced the evaluation. The 5 rubric dimensions themselves
    stay honest and untouched — only the final score is clamped — so the
    "why" behind a skip is still legible. prompts/eval_v2.md documents the
    contract explicitly, including an anchor scale for scoring logistics
    honestly as a preference-ranking signal rather than a pass/fail gate.
  • Fantastic Jobs' weekly quota counter was locally calculated, not
    live-verified.
    The local .careeros/discovery_budget.json weekly
    record counter is independent of which API key is configured, so
    rotating in a fresh key did nothing to clear a "quota exhausted" state.
    The counter is now advisory-only; the live API call (and its
    x-ratelimit-* response headers) is authoritative, so a fresh key works
    immediately.
  • An exhausted Apify token stayed "exhausted" for the rest of the billing
    cycle
    , even after a mid-month top-up on the same token, with zero
    re-verification. A token now only pre-emptively skips on the same day
    it was marked exhausted — any other day gets one fresh live retry before
    being trusted as exhausted again. Old on-disk state (the pre-1.3.1
    bare-list format) migrates automatically.
  • AI reasoning stages could be silently replaced by scripts. A new
    standing rule in AGENT_GUIDE.md ("Reasoning stages must be reasoned,
    never scripted"), referenced from prompts/gate_v1.md,
    prompts/eval_v2.md, and skills/daily.md's Gate/Evaluate steps, makes
    explicit that Gate/Evaluate/Resume/Cover/Application-Answer output must
    always come from real per-job reasoning — never a keyword-matching or
    formulaic script standing in for it, even under batch-size pressure.

v1.1.0

Choose a tag to compare

@vbkatarnaware vbkatarnaware released this 10 Jul 13:59

Added

  • Automatic Application Answers. For every Apply-tier job, daily now
    drafts real answers to that job's actual application questions — no
    waiting until you've manually opened the form. A new background form-reader
    (careeros/apply/browser.py) fetches the form's visible text: a
    zero-dependency HTTP tier first, an optional headless-Playwright fallback
    only when the form genuinely needs JavaScript to render. Neither tier uses
    per-site scraping selectors — the agent identifies the real questions from
    plain text and drafts grounded answers the same "selector, not writer" way
    as the resume.
  • careeros publish <job-id> — upload one job's current artifacts
    (resume, cover, evaluation, deep report, application answers — whichever
    exist) to Drive and patch just that Sheet row, without a full daily run.
    The prep and on-demand apply <job-id> skills now run this
    automatically as their last step.
  • A specific status for every unreadable form, replacing one generic
    fallback: ✅ Generated, 🔒 Login Required, ❌ Closed, ⚙️ Playwright
    Missing, 📄 No Essay Questions, 🌐 Network Error, 🛡️ Bot-Blocked, or the
    generic Manual review required only when none of those match. Each is
    detected generically (login walls, closed-posting notices, Cloudflare-style
    bot challenges, and pages that render real text but never reach an actual
    form) — no per-ATS logic.
  • A Status column in the Sheet (dropdown: Not Applied by default,
    Applied, Received Call, Interview, After Interview,
    Ongoing / In Process, Offer, Rejected) for tracking your own
    application progress by hand. The pipeline sets the default on a new row
    and never touches it again, exactly like Notes.
  • New Sheet columns: Evaluation (Drive), Deep Report (Drive),
    Application Answers (Drive) — direct, per-job links to artifacts that
    were already being generated (and, for Evaluation, already uploaded) but
    had nowhere to show up.
  • Newest-on-top Sheet ordering. New rows are now inserted directly below
    the header instead of appended at the bottom, so each day's run reads at
    the top without scrolling past a growing history.
  • careeros sheets sync-status — patch the Application Answers status
    of existing Sheet rows after re-running apply --prepare/--finalize
    (e.g. reclassifying old jobs into the newer, more specific statuses)
    without appending a duplicate row.
  • Personal/logistics questions, asked once. Notice period, work
    authorization/visa status, salary expectations, earliest start date, and
    employment type are the same answer on every application. The first time
    one is genuinely needed and missing, it's asked once and saved to
    profile.yaml's new logistics: section — every later application,
    batch or on-demand, reuses it automatically. Voluntary EEO/demographic
    self-identification questions are deliberately excluded and always left
    for you to answer yourself.
  • careeros --version — prints the installed version and exits.
  • careeros doctor now checks Playwright independently at two levels: the
    [apply] extra's Python package, and the chromium browser binary
    (playwright install chromium) — "package installed but browser binary
    missing" and "package not installed at all" are reported as distinct,
    actionable messages instead of one opaque failure.

Changed

  • Sheet columns removed: Resume Path, Cover Letter Path,
    Report Path (local filesystem paths, useless outside your own machine)
    and Drive Folder (redundant once every artifact has its own direct
    link). careeros sheets migrate / sheets append remove these
    automatically from an existing Sheet — see Migration notes below.
  • CI now installs the optional [apply] extra and runs
    playwright install chromium --with-deps, so the real headless-browser
    fetch path runs in automated tests instead of being skipped.
  • The package version now has a single source of truth
    (pyproject.toml); careeros.__version__ reads it back via
    importlib.metadata instead of duplicating the string.

Fixed

  • The Playwright fallback used wait_until="networkidle", which never
    fires on pages with persistent background network activity (analytics
    beacons, a bot-check's own verification polling) — it could hard-timeout
    with zero text captured even though the real content rendered within a
    second or two. Switched to wait_until="load" plus a short fixed buffer.
  • PDF rendering crashed instead of falling back to Markdown when resume,
    cover, or answers content contained a non-Latin currency symbol (₹, €, £,
    ¥) — blocking the entire Drive upload for that job. Now sanitized to a
    latin-1-safe equivalent, and any other still-unmappable character falls
    back to Markdown instead of failing the upload.
  • careeros sheets sync-status could overwrite a just-published
    Application Answers link with a blank cell, because the local
    drive_links.json cache isn't refreshed by careeros publish. It now
    never touches a generated-status job's cell — only publish does.
  • A login-wall, closed-posting, or bot-blocked page returns real,
    substantial (non-empty) text — it's just the wrong page. The batch apply
    stage now checks for these BEFORE treating any non-empty fetch as a
    readable form, so a LinkedIn login-wall page can no longer be silently
    sent to the drafting step as if it were the real application form.

Migration notes: upgrading an existing Sheet

If you were running CareerOS before this release, your Google Sheet needs
one one-time cleanup pass:

careeros sheets migrate

This removes the four deprecated columns, adds the four new ones, applies
header/Score/Status formatting, backfills Status to Not Applied on
existing rows, and sorts your Sheet's existing rows by Date descending (a
one-time fix for history that was written bottom-up before this release).
It's safe to re-run — every step is idempotent, and a Sheet already on the
current schema is a no-op. After this one pass, every future daily run
keeps the Sheet current automatically; you never need to run it again
unless you skip several releases at once.

CareerOS v1.0.0

Choose a tag to compare

@vbkatarnaware vbkatarnaware released this 09 Jul 08:06

CareerOS v1.0.0

An AI-powered, deterministic job discovery and recommendation engine.
Deterministic where possible, AI only where reasoning adds value.

This is the definitive v1.0.0 — hardened through a full production audit
(P2.8) after the initial public commit, with discovery now permanently
frozen
on evidence.

Highlights

  • End-to-end daily pipeline: discover → normalize → dedupe → constraints →
    AI gate → evaluate → two-tier threshold → resume/cover artifacts → daily
    report → Google Sheets — driven from your host coding CLI (Claude Code,
    Codex, Gemini CLI, etc.), zero server, zero database.
  • Discovery (frozen default): queries both Fantastic Jobs endpoints —
    active-ats (career sites/ATS) and active-jb (+LinkedIn/YC/Wellfound) —
    every run, splitting the record budget 50/50 rather than doubling it.
    Backed by a full 107-job production acceptance audit: the two sources
    score a statistically equal ~8% ≥4.0 rate but are 92% disjoint, so
    querying both roughly doubles interview-worthy jobs found at the same
    quota cost. A single endpoint remains selectable via config.
  • Quota guard: recommends a daily discovery limit from your plan/quota/
    goal, warns before exhaustion, and never silently overrides your
    configured limit — fully config-driven, no hardcoded numbers.
  • Two-tier selection: jobs scoring ≥ threshold (default 4.0) get the
    full pipeline (resume, cover, report, Drive, Sheet row); jobs in the
    Consider band (default 3.5–3.9) get a Sheet row with score + a concise
    reason only — no AI artifacts, no Drive cost — so near-misses stay
    visible for free.
  • Profile-driven segmented discovery — one query per work-mode tier instead
    of a single broad fetch.
  • Cache-everything AI stages, fingerprinted on job content + profile version
    • prompt version — an unchanged re-run costs zero tokens.
  • Optional Google Drive backup of shortlisted artifacts, additive and off
    by default. New step-by-step setup guide: docs/google-setup.md.
  • 204 tests, including a parity test proving the REST and legacy-actor
    providers map identical raw data to an identical Job shape, and a
    regression suite for the two-tier selection, quota guard, and Sheets
    schema-migration logic.

What changed in the P2.8 hardening pass

  • Fixed a real Sheets bug: dedup and row-writes now key columns by header
    name, not position, with additive self-healing migration for sheets
    created under an older column layout (dedup was silently broken on drift).
  • Fixed the day-level summary re-deriving Apply/Consider from raw evals
    independently of the actual selection — it now renders the same
    partitioned lists the pipeline already computed, so the report can never
    disagree with what's in your Sheet.
  • Fixed the quota guard crashing discover on a non-ISO --date label.
  • Removed dead code found during the audit (an unused selection function,
    unused helper constants) and corrected several stale docs.

Installation

pip install -e .

See the README for Quickstart and provider setup.

Advanced configuration

The default REST provider supports two interchangeable transports for the
same Fantastic Jobs dataset: the direct API
(developer.fantastic.jobs) or RapidAPI's
"Active Jobs DB" listing — pick whichever fits your pricing/free-tier needs
via config.api.transport. Discovery endpoint (both/active-ats/
active-jb) and the quota guard's plan/limit are also configurable — see
careeros/providers/README.md and templates/config.example.yaml.

Discovery is now frozen

No further discovery changes (endpoint, query strategy, provider set) unless
a production issue is found. Future work moves to P3 (outcome tracking) and
the v1.1 onboarding/Drive+PDF overhaul.