Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Lever, Ashby, and Workday ATS adapters** (`atlas.discovery.ats`): three new
discovery sources alongside Greenhouse (PROJECT.md §5.4-A), each a drop-in on the
existing registry — the daemon poll, watchlist service, and `atlas company add`
are generic over the `ats_type` string, so no downstream code changed. Each
adapter's `detect(url)` recognizes both the public board URL and the raw API URL:
**Lever** (`jobs.lever.co/<site>` / `api.lever.co/v0/postings/<site>`, a raw JSON
array); **Ashby** (`jobs.ashbyhq.com/<name>` /
`api.ashbyhq.com/posting-api/job-board/<name>`, deriving the external id from the
job's URL since Ashby exposes none, and skipping unlisted postings); and
**Workday** (`<tenant>.<wdN>.myworkdayjobs.com` — a POST-based, paginated CxS API,
with a compound `<tenant>:<wd>:<site>` board reference). Known limitations: Lever
polls the US base only, and Workday apply URLs omit any locale segment.
- **POST support on the `Fetcher` seam** (`atlas.scrape.fetcher`): the `Fetcher` /
`BrowserFetcher` protocols and `default_fetcher` gained optional, GET-defaulted
`method` / `json_body` / `headers` params so an ATS adapter can issue a JSON POST
(Workday's CxS API) over the same boundary. Backward compatible — existing GET
callers are unchanged; the test `FakeFetcher` now records these and can replay a
sequence of pages for pagination.
- **Discover screen** (`atlas.tui.screens.discover`): the TUI's ranked queue of
scored postings (PROJECT.md §8 screen #2) — the piece that makes the daemon's
discovery/scoring work visible and actionable, closing Journey B (background
Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,14 @@ atlas discover # poll the watchlist now for new postings (--json)
```

`atlas company add` auto-detects the ATS provider and board token from a
careers/board URL (e.g. `https://boards.greenhouse.io/<token>`) — **Greenhouse**
is supported today, with Lever/Ashby/Workday to follow — and adds the company to
your watchlist; an unrecognized URL is refused with the list of supported
providers. `atlas discover` runs one poll now, fetching each enabled board,
normalizing and de-duplicating its postings (against both what discovery and
`atlas add` already saved), and saving the new ones; it's AI-free and points you
at `atlas score` (or the daemon) to score them.
careers/board URL — **Greenhouse** (`boards.greenhouse.io/<token>`), **Lever**
(`jobs.lever.co/<site>`), **Ashby** (`jobs.ashbyhq.com/<name>`), and **Workday**
(`<tenant>.<wdN>.myworkdayjobs.com/<site>`) are supported, and each provider's raw
API URL is accepted too — then adds the company to your watchlist; an unrecognized
URL is refused with the list of supported providers. `atlas discover` runs one poll
now, fetching each enabled board, normalizing and de-duplicating its postings
(against both what discovery and `atlas add` already saved), and saving the new
ones; it's AI-free and points you at `atlas score` (or the daemon) to score them.

Run background work with the daemon:

Expand Down
17 changes: 9 additions & 8 deletions docs/PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1106,14 +1106,15 @@ The document specs everything; build order is phased. Each phase is independentl
(`poll_interval_minutes`), and `atlas daemon start|stop|status`. **Remaining:** the **IPC
surface** (Unix socket / Windows named pipe) for the TUI to trigger work + stream progress, and
wiring the poll to real discovery sources once the adapters below land.)*
- [ ] **Company watchlist** + ATS adapters (Greenhouse, Lever, Ashby, Workday).
*(**Greenhouse ✅** — an extensible `AtsAdapter` Protocol + registry
(`atlas.discovery.ats`) with URL-based provider detection; the Greenhouse boards-API
adapter; a watchlist on the existing `company`/`job_source` tables (no migration);
`run_discovery_poll` (best-effort per source, dedup by external id + apply-URL hash),
wired into the daemon before the scoring poll; and `atlas company add|list` /
`atlas discover`. **Remaining:** the Lever/Ashby/Workday adapters (drop into the same
registry) and URL auto-detection for them.)*
- [x] **Company watchlist** + ATS adapters (Greenhouse, Lever, Ashby, Workday). *(✅ an
extensible `AtsAdapter` Protocol + registry (`atlas.discovery.ats`) with URL-based provider
detection; a watchlist on the existing `company`/`job_source` tables (no migration);
`run_discovery_poll` (best-effort per source, dedup by external id + apply-URL hash), wired
into the daemon before the scoring poll; and `atlas company add|list` / `atlas discover`.
**All four adapters** land: Greenhouse (boards API) and Lever (raw-array postings API) and
Ashby (job-board API, id derived from the job URL) over GET; Workday (per-tenant CxS API) over
a POST-with-pagination extension of the `Fetcher` seam, with a compound `<tenant>:<wd>:<site>`
board reference. Follow-ups: Lever EU region and Workday apply-URL locale.)*
- [ ] **Aggregator** adapters + saved keyword searches.
- [x] Dedup + scored **Discover** queue in the TUI. *(✅ `DiscoverScreen` — a ranked queue of
scored postings (`matching.repository.list_scored_postings`) with tailor / dismiss / save /
Expand Down
70 changes: 44 additions & 26 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,16 @@
> whenever a roadmap item lands, tick it here and move the "Next up" pointer. A stale
> STATUS.md is a bug.

- **Last updated:** 2026-08-05 (**the TUI Discover queue landed** — `DiscoverScreen`
ranks scored postings by fit with tailor / dismiss / save / open-URL actions and
drill-in, backed by `list_scored_postings`, a new `JobPosting.queue_status`
(migration), and a `UrlOpener` seam. Journey B — background discovery → review →
tailor — is now closed end-to-end. Earlier the same phase: the daemon + scheduler
and the company watchlist + Greenhouse ATS discovery. **Next:** Lever/Ashby/Workday
adapters, aggregators, multiple profiles, and the daemon IPC surface)
- **Last updated:** 2026-08-05 (**the Lever, Ashby, and Workday ATS adapters landed**
— all four ATS providers the roadmap names now discover jobs, each a drop-in on the
registry; Workday required extending the `Fetcher` seam to POST + paginate. Earlier
the same phase: the daemon + scheduler, the Greenhouse adapter + watchlist, and the
TUI Discover queue. **Next:** aggregator adapters, multiple profiles, and the daemon
IPC surface)
- **Current phase:** Phase 2 — Discovery & background 🚧 (daemon + scheduler ✅;
company watchlist + Greenhouse ATS discovery ✅; **scored Discover queue in the
TUI ✅** — `DiscoverScreen` + `list_scored_postings` + `queue_status` +
`UrlOpener`; **more ATS adapters, aggregators, multiple profiles, IPC next**).
Phase 1 core loop ✅ complete.
company watchlist + **all four ATS adapters ✅** (Greenhouse · Lever · Ashby ·
Workday); scored Discover queue in the TUI ✅; **aggregators, multiple profiles,
IPC next**). Phase 1 core loop ✅ complete.
- **Design source of truth:** [`docs/PROJECT.md`](./PROJECT.md) — especially the
[phased roadmap](./PROJECT.md#15-phased-roadmap).
- **Working agreement:** [`AGENTS.md`](../AGENTS.md) (branching, commits, tests, PR flow).
Expand All @@ -30,28 +28,27 @@
## ▶ Next up (do this next)

**Phase 2 (Discovery & background) is well underway.** The **daemon + scheduler**, the
**company watchlist + Greenhouse ATS discovery**, and now the **scored Discover queue in the
TUI** have landed (see "What has landed"). The daemon discovers postings from watchlisted ATS
boards and scores them; `DiscoverScreen` (press `w`) presents them ranked by fit with tailor /
dismiss / save / open-URL actions — so Journey B (background discovery → review → tailor) works
end-to-end.
**company watchlist + all four ATS adapters** (Greenhouse, Lever, Ashby, Workday), and the
**scored Discover queue in the TUI** have landed (see "What has landed"). The daemon discovers
postings from watchlisted ATS boards across every supported provider and scores them;
`DiscoverScreen` (press `w`) presents them ranked by fit with tailor / dismiss / save / open-URL
actions — so Journey B (background discovery → review → tailor) works end-to-end.

**Do these next to grow discovery (PROJECT.md §4.1, §5.4, §15):**
1. **More ATS adapters** — **Lever**, **Ashby**, **Workday**. Each is a new module in
`atlas.discovery.ats` implementing the `AtsAdapter` Protocol (a `detect(url)` hook + a
`list_postings` that fetches the board's public API through the injected `Fetcher`) plus one
entry in the `_ADAPTERS` registry tuple — no interface change. Greenhouse
(`ats/greenhouse.py`) is the worked example.
2. **Aggregator adapters** + saved keyword searches — the second discovery strategy (RemoteOK,
1. **Aggregator adapters** + saved keyword searches — the second discovery strategy (RemoteOK,
Remotive, Adzuna, …), key-gated ones inactive until a key is pasted. A new
`atlas.discovery.aggregators` package alongside `ats/`, feeding the same `persist_discovered`.
3. **Multiple profiles** fully wired (the Discover queue and scoring are single-active-profile
2. **Multiple profiles** fully wired (the Discover queue and scoring are single-active-profile
today). Add the "owned by" claim convention (PROJECT.md §4.1) and a `busy_timeout` PRAGMA now
that the daemon writes discovery rows while the TUI reads.
4. **IPC surface** — a local socket (Unix domain / Windows named pipe) so the TUI can trigger
3. **IPC surface** — a local socket (Unix domain / Windows named pipe) so the TUI can trigger
"poll/tailor now" and stream progress. Follow the `platform/opener.py` seam (a Protocol +
`sys.platform`-dispatched, pragma'd transport); a pure `handle_request` is the tested core.
5. **Desktop notifications** (`desktop-notifier`, §5.16) for new high-fit matches / deadlines.
4. **Desktop notifications** (`desktop-notifier`, §5.16) for new high-fit matches / deadlines.

**Smaller ATS follow-ups (optional):** Lever EU-region boards (encode the region in `board_ref`);
Workday apply-URL locale (carry an optional locale in `board_ref`); SmartRecruiters / Recruitee /
Personio adapters (drop into the same registry).

> **Deferred Phase-1 depth (optional, revisit as needed — not blockers for Phase 2):**
> **PR 2b — tailoring depth**: `honesty_validate` traceability (§11), AI-phrase scrub (§5.7
Expand Down Expand Up @@ -104,6 +101,27 @@ The Phase-0 AI-provider checklist below is retained as historical reference.

## ✅ What has landed

Phase 2 · Lever, Ashby & Workday ATS adapters — `atlas.discovery.ats` (all four roadmap ATS
providers now discover jobs):

- Three new adapters join Greenhouse on the registry, each a drop-in (a new module + one
`_ADAPTERS` entry — the poller/service/CLI stay generic over the `ats_type` string). Each
`detect(url)` accepts both the public board URL and the raw API URL. **Lever**
(`ats/lever.py`): `GET api.lever.co/v0/postings/<site>?mode=json`, a **raw JSON array**.
**Ashby** (`ats/ashby.py`): `GET api.ashbyhq.com/posting-api/job-board/<name>`; Ashby jobs
carry no id, so the external id is derived from the job URL, and unlisted postings are skipped.
**Workday** (`ats/workday.py`): the per-tenant **CxS API**, a POST that paginates — a compound
`<tenant>:<wd>:<site>` board ref (detect emits it, list_postings parses it), offset paging to a
`_MAX_PAGES` cap with a warning if capped (no silent truncation).
- The `Fetcher` seam (`atlas.scrape.fetcher`) gained optional GET-defaulted
`method`/`json_body`/`headers` so Workday can POST over the same boundary — backward compatible
(existing GET callers unchanged); the test `FakeFetcher` records them and can replay a page
sequence for pagination.
- 959 tests at 100% line+branch; `mypy --strict` incl. win32; no new dependency. Each adapter
mirrors the Greenhouse test suite plus its own quirks (Lever raw-array, Ashby id-from-URL +
isListed filter, Workday POST body / pagination / cap-warning / compound-ref parsing).
Follow-ups: Lever EU region, Workday apply-URL locale.

Phase 2 · Scored Discover queue — `atlas.tui.screens.discover` (PROJECT.md §8 screen #2 —
makes the discovery/scoring pipeline visible + actionable, closing Journey B):

Expand Down Expand Up @@ -711,6 +729,6 @@ high-level state.
|---|---|---|
| 0 | Foundations (hygiene/CI · scaffold · config/DB/logging · AI providers) | ✅ **complete** — hygiene/CI · scaffold · config/keyring · data layer (SQLModel/SQLite WAL/Alembic) · logging · AI provider abstraction (core contract · CLI + API backends · failover · `atlas doctor` · capability probe) |
| 1 | Core loop (onboarding · resume · scrape · scoring · tailoring · tracking · TUI) | ✅ **complete** — onboarding · master resume · paste-URL scrape · fit scoring · tailoring + cover letter + rendering · application tracking (state machine + CLI) · full TUI (Dashboard · Applications/Kanban · Application detail · Posting detail · Tailor workspace with background action workers). Optional depth (PR-2b tailoring / interactive editing) deferred |
| 2 | Discovery & background (daemon · ATS · aggregators · Discover queue) | 🚧 in progress — daemon + scheduler ✅ (APScheduler · `[discovery]` config · PID-file lifecycle · score-backlog poll · `atlas daemon start\|stop\|status`); company watchlist + Greenhouse ATS discovery ✅ (`atlas.discovery` registry + adapter · `run_discovery_poll` discover→score in the daemon · `atlas company add\|list` · `atlas discover`); **scored Discover queue in the TUI ✅** (`DiscoverScreen` ranked by fit · tailor/dismiss/save/open-URL · `list_scored_postings` · `queue_status` migration · `UrlOpener` seam); more ATS adapters (Lever/Ashby/Workday), aggregators, multiple profiles, IPC surface next |
| 2 | Discovery & background (daemon · ATS · aggregators · Discover queue) | 🚧 in progress — daemon + scheduler ✅ (APScheduler · `[discovery]` config · PID-file lifecycle · score-backlog poll · `atlas daemon start\|stop\|status`); company watchlist + **all four ATS adapters ✅** (Greenhouse · Lever · Ashby · Workday, on the `atlas.discovery.ats` registry · `run_discovery_poll` discover→score in the daemon · `atlas company add\|list` · `atlas discover`); scored Discover queue in the TUI ✅ (`DiscoverScreen` · `list_scored_postings` · `queue_status` · `UrlOpener`); aggregators, multiple profiles, IPC surface next |
| 3 | Scheduling & status intelligence (CalDAV · email scan · Q&A drafting) | ⬜ not started |
| 4 | Polish & depth (analytics · more adapters · scraping · DOCX · encryption) | ⬜ not started |
10 changes: 9 additions & 1 deletion src/atlas/discovery/ats/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,22 @@

from __future__ import annotations

from atlas.discovery.ats.ashby import AshbyAdapter
from atlas.discovery.ats.base import AtsAdapter
from atlas.discovery.ats.greenhouse import GreenhouseAdapter
from atlas.discovery.ats.lever import LeverAdapter
from atlas.discovery.ats.workday import WorkdayAdapter
from atlas.discovery.errors import UnknownAtsError

__all__ = ["ATS_TYPES", "AtsAdapter", "detect_ats", "get_adapter"]

#: Every registered ATS adapter, tried in order by :func:`detect_ats`.
_ADAPTERS: tuple[AtsAdapter, ...] = (GreenhouseAdapter(),)
_ADAPTERS: tuple[AtsAdapter, ...] = (
GreenhouseAdapter(),
LeverAdapter(),
AshbyAdapter(),
WorkdayAdapter(),
)

#: The registered provider names (sorted), for help text and error messages.
ATS_TYPES: tuple[str, ...] = tuple(sorted(adapter.ats_type for adapter in _ADAPTERS))
Expand Down
Loading