mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
feat: add /rank command to triage scraped jobs into a ranked shortlist (#43)
/scrape finds and dedupes postings; /apply evaluates one at a time in depth. Nothing connects the two ends: after a scrape returns 20 jobs, the user eyeballs a table to decide where to spend /apply effort. /rank is the bridge: batch-score every new posting against the fit framework and return a ranked shortlist. How it works: - Selects jobs with status "new" from job_scraper/seen_jobs.json (--all re-ranks everything unapplied; a focus argument filters), excluding anything already in job_search_tracker.csv - Dispatches parallel general-purpose agents (~5 jobs each) that WebFetch each posting and score the five dimensions from 04-job-evaluation.md. The rubric (skill match areas, career goals, deal-breakers) is passed inline per the same token-efficiency rules /apply uses; agents score only from actually fetched content and mark dead postings expired, never guessing from a title - Triage depth by design: posting text vs. profile only - no company research, no salary lookups. /apply's Step 1 evaluation stays authoritative and always re-runs on handoff - Aggregates with the framework's 30/25/15/30 weighting and verdict bands; location deal-breakers veto regardless of score; deadlines within 7 days get urgency flags and win ties - Updates seen_jobs.json additively (status "ranked"/"expired" plus rank_score/rank_verdict/rank_date) so /scrape dedup keeps working; the tracker is read-only. Re-running is idempotent Integration: job-scraper SKILL.md documents the new status values and suggests /rank after large scrape batches; README (commands list, file tree, quick-start step 4).
This commit is contained in:
@@ -0,0 +1,127 @@
|
|||||||
|
# /rank - Triage Scraped Jobs into a Ranked Shortlist
|
||||||
|
|
||||||
|
You are batch-scoring the jobs that `/scrape` has collected, so the user can decide where to spend `/apply` effort. `/scrape` finds and dedupes postings; `/apply` evaluates one at a time in depth. `/rank` is the bridge: it scores every new posting against the fit framework and returns a ranked shortlist.
|
||||||
|
|
||||||
|
`/rank` produces **triage scores**, not final evaluations. It scores from the posting text and the candidate profile only - no company research, no reviewer agent. `/apply`'s Step 1 evaluation (which adds company research) remains authoritative and always re-runs when the user applies.
|
||||||
|
|
||||||
|
Follow these steps **in order**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 0: Parse Input
|
||||||
|
|
||||||
|
`$ARGUMENTS` may contain:
|
||||||
|
|
||||||
|
- Nothing → rank all jobs with status `new` in `job_scraper/seen_jobs.json`
|
||||||
|
- A focus area (e.g. `/rank data science`) → rank only jobs whose title or stored fit-notes match the focus
|
||||||
|
- `--all` → re-rank every job that has not been applied to, including previously ranked ones (useful after the profile changes)
|
||||||
|
- `--top <N>` → shortlist size (default 5)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Load State
|
||||||
|
|
||||||
|
1. Read `job_scraper/seen_jobs.json`. If the file is missing or has no entries, tell the user to run `/scrape` first and stop.
|
||||||
|
2. Read `job_search_tracker.csv`. Build the exclusion set: any company+role already in the tracker is out of scope regardless of flags - it has been applied to or consciously tracked.
|
||||||
|
3. Select candidates: entries with status `new` (or all non-applied entries with `--all`), minus the exclusion set, filtered by the focus area if one was given.
|
||||||
|
4. If no candidates remain, say so ("Nothing new to rank - run /scrape to find fresh postings") and stop.
|
||||||
|
5. Read the scoring framework and profile **once**:
|
||||||
|
- `.claude/skills/job-application-assistant/04-job-evaluation.md`
|
||||||
|
- `.claude/skills/job-application-assistant/01-candidate-profile.md`
|
||||||
|
|
||||||
|
State how many jobs will be ranked before proceeding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Batch-Fetch and Score
|
||||||
|
|
||||||
|
Dispatch parallel `general-purpose` agents via the **Agent tool**, ~5 jobs per agent (a single agent is fine for ≤5 jobs). Token-efficiency rules, consistent with `/apply`:
|
||||||
|
|
||||||
|
- Pass each agent everything it needs **inline in the prompt** - the job list (title, company, URL) and a compact scoring rubric extracted from the files you read in Step 1: the strong/moderate/weak skill match areas, direct/adjacent experience domains, behavioral thrive/drain factors, career goals, deal-breakers, and the location constraints. Do **not** make agents re-read the profile files.
|
||||||
|
- Agents fetch each posting URL with WebFetch and score **only from actually fetched content**. If a URL is dead, redirects to a listing page, or the posting has expired, the agent marks that job `expired` - it never scores from the title alone and never fabricates posting content.
|
||||||
|
- Scope is triage: posting text vs. rubric. **No company research, no salary lookup, no web searches** - that depth belongs to `/apply`.
|
||||||
|
|
||||||
|
Each agent returns a JSON array, one object per job:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"key": "<the job's key in seen_jobs.json>",
|
||||||
|
"status": "scored" | "expired",
|
||||||
|
"scores": { "technical": 0-100, "experience": 0-100, "behavioral": 0-100, "career": 0-100 },
|
||||||
|
"location": "PASS" | "FAIL" | "FLAG",
|
||||||
|
"deadline": "YYYY-MM-DD" | null,
|
||||||
|
"strengths": ["1-3 bullets, grounded in the posting text"],
|
||||||
|
"gaps": ["1-3 bullets, honest"],
|
||||||
|
"language": "<posting language>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Scoring uses the dimension definitions from `04-job-evaluation.md` verbatim. The honesty rule applies to triage too: gaps are stated, never smoothed over, and a posting that is a poor fit gets a low score even if it looks prestigious.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Aggregate and Rank
|
||||||
|
|
||||||
|
Back in the main context, for each scored job:
|
||||||
|
|
||||||
|
1. Compute the overall score with the weighting from `04-job-evaluation.md` (Technical 30%, Experience 25%, Behavioral 15%, Career Alignment 30%; location is unweighted).
|
||||||
|
2. Map to the framework's verdict bands (Strong Fit 75+, Good Fit 60-74, Moderate Fit 45-59, Weak Fit 30-44, Poor Fit <30).
|
||||||
|
3. **Location veto:** `FAIL` (e.g. requires relocation) excludes the job from the shortlist no matter the score - list it separately with the reason. `FLAG` (e.g. heavy travel) stays in the ranking but carries a visible ⚠ marker for the user to judge.
|
||||||
|
4. **Deadline urgency:** a deadline within 7 days gets a 🔥 marker and wins ties. A deadline that has already passed moves the job to `expired`.
|
||||||
|
|
||||||
|
Sort by overall score (descending), urgency as tiebreaker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Update State
|
||||||
|
|
||||||
|
Update `job_scraper/seen_jobs.json` in place - these fields are additive to the scraper's schema:
|
||||||
|
|
||||||
|
- Ranked jobs: set `"status": "ranked"` and add `"rank_score": <overall>`, `"rank_verdict": "<band>"`, `"rank_date": "YYYY-MM-DD"`
|
||||||
|
- Dead or past-deadline jobs: set `"status": "expired"`
|
||||||
|
|
||||||
|
Do not modify `job_search_tracker.csv` - that file records applications, and `/rank` never applies. Re-running `/rank` is idempotent: already-`ranked` jobs are skipped unless `--all` re-scores them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Present the Shortlist
|
||||||
|
|
||||||
|
```
|
||||||
|
## Job Ranking - YYYY-MM-DD
|
||||||
|
|
||||||
|
Ranked <N> new postings (<X> shortlisted, <Y> below threshold, <Z> expired/vetoed).
|
||||||
|
|
||||||
|
### Shortlist
|
||||||
|
|
||||||
|
| # | Score | Verdict | Title | Company | Location | Deadline | |
|
||||||
|
|---|-------|---------|-------|---------|----------|----------|---|
|
||||||
|
| 1 | 78 | Strong Fit | ... | ... | ... | ... | 🔥 |
|
||||||
|
|
||||||
|
### Why these ranked highest
|
||||||
|
**1. <Title> at <Company> (78)** - [2-3 strength bullets and the honest gap, from the agent's findings]
|
||||||
|
[repeat for each shortlisted job]
|
||||||
|
|
||||||
|
### Below threshold
|
||||||
|
| Score | Verdict | Title | Company | One-line reason |
|
||||||
|
|
||||||
|
### Excluded
|
||||||
|
- <Title> at <Company> - location FAIL: requires relocation
|
||||||
|
- <Title> at <Company> - expired <date>
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules for the presentation:
|
||||||
|
|
||||||
|
- Every claim traces to fetched posting text or the profile - no invented details.
|
||||||
|
- Say explicitly that these are **triage scores from the posting text only**, and that `/apply` will re-evaluate with company research before anything is drafted.
|
||||||
|
- Then ask: "Want to apply to any of these? Give me the number(s) and I'll start with the full `/apply` workflow."
|
||||||
|
- If the user picks one, run the `/apply` workflow on that job's URL, passing the triage verdict as prior context but **re-running the full Step 1 evaluation** - triage never substitutes for it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Important Rules
|
||||||
|
|
||||||
|
1. **Never rank unfetched postings.** A job whose posting cannot be retrieved is marked expired, not guessed at.
|
||||||
|
2. **Triage depth only.** No company research, no salary lookups, no reviewer agents - `/rank` exists to be cheap enough to run on every scrape batch.
|
||||||
|
3. **Deal-breakers veto scores.** A 90-point job that fails a location deal-breaker is excluded, not ranked first.
|
||||||
|
4. **Honest scoring.** Gaps are reported per job; a low-scoring posting is presented as such. The score bands and weights come from `04-job-evaluation.md` - if the user disagrees with a ranking, the fix is updating their profile or the framework, not bending scores.
|
||||||
|
5. **State stays consistent.** `seen_jobs.json` fields are only added, never restructured, so `/scrape`'s dedup keeps working; the tracker is read-only for this command.
|
||||||
@@ -75,7 +75,7 @@ For each new job, do a rapid fit check (NOT the full evaluation from `04-job-eva
|
|||||||
"url": "...",
|
"url": "...",
|
||||||
"first_seen": "YYYY-MM-DD",
|
"first_seen": "YYYY-MM-DD",
|
||||||
"fit": "high/medium/low",
|
"fit": "high/medium/low",
|
||||||
"status": "new/skipped/evaluated"
|
"status": "new/skipped/evaluated/ranked/expired"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,6 +107,8 @@ After presenting, ask:
|
|||||||
|
|
||||||
If the user picks a number, invoke the **job-application-assistant** skill workflow (fit evaluation first, then CV + cover letter if approved).
|
If the user picks a number, invoke the **job-application-assistant** skill workflow (fit evaluation first, then CV + cover letter if approved).
|
||||||
|
|
||||||
|
If the run found many new jobs (roughly 8+), also suggest `/rank` - it batch-scores all new postings against the full fit framework and returns a ranked shortlist, which beats eyeballing a long table. (`/rank` sets the `ranked` and `expired` status values in `seen_jobs.json`; treat both as already-seen for dedup purposes.)
|
||||||
|
|
||||||
### Step 6: Update Tracker (Optional)
|
### Step 6: Update Tracker (Optional)
|
||||||
|
|
||||||
If the user decides to apply to any job, add a row to `job_search_tracker.csv`.
|
If the user decides to apply to any job, add a row to `job_search_tracker.csv`.
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ claude
|
|||||||
/scrape
|
/scrape
|
||||||
```
|
```
|
||||||
|
|
||||||
This searches multiple job portals for positions matching your profile, deduplicates results, and presents them sorted by fit. Pick a match to run `/apply` on it directly.
|
This searches multiple job portals for positions matching your profile, deduplicates results, and presents them sorted by fit. Pick a match to run `/apply` on it directly — or, when a scrape returns more jobs than you want to eyeball, run `/rank` to batch-score them all against the fit framework and get a ranked shortlist first.
|
||||||
|
|
||||||
### 5. Apply to a job
|
### 5. Apply to a job
|
||||||
|
|
||||||
@@ -99,8 +99,9 @@ This runs the full workflow: evaluate fit, draft CV + cover letter, review with
|
|||||||
|
|
||||||
## Other commands
|
## Other commands
|
||||||
|
|
||||||
`/setup`, `/scrape`, and `/apply` form the core workflow. Four more commands extend it once your profile is in place:
|
`/setup`, `/scrape`, and `/apply` form the core workflow. Five more commands extend it once your profile is in place:
|
||||||
|
|
||||||
|
- **`/rank`** bridges `/scrape` and `/apply`: it batch-scores all newly scraped postings against the fit framework (parallel agents fetch each posting and score the five evaluation dimensions) and returns a ranked shortlist with honest per-job strengths and gaps. Deal-breakers veto, deadlines get urgency flags, dead postings get marked expired. Pick a number and it hands off to the full `/apply` workflow.
|
||||||
- **`/expand`** enriches your profile by scanning public sources you've already linked in it (GitHub repos, portfolio site, Kaggle, Google Scholar) and looking up syllabi for named courses and certifications. Discovered competencies are added to your profile with a source tag. Useful right after `/setup` to surface skills that documents alone don't make explicit.
|
- **`/expand`** enriches your profile by scanning public sources you've already linked in it (GitHub repos, portfolio site, Kaggle, Google Scholar) and looking up syllabi for named courses and certifications. Discovered competencies are added to your profile with a source tag. Useful right after `/setup` to surface skills that documents alone don't make explicit.
|
||||||
- **`/upskill`** analyzes the gap between your profile and your tracked job postings (or a single posting via `/upskill <URL>`). Produces a prioritized heatmap of skill gaps and a learning plan with web-searched study resources and time estimates. Useful for career planning between applications.
|
- **`/upskill`** analyzes the gap between your profile and your tracked job postings (or a single posting via `/upskill <URL>`). Produces a prioritized heatmap of skill gaps and a learning plan with web-searched study resources and time estimates. Useful for career planning between applications.
|
||||||
- **`/add-template`** registers your own LaTeX CV or cover letter template in place of the stock ones. It captures the template's instructions (compile engine, fonts, style rules, page limit), runs a mandatory test compile, and wires the template into `/apply`. See [LaTeX templates](#latex-templates) below.
|
- **`/add-template`** registers your own LaTeX CV or cover letter template in place of the stock ones. It captures the template's instructions (compile engine, fonts, style rules, page limit), runs a mandatory test compile, and wires the template into `/apply`. See [LaTeX templates](#latex-templates) below.
|
||||||
@@ -120,6 +121,7 @@ ai-job-search/
|
|||||||
│ │ ├── expand.md # /expand competency enrichment from documents and online presence
|
│ │ ├── expand.md # /expand competency enrichment from documents and online presence
|
||||||
│ │ ├── add-template.md # /add-template register custom LaTeX templates
|
│ │ ├── add-template.md # /add-template register custom LaTeX templates
|
||||||
│ │ ├── add-portal.md # /add-portal generate a job-portal search skill for your market
|
│ │ ├── add-portal.md # /add-portal generate a job-portal search skill for your market
|
||||||
|
│ │ ├── rank.md # /rank triage scraped jobs into a ranked shortlist
|
||||||
│ │ └── reset.md # /reset wipe profile data or documents folder
|
│ │ └── reset.md # /reset wipe profile data or documents folder
|
||||||
│ ├── skills/
|
│ ├── skills/
|
||||||
│ │ ├── job-application-assistant/ # Core application skill
|
│ │ ├── job-application-assistant/ # Core application skill
|
||||||
|
|||||||
Reference in New Issue
Block a user