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
In this repository3 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- classify-jobsSkill
- tailor-resumeSkill
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
- Distinctive: line 28
No resume found (no RESUME.md and no .pdf in project root) — ask user to add one
- Distinctive: line 52
Run this check first, every time.
- Very specific: line 120
If profile.yaml was just created from example: proceed to Step 1 (Profile Setup) to customize it.
- Distinctive: line 121
If profile staleness check shows ⚠️: proceed with the pipeline — the matcher will rescore against the current profile.
- Distinctive: line 123
If RESUME.md is missing: Stop and use AskUserQuestion to ask the user for their resume:
- Distinctive: line 136
When the user selects option 3 ("Just let me dump my work history"), follow this flow:
- Distinctive: line 190
ONLY use information the user provided: Never fabricate companies, roles, dates, skills, or achievements.
- Distinctive: line 191
If a date range is vague ("a couple years at Google"), write "~2 years" and flag it for the user to correct.
- Distinctive: line 192
If the user mentioned skills but didn't tie them to specific roles, put them in the Skills section.
- Very specific: line 195
If contact info (email, location, LinkedIn) wasn't provided, leave placeholders: [your@email.com], [City, ST], [LinkedIn URL].
- Very specific: line 213
4. Continue to Step 1: After RESUME.md is saved, proceed normally to profile auto-generation.
- Very specific: line 220
When profile.yaml is missing or was just created from example, OR user asks to update:
- Distinctive: line 224
If a resume exists (RESUME.md or PDF), you ARE Claude — parse it directly:
- Distinctive: line 231
If resume exists, extract these fields by reading the full text:
- Distinctive: line 236
skills: All technical skills mentioned (languages, frameworks, tools, platforms)
- Distinctive: line 259
Use AskUserQuestion: "This profile was auto-generated from your resume.
- Distinctive: line 266
If no resume exists or user chose "Start over manually":
- Distinctive: line 268
Ask these questions one at a time (never batch — one AskUserQuestion per question):
- Distinctive: line 292
Run straight through. Only surface errors that block completion.
- Distinctive: line 321
When user mentions a specific company, check which channel it's on before scraping:
- Distinctive: line 336
If not in JBA: Use AskUserQuestion to offer browsing their careers page:
- Distinctive: line 346
After fetching, present jobs alongside scored results as 📌 Manual additions.
- Distinctive: line 360
After user installs, retry the browse.
- Distinctive: line 362
If >2,000 jobs at one company: verify it's not a staffing farm before including.
- Distinctive: line 368
After scoring, you ARE Claude — use your semantic understanding to enhance the results.
- Very specific: line 420
Read ALL P1 + top 30 P2 job titles and the user's target_roles from profile.yaml.
- Distinctive: line 463
After fetching, read the description and classify skills:
- Distinctive: line 550
If Step 4b ran, also show the LLM enhancement summary:
- Distinctive: line 573
After showing results, scan the P1 list for clusters of roles that look like false positives.
- Distinctive: line 583
If false positive clusters are found, offer to add them as exclude_title_patterns:
- Distinctive: line 592
Use AskUserQuestion with options:
- Distinctive: line 597
After updating, re-run the matcher and show updated counts:
- Distinctive: line 606
After presenting JBA results, proactively identify major employers NOT in JBA that are highly relevant to the user's profile.
- Distinctive: line 612
Read the user's profile fields and apply these rules:
- Very specific: line 615
If location contains Canada, Toronto, Vancouver, Ottawa, Calgary, Montreal: → Add Canadian banks: RBC, TD Bank, BMO, CIBC, Scotiabank, …
- Very specific: line 619
If location is US + relocation_cities includes Canada → same as above
- Very specific: line 620
If location contains San Francisco, Seattle, New York, Austin: → Add: Google, Apple, Amazon, Microsoft, Meta, Lyft, Uber, Airbnb (not in …
- Distinctive: line 655
Check if gstack is available (see Step 5c)
- Distinctive: line 658
After all browsing is done, re-present updated headline counts
- Very specific: line 666
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 …
- Distinctive: line 685
If the page returned an error or login wall, tell the user and ask them to paste the JD instead.
- Distinctive: line 688
Use AskUserQuestion with these options:
- Distinctive: line 697
After installing, retry the browse.
- Distinctive: line 701
After fetching (via browse or paste), extract the job fields and score it through the pipeline:
- Very specific: line 737
After every successful run, append to .context/jobhunter-history.json:
- Very specific: line 753
If .context/jobhunter-history.json exists: load it and show a one-line trend:
≈ “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.
Files
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.mdwhen 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:
- "I have a resume file" — Ask for the path. Read it (PDF or text), write to RESUME.md.
- "I'll paste my resume" — Accept pasted text, write to RESUME.md.
- "Just let me dump my work history" — Accept unstructured "word vomit" and format it into a proper resume. See Word Vomit → Resume flow below.
- "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."
fiOptions:
- "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:
- Read the resume:
cat RESUME.md 2>/dev/null || echo "NO_RESUME_MD"- 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+)
- You can ALSO run the regex parser as a cross-check:
python3 -m src.resume_parser --dry-run- 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.
- 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, ...- 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":
- 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?"
- Show proposed
profile.yamlcontents and confirm before writing - 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/activateStep 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 detectionReport
python -m src.report # CSV + terminal summaryDashboard
python -m src.site_generator
open site/index.html # each job card shows a unique #xxxxxxxx job IDStep 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| Company | ATS | Notes |
| Netflix | Workday ✅ | JBA download already has it — no live scrape needed |
| Anthropic | Greenhouse ✅ | JBA download + live scrape (anthropic) |
| Stripe | Greenhouse ✅ | JBA download + live scrape (stripe) |
| OpenAI | Greenhouse ✅ | JBA download + live scrape (openai) |
| Google, Apple, Amazon, Microsoft, Meta | ❌ not in JBA | LinkedIn / careers page |
If not in JBA: Use AskUserQuestion to offer browsing their careers page:
- "Yes, browse their careers page" — Use gstack
/browseto fetch their jobs:
After fetching, present jobs alongside scored results as 📌 Manual additions.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 - "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.yaml4b.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"
fiFor each unenriched P1 job (up to 10):
$B goto <job_url>
$B textAfter fetching, read the description and classify skills:
For each job description:
- Identify sections: Requirements, Nice-to-Have, About Us, Responsibilities
- Map the user's profile skills against each section
- Output:
required: skills explicitly listed under requirements/qualificationsnice_to_have: skills under preferred/bonus sectionsimplicit: skills implied but not named (e.g., "cloud-native" implies AWS/GCP)match_pct: percentage of required skills the user hasmissing_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 agoAnchor 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 exclusionIf 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_patternsand 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.reportStep 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
locationcontainsCanada,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_citiesincludes Canada → same as above - If
locationcontainsSan 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 EngineeringAWS,Cloud→ add: Databricks, Snowflake, HashiCorp, CloudflareAI,ML,LLM,Bedrock,RAG→ add: OpenAI, Cohere, Mistral, Perplexity, Scale AI, Hugging FaceReact,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:
- Check if gstack is available (see Step 5c)
- Browse their careers page and extract job listings
- Score each via
score_and_save_browsed()and append to results - 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"
fiIf gstack is available — fetch the job description:
$B goto <job_url>
$B textValidate 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:
- "Install gstack now" — Show the install command:
git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setupThen 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. - "I'll paste the job description" — Accept pasted text and continue.
- "Skip this job" — Continue without it.
After fetching (via browse or paste), extract the job fields and score it through the pipeline:
- From the job description text, extract:
title,company,location,url, andskill_level(one of: intern, entry, mid, senior, lead, manager — infer from title/description). - 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']})\")
"- 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 addedTroubleshooting
| Symptom | Fix |
| No P1 jobs | Broaden target_roles in profile.yaml — run: /jobhunter update profile |
| Zero jobs total | Re-run python -m src.scraper — JBA download may have failed |
| Scrape failures | Check internet; specific ATS may be temporarily down — noted in report |
| Netflix not showing | Already in JBA download — no live scrape needed for Workday companies |
| Stale data | Run 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_generatorDoes 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
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.

