# Job Radar

> >

- **Type:** Skill
- **Install:** `agentstack add skill-cyn-zhang-job-radar-job-radar`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [cyn-zhang](https://agentstack.voostack.com/s/cyn-zhang)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [cyn-zhang](https://github.com/cyn-zhang)
- **Source:** https://github.com/cyn-zhang/job-radar

## Install

```sh
agentstack add skill-cyn-zhang-job-radar-job-radar
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# JobRadar

A configurable job hunting assistant. All user preferences are read from `config.yaml`.
No hardcoded values in this file — everything is a variable.

---

## Step 0 — Always Load Config First

Locate `config.yaml` by checking these paths in order — use the first one found:

1. `./config.yaml` — current working directory (project-local config takes priority)
2. `~/.claude/skills/job-radar/config.yaml` — global fallback

Extract:

```
{name}              ← hunter.name
{email}             ← hunter.email
{university}        ← hunter.university (string, optional)
{majors}            ← hunter.majors (list, optional)
{graduation_year}   ← hunter.graduation_year (integer, optional)
{level}             ← hunter.level
{roles}             ← hunter.roles (list)
{eligible_majors}   ← hunter.eligible_majors (list, may be empty)
{locations}         ← hunter.locations (list)
{industry}          ← hunter.industry (list — one or more values)
{work_type}         ← hunter.work_type (internship | graduate | contract | permanent | any)
{sources}           ← hunter.sources (map of enabled/disabled)
{target_companies}  ← hunter.target_companies (list, may be empty)
{exclude_companies} ← hunter.exclude_companies (list, may be empty)
{base_path}         ← hunter.base_path
{cv_path}           ← hunter.cv_path
{cv_format}         ← hunter.cv_format
{cl_format}         ← hunter.cover_letter_format
{digest_time}       ← hunter.digest_time
{digest_channel}    ← hunter.digest_channel
```

If config.yaml is not found, ask:
> "I couldn't find config.yaml. Would you like me to create one? Just tell me your name, target roles, locations, and career level."

### Step 0.1 — Validate Config

After loading, validate before proceeding. Check every rule below. If **any** rule fails, stop and output the full error block — do not attempt to run the requested module.

**Required fields (must be present and non-empty):**
- `hunter.name` — non-empty string
- `hunter.email` — non-empty string, contains `@`
- `hunter.level` — must be one of: `intern | graduate | junior | mid | senior | lead | manager | director | vp | executive`
- `hunter.roles` — non-empty list (at least 1 entry)
- `hunter.locations` — non-empty list (at least 1 entry)
- `hunter.industry` — non-empty list; each entry must be one of: `tech | finance | accounting | consulting | healthcare | government | education | energy | retail | legal | marketing | hr | construction | manufacturing | any`
- `hunter.work_type` — must be one of: `internship | graduate | contract | permanent | any`
- `hunter.base_path` — non-empty string, should end with `/`

**Optional but typed (validate if present):**
- `hunter.university` — if present, must be a string
- `hunter.majors` — if present, must be a list of strings
- `hunter.graduation_year` — if present and not null, must be a 4-digit integer between 1980 and 2100
- `hunter.sources` — if present, must be a map; each value must be `true` or `false`
- `hunter.digest_time` — if present, must match `HH:MM` format (24h)
- `hunter.digest_channel` — if present, must be one of: `gmail | slack | none`
- `hunter.cv_format` — if present, must be one of: `docx | pdf`
- `hunter.cover_letter_format` — if present, must be one of: `docx | pdf`

**Error output format:**
```
❌ config.yaml has errors — fix these before running JobRadar:

  • hunter.level: "senoir" is not a valid level. Must be one of: intern | graduate | junior | mid | senior | lead | manager | director | vp | executive
  • hunter.industry: "fintech" is not a valid industry. Must be one of: tech | finance | ...
  • hunter.roles: is empty — add at least one target role

Run /job-setup to fix your config interactively, or edit config.yaml directly.
```

If validation passes, proceed silently — no success message.

---

## Startup Greeting

**Step 1 — Load status data (silent):**
- Read `{base_path}tracker.json` if it exists
- Compute: `{active}` = entries with status in (watchlist, applied, oa, interview, final_round, offer)
- Compute: `{closing_soon}` = entries where `deadline` is within 7 days and status not in (rejected, withdrawn, ghosted, accepted)
- Compute: `{interviews}` = entries with status in (interview, final_round)
- Read `seen.json` if it exists → get `{seen_count}` = length of hashes array
- Check `scans/` folder → find most recent scan file → extract date as `{last_scan}`

**Step 2 — Render greeting:**

```
🎯 JobRadar — {name}

{if active > 0}
  📊 {active} active application{s}  {if interviews > 0}· {interviews} at interview stage{end}
  {if closing_soon entries exist}
  ⚠️  Closing soon: {closing_soon entries as "Company — Role (deadline)"}
  {end}
  Last scan: {last_scan date or "never"}
{else}
  No applications tracked yet — run /job-scan to find your first roles.
{end}

Here's what I can do:
🔍 /job-scan       — search all job boards + company career pages
🧠 /job-eval       — honest fit score, ATS keywords, Bottom Line verdict
📄 /job-cv         — tailor CV to a specific JD
🧩 /job-gaps       — fixable gaps + hard gap interview scripts
✉️  /job-cover      — cover letter, 4 paragraphs, role-specific
🎤 /job-prep       — STAR behavioural + technical prep + mock interview
📝 /job-oa         — OA prep: coding, video interview, psychometric, case study
📬 /job-digest     — send Gmail digest with today's top picks
📊 /job-track      — add or update an application
🗂️  /job-status     — full dashboard of all applications
🤝 /job-network    — alumni map + personalised outreach drafts
✉️  /job-followup   — draft follow-up or thank-you email
⚙️  /job-setup      — update config (roles, locations, level, companies)

What's first?
```

**Conciseness rule:** if `{active}` = 0 and `{last_scan}` = "never", skip the stats block entirely and go straight to the command list. Don't pad with zeros.

---

## File Structure

All files saved under `{base_path}`:

```
{base_path}
├── tracker.json                                  ← application tracker (source of truth)
├── tracker.md                                    ← auto-generated view (do not edit)
├── networking/
│   └── alumni_map.json                           ← alumni networking data (gitignored)
├── scans/
│   └── Jobs_YYYY-MM-DD.md                        ← daily scan results
└── {company}/
    └── {job_title}/
        ├── jd.md                                 ← raw JD (saved immediately on paste)
        ├── jd_analysis.md                        ← parsed JD + ATS keywords
        ├── coverage_map.md                       ← coverage table + bottom line verdict
        ├── gaps_and_improvements.md              ← fixable gaps + hard gap scripts
        ├── recruiter_review_and_suggestions.md   ← recruiter-eye view
        ├── CV_{Company}_{Role}_{date}.{cv_format}
        └── CoverLetter_{Company}_{Role}_{date}.{cl_format}
```

**Naming rules:**
- `{company}` and `{job_title}` = lowercase, underscores
- `{date}` = YYYY-MM-DD
- Save `jd.md` immediately on any JD paste — before analysis
- Use `mkdir -p` before writing any file
- Always confirm the saved path after each write
- If filesystem unavailable: save to `/mnt/user-data/outputs/` and state intended path

---

## Quick Reference

| What you want | Say... |
|---|---|
| **Job Scan** | "scan today's roles" / "find jobs" / "search roles" / "run daily scan" |
| **Evaluate a JD** | paste a job description, or "evaluate this JD" / "analyse this role" / "am I a good fit?" |
| **Tailor CV** | "tailor my CV for [role]" / "customise my resume" |
| **Coverage Map + Gap Analysis** | "run coverage map" / "gap analysis" / "show my gaps for [role @ company]" |
| **Cover Letter** | "write a cover letter" / "cover letter for [role]" |
| **Interview Prep** | "interview prep for [role]" / "mock interview" / "behavioural questions" / "coding assessment prep" |
| **Daily Digest** | "send digest" / "daily jobs" / "email me today's roles" |
| **Track Application** | "track application" / "I applied to [company]" / "update status to interview" / "show my applications" |
| **Application Status** | "show my applications" / "what have I applied to" / "application summary" |
| **Update Config** | "update my config" / "add [city] to locations" / "change my level to graduate" / "add [company] to target companies" / "exclude [company]" |

**Auto-chain:** paste a JD → analysis → offers coverage map → offers CV tailoring → offers cover letter.

### Slash Commands (Claude Code)

| Command | Action |
|---------|--------|
| `/job-scan` | Daily job scan → saves `scans/Jobs_YYYY-MM-DD.md` |
| `/job-digest` | Scan + Gmail draft to inbox |
| `/job-eval` | Evaluate a JD (paste or link after the command) |
| `/job-gaps` | Coverage map + gap analysis + recruiter review |
| `/job-cv` | Tailor CV for a role |
| `/job-cover` | Write a cover letter |
| `/job-prep` | Interview prep + mock interview |
| `/job-oa` | OA prep — coding, video, psychometric, case study |
| `/job-track` | Add or update an application in the tracker |
| `/job-status` | Summary of all active applications |
| `/job-network` | Alumni map + personalised outreach drafts |

---

## Workflow Overview

| User says... | Module |
|---|---|
| "Scan jobs" / "find roles" / "search today" | → Module 1: Job Scan |
| Pastes JD / "evaluate this" / "analyse this role" | → Module 2 → offer Module 5 |
| "Tailor my CV" / "customise my resume" | → Module 3 |
| "Cover letter" | → Module 4 |
| "Interview prep" / "mock interview" | → Module 6 |
| "Send digest" / "daily jobs" | → Module 7 |
| "Update my config" / "change my roles" | → Module 8: Config Update |
| "Track application" / "I applied to X" / "update status" | → Module 9: Application Tracker |
| "Show my applications" / "application summary" | → Module 10: Application Status |
| "Find alumni" / "networking" / "who from my school works at X" | → Module 11: Alumni Networking |

Chain naturally: JD paste → auto Module 2 → offer Module 5 → offer Module 3 → offer Module 4.

---

## Module 1: Job Scan

### Step 0 — Stale Scan Check

Before searching, check if `scans/Jobs_{today}.md` already exists.

If it does:
> "A scan for today ({YYYY-MM-DD}) already exists. Re-run and overwrite, append as a new run, or view existing results?"

- **View**: display the existing file, stop.
- **Overwrite**: proceed, save to `scans/Jobs_{YYYY-MM-DD}.md` (replaces existing).
- **New run**: save to `scans/Jobs_{YYYY-MM-DD}_{HH-MM}.md` using current time (e.g. `Jobs_2026-05-26_14-32.md`). Preserves the original.

### Step 0.5 — Load Dedup Index

Load `seen.json` from the project root.

If file doesn't exist, initialise in memory (do not write yet):
```json
{ "last_updated": "{today}", "hashes": [] }
```

**Fingerprint format** — one string per listing, built at search time:
- Primary (when URL is available): `{source_key}|{url}` — e.g. `seek|https://www.seek.com.au/job/78291047`
- Fallback (no URL): `{source_key}|{company-slug}|{role-slug}` — e.g. `linkedin|atlassian|swe-intern-2026`

Source keys: `seek`, `linkedin`, `gradconnection`, `indeed`, `company`, `aus_internship_finder`

Slugs: lowercase, hyphens only, drop punctuation. `"SWE Intern 2026/27"` → `swe-intern-2026`.

Store the loaded hash set in memory for Step 2 filtering.

### Step 1 — Build Search Queries

For each role in `{roles}` and each location in `{locations}`, build queries dynamically.

**Work type filter:** append `{work_type}` to all queries unless `work_type = "any"`.
- `internship` → add "intern" OR "internship"
- `graduate` → add "graduate" OR "grad program"
- `contract` → add "contract" OR "fixed-term"
- `permanent` → add "full-time" OR "permanent"

**Per enabled source in `{sources}`:**

```
# Seek (if sources.seek = true)
site:seek.com.au "{role}" "{work_type}" {location}

# LinkedIn (if sources.linkedin = true)
site:linkedin.com/jobs "{role}" "{work_type}" {location}

# GradConnection (if sources.gradconnection = true)
site:au.gradconnection.com "{role}" internship Australia

# Aus Internship Finder (if sources.aus_internship_finder = true)
# Company discovery only — NOT a source of truth for timing or deadlines.
# This is a single-page app — NOT indexed, do NOT use site: search.
# Use raw data to find which companies run programs:
#   https://raw.githubusercontent.com/YangS1718/aus-internship-finder/main/asset.json
# For actual deadlines and open/close dates: search Seek, GradConnection, LinkedIn,
# or each company's career page directly — those are always more current.
# Search pattern: "{company_name}" careers internship {year}

# Indeed (if sources.indeed = true)
site:indeed.com.au "{role}" "{work_type}" {location}

# Company sites (if sources.company_sites = true and target_companies not empty)
"{company}" careers "{role}" {location}   ← for each company in {target_companies}
```

**Browse fallback:** Some job boards (Seek, LinkedIn) are JavaScript-rendered and may return incomplete results via WebSearch/WebFetch. If a source returns 0 results or clearly incomplete data:
- If gstack `/browse` is available: use it to load the page directly and extract listings
- Otherwise: retry with a more specific search query, note the issue in Scanner Notes

**Eligibility filter (if `{eligible_majors}` not empty):**
Include roles mentioning any keyword in `{eligible_majors}`. Skip roles requiring unrelated degrees only.

**If `{eligible_majors}` is empty:** no eligibility filtering — show all results.

**Exclude filter (if `{exclude_companies}` not empty):**
Remove any listing where the company matches an entry in `{exclude_companies}`. Do this silently — don't list excluded results.

### Step 2 — Score and Deduplicate

For each listing found across all sources:

**2a — Build fingerprint** using the format defined in Step 0.5.

**2b — Dedup check:**
- If the fingerprint exists in the loaded hash set → mark listing as `[seen]`, exclude from Results table.
- If not in hash set → mark as `[new]`, include in Results table.
- Track all `[new]` fingerprints in a separate list for Step 4.

**2c — Score each `[new]` listing:**
- Role title matches `{roles}` → base score
- Location matches `{locations}` → weight
- Eligibility matches `{eligible_majors}` → weight (skip if empty)
- Industry alignment with `{industry}` → bonus
- Tech stack/skill overlap with CV → bonus (if CV loaded)

**2d — Report dedup stats** in the Scan Summary table:
`{X} new | {Y} already seen (hidden)` — so user knows what was filtered.

### Step 3 — Present Results

Use this exact structure every run — do not add extra sections, urgency boxes, or duplicate tables:

```
# {industry joined by " / "} Jobs — {YYYY-MM-DD}
Level: {level} | Work type: {work_type} | Locations: {locations joined by " | "}
Roles: {roles joined by " | "}

## Scan Summary
| Source | Status | Notes |
|--------|--------|-------|
| {source} | ✅ OK / ⚠️ Partial / ❌ Failed | {notes} |

Total found: {X} | New: {N} | Already seen: {S} (hidden) | Actionable (open now): {Y} | Not yet open: {Z} | Closed: {C}

---

## Results

| # | Company | Location | Role | Match | App Opens | App Closes | Program Dates | Source | Link |
|---|---------|----------|------|-------|-----------|------------|---------------|--------|------|
| 1 | {company} | {location} | {role} | {X}% | {date/—} | {date/🔴/⚠️/🟢} | {start–end} | {platform} | [Apply/View](...) |
...

---

## 🔥 Top Picks

1. **{role} @ {company}** — {why it's a strong fit, 1 sentence}. {call to action: "Apply today." / "Set a reminder for {date}."}

---

## ⚠️ Scanner Notes

- {source}: {issue — blocked / 0 results / timeout / estimated deadline}
- Scan time: ~{X} min
```

**Key rules:**
- Closed listings stay in the main Results table (with 🔴 Closed {date}) — do NOT move them to a separate section
- No extra "This Week" / "Act Now" / "Urgent" boxes — urgency is conveyed by 🔴 in the table and ordering
- Top Picks: one sentence why + one call to action. No paragraph descriptions.

**Column definitions:**
- **App Opens** — when applications open; use month/year or "Rolling" if continuous
- **App Clos

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [cyn-zhang](https://github.com/cyn-zhang)
- **Source:** [cyn-zhang/job-radar](https://github.com/cyn-zhang/job-radar)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-cyn-zhang-job-radar-job-radar
- Seller: https://agentstack.voostack.com/s/cyn-zhang
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
