agentleFS
adityamujumdaradityamujumdar/job-finder/jobhunter/SKILL.md

jobhunter

Skill1 starsChanged 8 months agoMITView on GitHub

Full job-hunting pipeline: download 502K+ jobs, score against your profile, generate a ranked dashboard, classify matches, and run the company router. For resume tailoring use /tailor-resume. For classification only use /classify-jobs.

  • Installs packages

The file

Show its rules and text

We split this skill into the rules it gives an agent

46 rules. About 0 are common, 35 are distinctive, and 11 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈35

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 11

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    No resume found (no RESUME.md and no .pdf in project root) — ask user to add one

    line 28
  2. Distinctive:

    Run this check first, every time.

    line 52
  3. Very specific:

    If profile.yaml was just created from example: proceed to Step 1 (Profile Setup) to customize it.

    line 120
  4. Distinctive:

    If profile staleness check shows ⚠️: proceed with the pipeline — the matcher will rescore against the current profile.

    line 121
  5. Distinctive:

    If RESUME.md is missing: Stop and use AskUserQuestion to ask the user for their resume:

    line 123
  6. Distinctive:

    When the user selects option 3 ("Just let me dump my work history"), follow this flow:

    line 136
  7. Distinctive:

    ONLY use information the user provided: Never fabricate companies, roles, dates, skills, or achievements.

    line 190
  8. Distinctive:

    If a date range is vague ("a couple years at Google"), write "~2 years" and flag it for the user to correct.

    line 191
  9. Distinctive:

    If the user mentioned skills but didn't tie them to specific roles, put them in the Skills section.

    line 192
  10. Very specific:

    If contact info (email, location, LinkedIn) wasn't provided, leave placeholders: [your@email.com], [City, ST], [LinkedIn URL].

    line 195
  11. Very specific:

    4. Continue to Step 1: After RESUME.md is saved, proceed normally to profile auto-generation.

    line 213
  12. Very specific:

    When profile.yaml is missing or was just created from example, OR user asks to update:

    line 220
  13. Distinctive:

    If a resume exists (RESUME.md or PDF), you ARE Claude — parse it directly:

    line 224
  14. Distinctive:

    If resume exists, extract these fields by reading the full text:

    line 231
  15. Distinctive:

    skills: All technical skills mentioned (languages, frameworks, tools, platforms)

    line 236
  16. Distinctive:

    Use AskUserQuestion: "This profile was auto-generated from your resume.

    line 259
  17. Distinctive:

    If no resume exists or user chose "Start over manually":

    line 266
  18. Distinctive:

    Ask these questions one at a time (never batch — one AskUserQuestion per question):

    line 268
  19. Distinctive:

    Run straight through. Only surface errors that block completion.

    line 292
  20. Distinctive:

    When user mentions a specific company, check which channel it's on before scraping:

    line 321
  21. Distinctive:

    If not in JBA: Use AskUserQuestion to offer browsing their careers page:

    line 336
  22. Distinctive:

    After fetching, present jobs alongside scored results as 📌 Manual additions.

    line 346
  23. Distinctive:

    After user installs, retry the browse.

    line 360
  24. Distinctive:

    If >2,000 jobs at one company: verify it's not a staffing farm before including.

    line 362
  25. Distinctive:

    After scoring, you ARE Claude — use your semantic understanding to enhance the results.

    line 368
  26. Very specific:

    Read ALL P1 + top 30 P2 job titles and the user's target_roles from profile.yaml.

    line 420
  27. Distinctive:

    After fetching, read the description and classify skills:

    line 463
  28. Distinctive:

    If Step 4b ran, also show the LLM enhancement summary:

    line 550
  29. Distinctive:

    After showing results, scan the P1 list for clusters of roles that look like false positives.

    line 573
  30. Distinctive:

    If false positive clusters are found, offer to add them as exclude_title_patterns:

    line 583
  31. Distinctive:

    Use AskUserQuestion with options:

    line 592
  32. Distinctive:

    After updating, re-run the matcher and show updated counts:

    line 597
  33. Distinctive:

    After presenting JBA results, proactively identify major employers NOT in JBA that are highly relevant to the user's profile.

    line 606
  34. Distinctive:

    Read the user's profile fields and apply these rules:

    line 612
  35. Very specific:

    If location contains Canada, Toronto, Vancouver, Ottawa, Calgary, Montreal: → Add Canadian banks: RBC, TD Bank, BMO, CIBC, Scotiabank, …

    line 615
  36. Very specific:

    If location is US + relocation_cities includes Canada → same as above

    line 619
  37. Very specific:

    If location contains San Francisco, Seattle, New York, Austin: → Add: Google, Apple, Amazon, Microsoft, Meta, Lyft, Uber, Airbnb (not in …

    line 620
  38. Distinctive:

    Check if gstack is available (see Step 5c)

    line 655
  39. Distinctive:

    After all browsing is done, re-present updated headline counts

    line 658
  40. Very specific:

    If the user provides specific job URLs that are NOT in the scored results (e.g., jobs from company career pages not covered by JBA like …

    line 666
  41. Distinctive:

    If the page returned an error or login wall, tell the user and ask them to paste the JD instead.

    line 685
  42. Distinctive:

    Use AskUserQuestion with these options:

    line 688
  43. Distinctive:

    After installing, retry the browse.

    line 697
  44. Distinctive:

    After fetching (via browse or paste), extract the job fields and score it through the pipeline:

    line 701
  45. Very specific:

    After every successful run, append to .context/jobhunter-history.json:

    line 737
  46. Very specific:

    If .context/jobhunter-history.json exists: load it and show a one-line trend:

    line 753

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

JobHunter AI — Full Pipeline

Philosophy

You are not a passive tool — you are the user's job-hunting AI. When someone says "find me jobs" or /jobhunter, that means DO IT. Run the pipeline straight through and show results. Only stop when a blocking issue actually prevents progress.

Zero silent failures: every error is visible. Lead with the number: the first thing the user sees should be the headline — not logs, not setup noise, not warnings. Terse.

Borrowed from gstack: non-interactive by default, read full context before acting, anchor every output in actual data, save history for trend tracking, frame gaps as leveling-up opportunities not failures.


Only stop for:

  • No resume found (no RESUME.md and no .pdf in project root) — ask user to add one
  • Network is down and JBA download fails after retry
  • Scored data is 0 jobs (something went wrong — show the error)

Never stop for:

  • Missing .venv — create it and install deps automatically
  • Missing config/profile.yaml — copy the example and run profile setup
  • Missing RESUME.md when a PDF resume exists in the directory (use the PDF)
  • Missing gstack — warn and continue, pipeline works without it (browse is a bonus for non-JBA jobs)
  • Stale cached data (use it, note its age)
  • Some companies failing to live-scrape (report them, continue with what succeeded)
  • Warnings in scraper output (log them, don't surface to user)
  • P1 count being lower than expected (show what's there, let user tune profile)

Trigger

"find me jobs", "job search", "run jobhunter", "daily report", "show jobs", "set up my profile", "update profile", "what's new today", "/jobhunter"


Step 0 — Prerequisite Check & Auto-Fix

Run this check first, every time. Fix everything automatically — never tell the user to do it themselves.

First: cd to the job-finder project directory. The pipeline runs from there — all paths (config/, data/, src/) are relative to it. Read the project directory from ~/.claude/CLAUDE.md (the Project directory: line in the ## job-finder section), or default to ~/.claude/skills/job-finder.

# cd to the job-finder project directory
JF_DIR="${JOBHUNTER_DIR:-$HOME/.claude/skills/job-finder}"
cd "$JF_DIR"
echo "=== JobHunter AI — Prerequisite Check ==="
echo "📁 Project: $JF_DIR"

# 1. Python environment — create if missing
if [ ! -d ".venv" ]; then
  echo "⚙️  Creating .venv..."
  python3 -m venv .venv
  source .venv/bin/activate
  pip install -q -r requirements.txt
  echo "✅ .venv created and dependencies installed"
else
  echo "✅ .venv found"
  source .venv/bin/activate
fi

# 2. Profile — copy example if missing (profile setup will customize it in Step 1)
if [ ! -f "config/profile.yaml" ]; then
  cp config/profile.yaml.example config/profile.yaml
  echo "⚙️  Created config/profile.yaml from example — will customize in profile setup"
else
  echo "✅ profile.yaml found"
fi

# 3. Resume check
if [ -f "RESUME.md" ]; then
  echo "✅ RESUME.md found"
else
  echo "❌ No RESUME.md"
fi

# 4. gstack/browse check — ADVISORY (not blocking).
if [ -d "$HOME/.claude/skills/gstack" ] && [ -f "$HOME/.claude/skills/gstack/browse/bin/find-browse" ]; then
  echo "✅ gstack/browse available"
else
  echo "⚠️  gstack not installed (optional — enables browsing company career pages)"
fi
# Profile staleness check — detect if profile changed since last score.
# If stale, the matcher step below will rescore automatically.
python3 -c "
import json, sys
from src.config import profile_hash, SCORED_DIR, today
meta_path = SCORED_DIR / f'{today()}.meta.json'
if not meta_path.exists():
    print('ℹ️  No scored data yet — will score fresh')
    sys.exit(0)
meta = json.load(open(meta_path))
current = profile_hash()
if meta.get('profile_hash') != current:
    print(f'⚠️  Profile changed since last score (was {meta[\"profile_hash\"]}, now {current}). Will rescore.')
else:
    print(f'✅ Profile hash: {current} (matches scored data)')
" 2>/dev/null || echo "⚠️  Could not check profile staleness — will rescore to be safe"

If profile.yaml was just created from example: proceed to Step 1 (Profile Setup) to customize it. If profile staleness check shows ⚠️: proceed with the pipeline — the matcher will rescore against the current profile.

If RESUME.md is missing: Stop and use AskUserQuestion to ask the user for their resume:

  1. "I have a resume file" — Ask for the path. Read it (PDF or text), write to RESUME.md.
  2. "I'll paste my resume" — Accept pasted text, write to RESUME.md.
  3. "Just let me dump my work history" — Accept unstructured "word vomit" and format it into a proper resume. See Word Vomit → Resume flow below.
  4. "Skip for now" — Warn that the pipeline needs a resume to generate a profile. Continue but skip profile auto-generation.

Do NOT scan the filesystem for resume files. Just ask the user directly. Do NOT proceed with the full pipeline until a resume is present. NEVER write RESUME.md without first showing the user a formatted preview and getting explicit confirmation via AskUserQuestion. This applies to ALL resume intake methods above — file, paste, and word vomit. For all other issues: never stop. Never tell the user to "run X first." Fix it and keep going.

Word Vomit → Resume Flow

When the user selects option 3 ("Just let me dump my work history"), follow this flow:

1. Accept the dump. Tell the user:

"Go ahead — dump everything you remember about your work history. Job titles, companies, dates, what you built, technologies, anything. Don't worry about formatting or order. I'll organize it into a proper resume."

Let the user type freely. They may send one big message or several. Wait until they signal they're done (e.g., "that's it", "done", or a natural stopping point). If the dump is very thin (<3 jobs or <100 words), ask: "Is there anything else? More projects, skills, education?"

2. Format into structured RESUME.md. Using RESUME.md.example as the target format, organize the word vomit into a clean markdown resume with these required sections in this order:

# [Full Name] — Resume

**Contact:** [City, ST] · [email] · [LinkedIn URL]

---

## Experience

### [Company Name] — [Job Title]
**[Start Date] – [End Date or Present]**

- [Achievement bullet — impact-first, with metrics where mentioned]
- [Key project or technical work]
- [Tools/technologies used]

(repeat for each role, reverse-chronological order)

---

## Projects  (if mentioned)

- **[Project Name]** — [One-line description] — *[Technologies]*

---

## Skills & Tools

**Programming:** [languages mentioned]
**Frameworks:** [frameworks mentioned]
**Cloud & Data:** [infrastructure/data tools mentioned]

---

## Education

**[University]** — [Degree], [Major]

Rules for formatting:

  • ONLY use information the user provided. Never fabricate companies, roles, dates, skills, or achievements.
  • If a date range is vague ("a couple years at Google"), write "~2 years" and flag it for the user to correct.
  • If the user mentioned skills but didn't tie them to specific roles, put them in the Skills section.
  • Preserve the user's voice in bullet points — clean up grammar and structure but don't rewrite their accomplishments into something they didn't say.
  • Group roles in reverse-chronological order (most recent first). If order is unclear, make your best guess and flag it.
  • If contact info (email, location, LinkedIn) wasn't provided, leave placeholders: [your@email.com], [City, ST], [LinkedIn URL].

3. Show the preview and ask. Display the formatted resume in full, then use AskUserQuestion:

"Here's your formatted resume based on what you shared. Take a look:"

# Guard: warn if RESUME.md already exists
if [ -f "RESUME.md" ]; then
  echo "⚠️  RESUME.md already exists. Saving will overwrite it."
fi

Options:

  • "Looks good — save it" → Write to RESUME.md. Print: ✅ Saved RESUME.md — this stays local and is never uploaded anywhere.
  • "I want to edit some things" → Ask what to change. Apply edits, show updated preview, re-ask.
  • "Start over" → Go back to step 1 of this flow.

4. Continue to Step 1. After RESUME.md is saved, proceed normally to profile auto-generation. The existing resume_parser.py will parse the structured RESUME.md into profile.yaml.


Step 1 — Profile Setup (first-time or "update profile" trigger)

When profile.yaml is missing or was just created from example, OR user asks to update:

Auto-Generate from Resume (preferred — zero questions)

If a resume exists (RESUME.md or PDF), you ARE Claude — parse it directly:

  1. Read the resume:
cat RESUME.md 2>/dev/null || echo "NO_RESUME_MD"
  1. If resume exists, extract these fields by reading the full text:
    • name: From the heading or first bold line
    • email: First email address
    • location: Most recent "City, ST" or "City, Province"
    • years_experience: Calculate from earliest start date to now (or latest end date)
    • skills: All technical skills mentioned (languages, frameworks, tools, platforms)
    • target_roles: Job titles held (e.g., "Senior Software Engineer", "Backend Engineer")
    • target_level: entry (0-2yr), mid (2-5), senior (5-10), staff (10-15), principal (15+)
  2. You can ALSO run the regex parser as a cross-check:
python3 -m src.resume_parser --dry-run
  1. Compare your extraction with the regex output. Use YOUR extraction as primary (you understand context, international formats, non-standard layouts better), but verify against the regex output for anything you might have missed.
  2. Generate profile.yaml content combining both sources. Show it to the user:
📋 Auto-generated profile from your resume:
   Name: Jane Doe
   Location: San Francisco, CA
   Experience: 8 years → target level: senior
   Skills: Python, AWS, Kubernetes, Go, PostgreSQL, ...
   Target roles: Senior Software Engineer, Backend Engineer, ...
  1. Use AskUserQuestion: "This profile was auto-generated from your resume. Want to customize anything?"
    • "Looks good — save it" → Write profile.yaml
    • "I want to adjust roles/skills" → Ask what to change, then save
    • "Start over manually" → Fall through to manual Q&A below

Manual Setup (fallback — when no resume exists)

If no resume exists or user chose "Start over manually":

  1. Ask these questions one at a time (never batch — one AskUserQuestion per question):
    • "What kind of roles are you looking for?" (e.g. Software Engineer, Data Analyst)
    • "Where are you based? Open to remote or relocation?"
    • "How many years of experience do you have?"
    • "Any specific companies you'd love to work at?"
  2. Show proposed profile.yaml contents and confirm before writing
  3. Re-run the pipeline after saving

NEVER write profile.yaml without showing the user what you're about to write. NEVER ask all questions at once — one at a time, like a conversation.


Step 2 — Setup

cd <your-project-directory>
source .venv/bin/activate

Step 3 — Core Pipeline

Run straight through. Only surface errors that block completion.

Scrape

python -m src.scraper              # full: JBA download (~15s) + live scrape preferred companies
python -m src.scraper --skip-live  # fast: JBA download only (~15s, skip live scrape)

Match

python -m src.matcher   # score all jobs against profile (~8s, 502K jobs)
# Also writes data/scored/DATE.meta.json with profile fingerprint for staleness detection

Report

python -m src.report    # CSV + terminal summary

Dashboard

python -m src.site_generator
open site/index.html    # each job card shows a unique #xxxxxxxx job ID

Step 4 — Company Intelligence Router

When user mentions a specific company, check which channel it's on before scraping:

grep -il "<company>" data/seed/greenhouse.json data/seed/lever.json \
  data/seed/workday.json data/seed/ashby.json data/seed/bamboohr.json
CompanyATSNotes
NetflixWorkday ✅JBA download already has it — no live scrape needed
AnthropicGreenhouse ✅JBA download + live scrape (anthropic)
StripeGreenhouse ✅JBA download + live scrape (stripe)
OpenAIGreenhouse ✅JBA download + live scrape (openai)
Google, Apple, Amazon, Microsoft, Meta❌ not in JBALinkedIn / careers page

If not in JBA: Use AskUserQuestion to offer browsing their careers page:

  • "Yes, browse their careers page" — Use gstack /browse to fetch their jobs:
    BROWSE_OUTPUT=$(~/.claude/skills/gstack/browse/bin/find-browse 2>/dev/null)
    B=$(echo "$BROWSE_OUTPUT" | head -1)
    if [ -n "$B" ]; then
      $B goto <careers_page_url>
      $B text
    fi
    After fetching, present jobs alongside scored results as 📌 Manual additions.
  • "No, skip" — Continue with JBA results only.

If gstack is not installed and user wants to browse: Show the install command:

To browse career pages, install gstack:
  git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup
Then add a "gstack" section to CLAUDE.md that says to use /browse for all web browsing,
never use mcp__claude-in-chrome__* tools, and lists available skills:
/office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review,
/design-consultation, /review, /ship, /browse, /qa, /qa-only,
/design-review, /setup-browser-cookies, /retro, /debug, /document-release.
Then ask the user if they also want to add gstack to the current project so teammates get it.

After user installs, retry the browse.

If >2,000 jobs at one company: verify it's not a staffing farm before including.


Step 4b — LLM Enhancement (Claude as the Model)

After scoring, you ARE Claude — use your semantic understanding to enhance the results. This step is automatic and non-interactive. No API key needed.

You are the LLM. No API key needed — you read the data and apply semantic understanding directly. This upgrades the regex-based pipeline with your judgment:

  • Title matching: "Staff Platform Engineer" is NOT the same as "Data Platform Engineer" — regex can't tell, you can.
  • Skill extraction: "Experience with cloud-native architectures" means AWS/GCP/Azure — regex only matches exact words.
  • Section awareness: "Python" in "About Us" ≠ "Python" in "Requirements" — you understand context.

4b.1 — Load Candidates

python3 -c "
import json, pathlib
today = __import__('datetime').date.today().isoformat()
scored_path = pathlib.Path(f'data/scored/{today}.json')
enriched_path = pathlib.Path(f'data/enriched/{today}.json')

if not scored_path.exists():
    print('⚠️  No scored data — skipping LLM enhancement')
    exit(0)

jobs = json.loads(scored_path.read_text())
p1 = [j for j in jobs if j.get('_priority') == 'P1']
p2 = sorted([j for j in jobs if j.get('_priority') == 'P2'], key=lambda x: x.get('_score',0), reverse=True)[:30]
candidates = p1 + p2
print(f'📊 {len(p1)} P1 + {len(p2)} P2 (top 30) = {len(candidates)} jobs to enhance')

enriched = {}
if enriched_path.exists():
    enriched_data = json.loads(enriched_path.read_text())
    if isinstance(enriched_data, dict):
        enriched = enriched_data
    elif isinstance(enriched_data, list):
        enriched = {j.get('url',''): j for j in enriched_data if j.get('url')}
    print(f'📝 {len(enriched)} jobs already enriched')

for j in candidates:
    url = j.get('url', '')
    has_desc = url in enriched and not enriched.get(url, {}).get('unenriched')
    desc_flag = '📄' if has_desc else '⚠️'
    print(f'  {desc_flag} {j.get(\"title\",\"?\")} @ {j.get(\"company\",\"?\")} — {j.get(\"_score\",0):.1f} ({j.get(\"_priority\",\"?\")})')
"
# Load profile for matching context
cat config/profile.yaml

4b.2 — Title Re-Scoring

Read ALL P1 + top 30 P2 job titles and the user's target_roles from profile.yaml.

For EACH job, assign a semantic title relevance score (0.0 to 1.0):

Scoring guide:

  • 1.0: Exact match or trivially equivalent ("Backend Engineer" ≈ "Software Engineer, Backend")
  • 0.85-0.95: Same role family, different seniority/specialization
  • 0.6-0.85: Related role, significant overlap in day-to-day work
  • 0.3-0.6: Tangentially related, some skill overlap
  • 0.0-0.3: Different job family entirely

Critical distinctions regex misses:

  • "Data Engineer" (builds pipelines) vs "Data Analyst" (writes SQL + dashboards) → 0.6, not 1.0
  • "Software Engineer - Data Platform" vs "Data Engineer" → 0.9 (same actual work)
  • "Staff Platform Engineer" vs "Staff Software Engineer" → 0.85 (same family, different focus)
  • "Machine Learning Engineer" vs "Software Engineer" → 0.7 (ML is SWE subspecialty)
  • "Technical Program Manager" vs "Software Engineer" → 0.2 (different function entirely)

Produce your re-scored titles as a JSON block for Step 4b.4.

4b.3 — Skill Extraction from Job Descriptions

For each candidate that has an enriched description, read the full description text and extract skills with section awareness.

If descriptions are missing — fetch them with gstack/browse:

BROWSE_OUTPUT=$(~/.claude/skills/gstack/browse/bin/find-browse 2>/dev/null)
B=$(echo "$BROWSE_OUTPUT" | head -1)
if [ -n "$B" ]; then
  echo "✅ gstack/browse available — can fetch missing job descriptions"
else
  echo "⚠️ gstack not available — will analyze existing enriched data only"
fi

For each unenriched P1 job (up to 10):

$B goto <job_url>
$B text

After fetching, read the description and classify skills:

For each job description:

  1. Identify sections: Requirements, Nice-to-Have, About Us, Responsibilities
  2. Map the user's profile skills against each section
  3. Output:
    • required: skills explicitly listed under requirements/qualifications
    • nice_to_have: skills under preferred/bonus sections
    • implicit: skills implied but not named (e.g., "cloud-native" implies AWS/GCP)
    • match_pct: percentage of required skills the user has
    • missing_critical: required skills the user lacks

Score and save any browsed jobs:

python3 -c "
from src.matcher import score_and_save_browsed
result = score_and_save_browsed({
    'title': 'REPLACE_TITLE',
    'company': 'REPLACE_COMPANY',
    'url': 'REPLACE_URL',
    'location': 'REPLACE_LOCATION',
    'skill_level': 'REPLACE_LEVEL',
})
print(f\"✅ {result['title']} @ {result['company']} → {result['_score']:.1f} ({result['_priority']})\")
"

4b.4 — Save Enhanced Data

Save your analysis to data/enriched/DATE-llm.json as a sidecar:

python3 -c "
import json, pathlib
from datetime import date

today = date.today().isoformat()
output = pathlib.Path(f'data/enriched/{today}-llm.json')
output.parent.mkdir(parents=True, exist_ok=True)

# Your analysis goes here — paste your JSON blocks
enhanced = {
    'title_rescores': [],      # from Step 4b.2
    'skill_extractions': [],   # from Step 4b.3
    'source': 'claude-code',
    'model': 'claude-code-inline',
}

output.write_text(json.dumps(enhanced, indent=2))
print(f'✅ Enhanced data saved to {output}')
print(f'   {len(enhanced[\"title_rescores\"])} title rescores')
print(f'   {len(enhanced[\"skill_extractions\"])} skill extractions')
"

4b.5 — Enhancement Summary (feeds into Step 5)

Note these results for Step 5 presentation:

  • How many titles were re-scored up vs down vs unchanged
  • Biggest false positives caught (title says X but job is actually Y)
  • Common skill gaps across P1 jobs
  • Average skill match percentage

This step should take ~60-90 seconds. The goal is comprehensive semantic analysis of all P1 + top P2 jobs in a single pipeline pass.


Step 5 — Present Results

Lead with the headline, every time:

🎯 Found {P1} P1 jobs — apply today!  +  {P2} P2 jobs worth applying this week.

Then show top 5-10 P1 matches in this format:

  #a3f9c1d2  Senior Software Engineer @ Stripe       SF · 93.2 · 3d ago
  #b7e2f4a9  Backend Engineer @ Anthropic             SF · 91.7 · 1d ago
  #c9d1e2f3  Staff Engineer @ Linear                  Remote · 88.4 · 2d ago

Anchor every claim in actual data:

  • "Stripe scores 93.2 because your Python + distributed systems background matches their exact keywords" → good
  • "Stripe is a strong match" → too vague, cut it

If Step 4b ran, also show the LLM enhancement summary:

🧠 LLM Enhancement (semantic analysis of {n} jobs):
  ↑ Upgraded: {count} jobs scored higher with semantic matching
  ↓ Downgraded: {count} false positives caught
  = Unchanged: {count} regex was already correct
  📝 Skill match: avg {pct}% across P1 jobs
  Common gaps: {skill1} (in {n} jobs), {skill2} (in {n} jobs)

Then mention:

  • P2 count and best 2-3 examples
  • Report saved at data/reports/YYYY-MM-DD.csv
  • Dashboard at site/index.html
  • Enhanced data saved at data/enriched/DATE-llm.json

End with an offer:

"Want me to classify these and rank which to apply to first? → /classify-jobs"


Step 5b — False Positive Detection & Negative Pattern Suggestions

After showing results, scan the P1 list for clusters of roles that look like false positives. A false positive cluster is 3+ P1 jobs sharing a title pattern that doesn't match the user's actual target function (e.g., "Cloud Platform Engineer" appearing in a Backend SWE search).

# Pseudocode for cluster detection:
# 1. Group P1 jobs by 2-word title prefix (e.g., "Cloud Platform", "DevOps Engineer")
# 2. Any group with 3+ jobs where the prefix is NOT in target_roles → suggest exclusion

If false positive clusters are found, offer to add them as exclude_title_patterns:

⚠️  I noticed 6 P1 jobs are "Cloud Platform Engineer" roles — these look like
infrastructure/DevOps, not backend SWE. Want me to exclude these from future runs?

→ This would add `exclude_title_patterns: ["cloud platform"]` to your profile.

Use AskUserQuestion with options:

  • "Yes, exclude those" — Add to profile.yaml exclude_title_patterns and rescore
  • "No, keep them" — Leave as-is
  • "Let me pick which to exclude" — Show the list and let user choose

After updating, re-run the matcher and show updated counts:

python -m src.matcher && python -m src.report

Step 5b.5 — Industry Browse Discovery (auto-suggest companies to browse)

After presenting JBA results, proactively identify major employers NOT in JBA that are highly relevant to the user's profile. Run this every time — the user shouldn't have to name companies; the system should recommend them.

How to generate the browse list

Read the user's profile fields and apply these rules:

Location-based triggers:

  • If location contains Canada, Toronto, Vancouver, Ottawa, Calgary, Montreal: → Add Canadian banks: RBC, TD Bank, BMO, CIBC, Scotiabank, National Bank of Canada → Add Canadian tech: Shopify, Wealthsimple, Lightspeed Commerce, Wattpad, Hootsuite, KOHO, Nuvei, Float → Add big tech Canadian offices: Amazon Canada (amazon.jobs), Google Canada, Atlassian (Sydney/Toronto)
  • If location is US + relocation_cities includes Canada → same as above
  • If location contains San Francisco, Seattle, New York, Austin: → Add: Google, Apple, Amazon, Microsoft, Meta, Lyft, Uber, Airbnb (not in JBA Workday)

Skills-based triggers (add to list if not already there):

  • Java, Kotlin, Spring Boot → add fintech/enterprise: Stripe (already in JBA), PayPal, Square, Affirm, Goldman Sachs Engineering
  • AWS, Cloud → add: Databricks, Snowflake, HashiCorp, Cloudflare
  • AI, ML, LLM, Bedrock, RAG → add: OpenAI, Cohere, Mistral, Perplexity, Scale AI, Hugging Face
  • React, TypeScript, Frontend → add: Figma, Linear, Notion, Vercel

Deduplicate against companies already appearing in the JBA P1/P2 results.

Present to user

🌐 Companies likely hiring for your profile NOT in today's JBA results:

  🇨🇦 Canadian employers (Toronto):
    • RBC — rbcjobs.com  [Senior SWE, Java/Kotlin backend]
    • TD Bank — jobs.td.com  [Senior Developer, cloud platform]
    • Shopify — shopify.com/careers  [Backend/infra, Go/Ruby but hires Java too]
    • Wealthsimple — wealthsimple.com/en-ca/jobs  [Fintech, Python/Go backend]

  🔬 Based on your AI/cloud skills:
    • Databricks — databricks.com/company/careers
    • Snowflake — careers.snowflake.com

→ Want me to browse any of these? I can fetch job listings and score them against your profile.

Use AskUserQuestion with:

  • A checkboxes-style listing or multi-select asking which companies to browse
  • Option: "Browse all of them"
  • Option: "Skip — JBA results are enough"

For each company the user selects:

  1. Check if gstack is available (see Step 5c)
  2. Browse their careers page and extract job listings
  3. Score each via score_and_save_browsed() and append to results
  4. After all browsing is done, re-present updated headline counts

Cap at 6 companies per run to keep the pipeline under 5 minutes total.


Step 5c — Non-JBA Job Support (Browse)

If the user provides specific job URLs that are NOT in the scored results (e.g., jobs from company career pages not covered by JBA like Scotiabank, or specific listings that weren't picked up), use gstack /browse to fetch the job description.

First, check if gstack is available:

BROWSE_OUTPUT=$(~/.claude/skills/gstack/browse/bin/find-browse 2>/dev/null)
B=$(echo "$BROWSE_OUTPUT" | head -1)
if [ -z "$B" ]; then
  echo "BROWSE_NOT_AVAILABLE"
fi

If gstack is available — fetch the job description:

$B goto <job_url>
$B text

Validate the output contains job description content (title, requirements, responsibilities). If the page returned an error or login wall, tell the user and ask them to paste the JD instead.

If gstack is NOT available — ask the user to install it or paste the JD: Use AskUserQuestion with these options:

  1. "Install gstack now" — Show the install command: git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup Then tell them to add a "gstack" section to CLAUDE.md that says to use /browse for all web browsing, never use mcp__claude-in-chrome__* tools, and lists available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review, /design-consultation, /review, /ship, /browse, /qa, /qa-only, /design-review, /setup-browser-cookies, /retro, /debug, /document-release. Then ask the user if they also want to add gstack to the current project so teammates get it. After installing, retry the browse.
  2. "I'll paste the job description" — Accept pasted text and continue.
  3. "Skip this job" — Continue without it.

After fetching (via browse or paste), extract the job fields and score it through the pipeline:

  1. From the job description text, extract: title, company, location, url, and skill_level (one of: intern, entry, mid, senior, lead, manager — infer from title/description).
  2. Score and save the browsed job:
python3 -c "
from src.matcher import score_and_save_browsed
result = score_and_save_browsed({
    'title': 'REPLACE_TITLE',
    'company': 'REPLACE_COMPANY',
    'url': 'REPLACE_URL',
    'location': 'REPLACE_LOCATION',
    'skill_level': 'REPLACE_LEVEL',
})
print(f\"✅ {result['title']} @ {result['company']} → {result['_score']:.1f} ({result['_priority']})\")
"
  1. Present the scored browsed job alongside pipeline results:
🌐 Browsed job (scored & saved):
   #xxxxxxxx  Backend Software Engineer @ Scotiabank  Toronto · 87.3 · P1
   URL: https://jobs.scotiabank.com/...

The job is now in data/scored/DATE.json (tagged _source: browse), so it will appear in the dashboard, CSV report, and /classify-jobs automatically.

Then offer: "Want me to classify this job or tailor your resume for it? → /classify-jobs or /tailor-resume"


Step 6 — Save Pipeline History

After every successful run, append to .context/jobhunter-history.json:

{
  "date": "2026-03-16",
  "p1": 51,
  "p2": 1175,
  "p3": 6773,
  "new_today": 48,
  "top_p1": [
    { "id": "a3f9c1d2", "title": "Senior Software Engineer", "company": "Stripe", "score": 93.2 }
  ],
  "pipeline_seconds": 25
}

If .context/jobhunter-history.json exists: load it and show a one-line trend:

  vs yesterday: P1 51 (↑8), P2 1175 (↓23), 48 new jobs added

Troubleshooting

SymptomFix
No P1 jobsBroaden target_roles in profile.yaml — run: /jobhunter update profile
Zero jobs totalRe-run python -m src.scraper — JBA download may have failed
Scrape failuresCheck internet; specific ATS may be temporarily down — noted in report
Netflix not showingAlready in JBA download — no live scrape needed for Workday companies
Stale dataRun with --skip-live for speed, or full scrape for freshness

Quick Re-run (cached data, no network)

python -m src.scraper --skip-download --skip-live
python -m src.matcher && python -m src.report && python -m src.site_generator

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.