diff --git a/.claude/commands/gather-recent-context.md b/.claude/commands/gather-recent-context.md index 0a32ee4..1314233 100644 --- a/.claude/commands/gather-recent-context.md +++ b/.claude/commands/gather-recent-context.md @@ -25,7 +25,7 @@ Follow these steps **in order**. Silently optional, same bar as `/sync-twenty`: a missing prerequisite ends in one clear message and a clean exit. -1. **Fastmail MCP** must be available (tools like `search_email`, `read_thread`). If not, say so and stop. +1. **Fastmail** must be reachable one way or the other: the **Fastmail MCP** tools (`search_email`, `read_thread`, `search_events`) in an interactive session, **or** `FASTMAIL_API_TOKEN` in the environment for the headless JMAP client (`python3 tools/fastmail_client.py search|thread|events|draft|reply` - same search surface, plus calendar and reply-drafting; drafts only, it never sends). Neither → say so and stop. 2. **Twenty** must be reachable one way or the other: `TWENTY_API_KEY` in the environment (script applies via REST) **or** the Twenty MCP session (you apply the plan). Neither → one line and stop. 3. Never initiate an OAuth flow from this command. diff --git a/.claude/commands/rank.md b/.claude/commands/rank.md index cf30218..7b7843d 100755 --- a/.claude/commands/rank.md +++ b/.claude/commands/rank.md @@ -94,6 +94,21 @@ Do not modify `job_search_tracker.csv` - that file records applications, and `/r --- +## Step 4.5: Sync the Delta to Twenty CRM + +`/rank` is the moment a posting becomes an opportunity, so the run pushes its own delta to Twenty instead of waiting for a manual `/sync-twenty`. **Silently optional**: if the Twenty MCP tools (`execute_tool` meta-tool / `mcp__twenty_*` devices) are not available in this session, or the one cheap preflight call fails, say so in one line inside the Step 5 report and skip this step - the ranking result above is complete without it, and the next `/sync-twenty` run will catch the CRM up. + +Follow the `/sync-twenty` contract exactly (it is the single source of truth for field maps, enum tables, batch caps, and idempotency rules) - this step is that command scoped to **this run's delta**: + +1. **Newly ranked jobs** (this run's Step 4 writes): ensure each employer's Company record exists (`/sync-twenty` Steps 4-5: dedup on normalized name, enrich via `tools/twenty_enrich.py` local layer, never overwrite existing records), then create the Opportunity (`/sync-twenty` Step 6: dedup on posting URL, `stage: RANKED`, score/verdict/url/location/`companyId`, batches ≤ 20) and its Fit Analysis note (`/sync-twenty` Step 8). +2. **Newly expired entries** (this run's rule 6 sweep and dead-posting marks): if the posting URL already has an Opportunity, set its `stage` to `EXPIRED` via `upsert_many_opportunities` with the explicit `id` (assert zero creates). Never delete records. +3. **Salary + name cleanup** (`/sync-twenty` Step 7) run only over the records this step touched. +4. Add one line to the Step 5 report: `Twenty CRM: opportunities created, expired, companies created (or: skipped - not connected).` + +Everything created here is what a subsequent full `/sync-twenty` would create - running both never duplicates (both dedup on the same posting-URL / normalized-name keys). + +--- + ## Step 5: Present the Shortlist ``` diff --git a/.claude/skills/job-scraper/SKILL.md b/.claude/skills/job-scraper/SKILL.md index a5165b4..cce4b22 100755 --- a/.claude/skills/job-scraper/SKILL.md +++ b/.claude/skills/job-scraper/SKILL.md @@ -196,6 +196,16 @@ Scraper-based portal CLIs rot silently: when a portal changes its markup, the pa **Probe-only mode (`/scrape health`).** Skip Steps 1-4 and this step's free pass (there is no fresh run to scan); instead probe every installed portal directly - enabled ones by default, a disabled one only when named explicitly (e.g. `/scrape health jobnet`). Each portal gets the sentinel probe above, the degraded criteria applied to whatever it returns, and - since the user explicitly asked for diagnosis - one `detail` fetch on the first result of each healthy portal (description must be readable decoded text; a failure downgrades to degraded). Report all statuses in this mode, including healthy. Volume stays bounded: one search, at most one retry, at most one detail per portal. +### Step 4.9: Twenty CRM Catch-Up (silently optional) + +The CRM pipeline starts at `ranked` (a scraped-but-unranked job is not yet an opportunity - `/rank` Step 4.5 is what promotes it), but a scrape run is still a natural moment to keep Twenty current. If the Twenty MCP tools (`execute_tool` meta-tool / `mcp__twenty_*` devices) are available in this session - never probe via shell, and skip this step with one report line when they are not: + +1. **Catch-up delta:** find `seen_jobs.json` entries with status `ranked` or `expired` whose posting URL has no Opportunity in Twenty (page `find_many_opportunities` for the URL map). Sync them per the `/sync-twenty` contract (companies → opportunities → fit notes; batches ≤ 20; dedup on posting URL / normalized company name). This heals any gap left by a `/rank` run that executed while Twenty was unreachable. +2. **Expiry refresh:** entries this run re-encountered whose stored status is `expired` but whose CRM `stage` is not `EXPIRED` get a stage update via `upsert_many_opportunities` with the explicit `id` (assert zero creates). +3. Add one line to the Step 5 summary: `Twenty CRM: caught up, expired (or: skipped - not connected).` + +This step never blocks the scrape: any Twenty failure is one report line, and the next `/rank` or `/sync-twenty` run picks up the same delta (all three dedup on the same keys, so nothing ever duplicates). + ### Step 5: Present Results Present new jobs in a table sorted by fit (high first). When Step 1b skipped