Discover open roles matching the user's target_job_titles (any field — AI Engineer, PM, UX Designer, SWE, TPM, Security Analyst, etc.) by polling company ATSes directly (Greenhouse, Lever, Ashby) and running targeted web searches. Produces a ranked markdown report of fresh postings, creates tracker rows (stage=analyzing) for each so ai-job-apply can pick them up, and auto-updates the ATS mapping file when new companies are probed.
Job-to-be-done: find open AI-related roles the user hasn't seen yet, ranked by fit, with URLs ready for ai-job-apply. Runs weekly (or on demand) and feeds the top of the application funnel.
cat knowledge/user_profile.json
The profile above drives this skill: targeting.target_job_titles controls which ATS postings count; locations[] + compensation drive ranking and comp-verification flags; agent_runtime.user_agent_contact sets the HTTP UA.
The user invoked this skill with: $ARGUMENTS
If empty, run the default full sweep across all target companies. If non-empty, treat the value as a keyword filter and narrow the WebSearch queries toward it (e.g. a specialty keyword, a city name, "remote", or a specific company name).
Triggers:
NOT this skill:
ai-job-applyai-job-interview-prepai-job-pipelineai-job-submitThis skill runs two parallel discovery tracks and merges their results. Each track covers what the other misses.
discover.py)Hits company ATSes directly through their public JSON APIs. Highest reliability, most authoritative source, covers ~80% of value.
boards-api.greenhouse.io/v1/boards/{id}/jobs)api.lever.co/v0/postings/{id}?mode=json)api.ashbyhq.com/posting-api/job-board/{id})Target list + ATS mappings live in knowledge/company_ats.json. When a company's ATS guess is wrong, the script auto-probes the other two and caches the correction back to the file — so it gets better over time.
Catches what the target list misses: new companies, stealth roles, YC recent batches, non-ATS postings.
Run 2–3 targeted searches per invocation. Construct queries from user.targeting.target_job_titles:
WebSearch("{primary target title} AI site:linkedin.com/jobs") — LinkedIn aggregation (noisy but discovery-rich)WebSearch("\"{primary target title}\" \"AI\" OR \"ML\" hiring {current year}") — broad freshness searchWebSearch("Hacker News who is hiring") (monthly) — catch YC + stealth startupsFilter results to: fresh (< 30 days ideally), titles matching user.targeting.target_job_titles or related seniority levels, AI/ML-relevant company. Dedupe against Track A's URLs.
All optional:
knowledge/company_ats.jsonIf no args, run the default full sweep.
Output paths follow knowledge/output_conventions.md. Create the date directory first (mkdir -p output/{YYYY-MM-DD}), then:
python3 .claude/skills/ai-job-discover/discover.py \
--output output/{YYYY-MM-DD}/_discover_raw.json \
--pretty
For weekly reviews, narrow to fresh roles only with --since YYYY-MM-DD. Pass last week's Monday (or whenever the previous discovery run was) so you only see roles the ATSes updated since you last looked:
python3 .claude/skills/ai-job-discover/discover.py \
--output output/{YYYY-MM-DD}/_discover_raw.json \
--since {one-week-ago YYYY-MM-DD} \
--pretty
The output JSON includes since_filter and roles_found_before_since_filter so the report can show "X new roles since last week (Y total in the funnel)".
Capture:
roles_found, companies_polled, warnings, elapsed_seconds from summaryroles array from the JSON fileIf warnings mention companies returning no jobs, note them for the user (they may need board_id correction in knowledge/company_ats.json). Don't fail the whole run over a few warning rows.
Execute 2–3 WebSearch calls tailored to the user's query. Build queries from user.targeting.target_job_titles:
site:linkedin.com/jobs to aggregate LinkedIn postingsFor each search, read the top 10 results. Keep rows where:
user.targeting.target_job_titles or a known seniority variantEach Track B entry needs these fields to merge cleanly:
user_profile.locations[] the same way discover.py does (word-boundary alias match against the location string)min_base_usd, set needs_comp_verification: true on the entryEnterprise / custom-ATS sources (handled in Track B only):
Entries in company_ats.json with "ats": null and a "Probe disabled" note are skipped by the script. Pick them up via targeted WebSearch instead if they're on the user's priority list. Example pattern:
WebSearch("site:{careers-domain} '{primary target title}' (AI OR ML)")
Only include results that are real job postings (not generic careers pages).output/tracker.csv if it existsnew / already_trackedSave to output/{YYYY-MM-DD}/discover.md per knowledge/output_conventions.md (cross-company artifact, lives at the date level — no company subfolder):
# Job Discovery — {today's date}
**Polled:** {N} target companies + {K} WebSearch queries
**Found:** {total} roles matching target titles
**New (not in tracker):** {new_count}
**Elapsed:** {seconds}s
## Location priorities applied
{render from user_profile.locations — e.g. "Remote (+10) · NYC (+7) · SF (+5, verify >=$200k)"}
This run: {location name} {count} · ... (flagged for comp: {f})
## Top new roles (ranked by fit)
### 1. [Company: Role Title](https://...)
- **Score:** 24 | **Tier 1** | **AI-native** | **Remote-friendly**
- Posted: {date}
- Source: greenhouse
- Location: {location}
- ⚠ COMP >= ${X}k — verify base before applying (if applicable)
### 2. ...
[continue through all new roles]
## Location-preference rollups
Surface any location-preference-specific sub-lists worth calling out:
- **{Top-preference location} roles:** all roles where `location_match == "{top preference}"`
- **Notable remote-friendly roles outside preferred cities** if the user is remote-first
## Already in tracker (for reference)
- [Company — Role](url) — row id, current stage
## ATS mapping updates (auto-applied)
- {company}: {old ats} → {new ats}
## Warnings (fix in `knowledge/company_ats.json` if these matter)
- {company}: {warning}
## Suggested next actions
1. Apply to rows 1–5 this week (one per day)
2. Verify `board_id` for warned companies if any are Tier 1 targets
3. Add any missing companies (from WebSearch) to `company_ats.json` for next run
For each NEW role (not already tracked), create a tracker row with stage=analyzing:
python3 .claude/skills/ai-job-pipeline/tracker.py add \
--company "{company}" \
--role "{title}" \
--url "{url}" \
--fit {score_as_fit_proxy} \
--stage analyzing \
--next "review + decide to apply" \
--notes "discovered {date} via {source}"
Where fit is derived from score:
These are provisional fits. The user's next invocation of ai-job-apply will compute a real 5-dimension fit score from the JD.
Tell the user:
help me apply to {company} {role} or paste its URL."board_id correction.discover.py — ATS orchestrator (stdlib only). Parses company_ats.json, polls Greenhouse/Lever/Ashby in parallel, filters to titles matching the user's target patterns, ranks, writes JSON.knowledge/user_profile.json — READ. Target titles, locations, comp gates, agent UA.knowledge/company_ats.json — READ + WRITE. Source of truth for target companies + their ATS mappings. Script auto-updates when probes succeed.knowledge/target_companies.md — READ only. For tier classification and warmth notes (useful when merging WebSearch hits against the known list).knowledge/writing_voice.md — READ only, applied to the summary report.knowledge/output_conventions.md — READ only. Required path layout for discover.md and _discover_raw.json (date folder, no company subfolder).output/tracker.csv — READ via ai-job-pipeline/tracker.py list, WRITE via tracker.py add. Stays at project root (cumulative).user_profile.locations (in knowledge/user_profile.json) is an ordered list of locations with a weight each. discover.py adds that weight to the role's score when the role's location matches (word-boundary, case-insensitive). Any location with a min_base_usd field flags matching roles as needs_comp_verification: true in the output JSON — those appear in the report with a COMP >= ${X}k tag so the user verifies base pay before applying.
Matching uses the canonical name plus the aliases array. Aliases are matched at word boundaries to prevent short aliases (e.g. "GA") from false-matching unrelated strings.
When the user says "find me {city} roles" or similar location-narrowed query, after the script runs, filter the merged list (Track A + Track B) down to roles where location_match == "{city}" or the location string contains a matching signal.
output/{YYYY-MM-DD}/discover.md and raw JSON to output/{YYYY-MM-DD}/_discover_raw.json per knowledge/output_conventions.md. Tracker stays at output/tracker.csv. Create the date directory with mkdir -p before writing.knowledge/writing_voice.md. No em-dashes in the generated markdown. Short, scannable.ai-job-apply separately.stage=analyzing, not applied. The user advances them after reviewing.< 7 days.company_ats.json, poll them directly; if not, run WebSearch with site:{company-domain}/careers as a fallback.When the user says "add {company} to my targets":
knowledge/company_ats.json:
{"slug": "newco", "display": "NewCo", "tier": 1, "ats": null, "board_id": "newco", "confidence": "guess"}
discover.py — it will auto-probe and update the entry when it finds the right ATS.