Skip to main content
ClaudeWave
Install in Claude Code
Copy
git clone --depth 1 https://github.com/MadsLorentzen/ai-job-search /tmp/upskill && cp -r /tmp/upskill/.claude/skills/upskill ~/.claude/skills/upskill
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Upskill

---

## Overview

`/upskill` analyses jobs you have tracked and your current profile to identify skill gaps, then produces a heatmap of those gaps and a learning plan with concrete, web-searched study resources and a recommended study order.

## Invocation

- **`/upskill`** — aggregate mode: analyses all jobs in `job_search_tracker.csv`, merged with ranked postings (`rank_score >= 45`) from `job_scraper/seen_jobs.json`
- **`/upskill <URL>`** — targeted mode: analyses a single job posting fetched from the URL

---

## Step 1: Detect Mode

Check whether the user provided a URL argument:

- If the invocation was `/upskill` with no argument → **aggregate mode**
- If the invocation was `/upskill <URL>` → **targeted mode**, store the URL for Step 2

In targeted mode, derive a slug from the job title and company for the report filename (e.g. `guardsix-senior-ai-engineer`). You will fetch the posting in Step 2.

## Step 2: Load Data

### Aggregate mode
1. Read `job_search_tracker.csv`. Extract all rows. The columns are:
   `date, company, sector, role, role_type, channel, status, contact_person, fit_rating, notes, cv_file, cover_letter_file, source, deadline`
2. For each row, note the `role`, `company`, and `fit_rating`. The `fit_rating` column is a 0–100 score where 100 = perfect fit. You will use it to weight gaps — a lower fit rating means the role exposed more gaps.
3. Read `job_scraper/seen_jobs.json`. Keep entries with `"status": "ranked"` and `rank_score >= 45` — the Moderate Fit floor from `04-job-evaluation.md` (below that, a job is Weak/Poor Fit and would otherwise dominate the heatmap with jobs the user shouldn't chase). For each kept entry, note its `title`, `company`, `rank_score`, and — when present — its recorded `gaps`. An entry with no `gaps` field (ranked before gap persistence existed) is skipped, counted, and reported once in the terminal: *"N ranked jobs were scored before gap persistence and contribute nothing; `/rank --all` re-scores them."* Never back-fill a missing `gaps` field by guessing from the title.
4. Read `.claude/skills/job-application-assistant/01-candidate-profile.md` to get the candidate's current skills and experience.
5. Check `upskill/` for the most recent aggregate report file (`report-YYYY-MM-DD.md`) — if one exists, note its date and load it for the diff in Step 8.

### Targeted mode
1. Use WebFetch to retrieve the job posting from the URL.
2. Extract: job title, company, required skills, preferred skills, responsibilities, and any domain context.
3. Read `.claude/skills/job-application-assistant/01-candidate-profile.md` for the candidate's current skills.
4. No tracker data is used in targeted mode.

## Step 3: Pass 1 — Hard Skill Diff

Extract required and preferred technical skills from each job source:

### Aggregate mode
This mode now merges two sources — tracker rows (Step 2.1) and ranked postings from `seen_jobs.json` (Step 2.3) — so the same job is never double-counted and recorded gaps are preferred over inferred ones:

1. **Dedupe.** Match tracker rows against ranked entries on case-insensitive company + role (casefold + strip on both fields) — the same match `/notion-sync`'s Step 2 describes. A job present in both counts once.
2. **Recorded gaps beat inferred skills.** For any job that has a recorded `gaps` array (from a ranked entry, or from a tracker row that matched one), use those gap bullets directly as the skill list for that job instead of inferring from `role`/`sector`/`notes`. For a ranked-only job with no `gaps` (already skipped and counted in Step 2.3) or a tracker-only row, fall back to inferring likely required skills from `role`, `sector`, and `notes` — optionally WebFetch the row's `source` URL for more detail, but skip if the URL is missing or dead.
3. **One weight per job**, both 0–100 on the same scale: `(100 - fit_rating) / 100` for tracker rows, `(100 - rank_score) / 100` for ranked-only rows. If a job is in both (Step 3.1 matched it), prefer the tracker's numeric `fit_rating` for the weight. A **blank or non-numeric `fit_rating`** (rows `/outcome` creates for applications made outside the workflow never got a fit evaluation) contributes no weight: fall back to a matched ranked entry's `rank_score` when Step 3.1 found one, otherwise skip the row, count it, and report the count once in the terminal — the same treatment Step 2.3 gives a missing `gaps` field, and for the same reason. Never treat a blank as 0: that reads as weight 1.0, the maximum, and lets the one job the framework knows nothing about dominate the heatmap.
4. **Score.** Build a **skill frequency map**: for each extracted skill (recorded gap bullet or inferred skill), count how many jobs mention it, then multiply each job's contribution by its weight from Step 3.3. Track whether each contribution came from a recorded gap or an inferred one, for Step 5's provenance column.

Final score for each skill: `sum of (weight × occurrence)` across all jobs.

### Targeted mode
Extract the explicit required and preferred skills from the fetched posting. Each skill gets equal weight (no fit weighting needed since there is only one job). List required skills before preferred skills, then sort alphabetically within each group.

### Diff against profile
Remove any skill from the list that is already present in the candidate profile (`01-candidate-profile.md`). Be generous — if the profile mentions a skill in any form (e.g. "Python" covers "Python scripting"), remove it.

What remains is the **hard skill gap list**. In aggregate mode, rank by score descending. In targeted mode, list required skill gaps before preferred skill gaps, then sort alphabetically within each group.

## Step 4: Pass 2 — LLM Synthesis

Now reason holistically about gaps that the hard skill diff would miss. Consider:

- **Domain knowledge gaps**: Does the candidate lack familiarity with the industry, domain, or problem space the jobs operate in? (e.g. cybersecurity, climate tech, quantitative