15 Commits
Author SHA1 Message Date
NotAbdelrahmanelsayed 1cdaf9497f feat(rank): persist triage gaps and strengths into seen_jobs.json (#263)
/rank's Step 2 scoring agents already return strengths and gaps per job,
but Step 4 only persisted rank_score/rank_verdict/rank_date - both arrays
were printed once in Step 5 and then discarded. Store them verbatim in
seen_jobs.json (replaced, not accumulated, on --all re-ranks) so downstream
consumers can read real triage findings instead of re-deriving them.

Discussed in #258.
2026-07-31 17:41:41 +02:00
Lautaro Emanuel JimenezandClaude Sonnet 5 7db231c680 feat(job-scraper): flag mass-posting and recycled-listing patterns (#207)
* feat(job-scraper): flag mass-posting and recycled-listing patterns

Adds Step 2.5 to detect two distribution patterns that are worth
surfacing to the user as a caution signal, not an accusation:

- Mass-posting: the same (or near-identical) listing posted across
  many cities/locations at once, consolidated into one row instead of
  presented as separate duplicate results.
- Recycled listing: a new candidate whose description closely matches
  an older seen_jobs.json entry from the same company, but under a
  different title.

Neither pattern is treated as proof of anything - fit is never
downgraded and results are never excluded because of it, the point is
giving the user the signal so they can decide. Explicitly scoped away
from naming companies as fraudulent (see the new Important Rules #8):
this documents a detectable behavior pattern, not a blacklist.

Motivated by real signal seen today: the same req ID posted across 6
different LATAM cities, and a single-city listing duplicated 3x under
slightly different titles.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix non-executable Pattern B: add snippet field, move check to Step 4

Pattern B instructed comparing a new posting's description against
existing seen_jobs.json entries, but the Step 4 schema never stored
descriptions - nothing existed to compare against, so the check
couldn't run as written.

- Adds an additive `snippet` field to the seen_jobs.json schema
  (same move as #193's `portal` field), populated when each entry is
  written.
- Moves the recycled-listing check itself to Step 4, where the write
  happens, leaving Step 2.5 as the in-run mass-posting check only.
- Makes the snippet match the actual discriminator, not company +
  different title alone - that alone would false-flag every company
  that legitimately runs several concurrent open roles, which is
  exactly what Rule 9's "signal, not accusation" framing is trying to
  avoid.
- Updates the Step 5 / Important Rules cross-references accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Trim to within-run mass-posting detection only

Drop the cross-run recycled-listing check (Step 4) and the snippet
field it depended on, per review: no evidence the pattern recurs
often enough to justify persisting a description snippet on every
seen_jobs.json entry permanently. Step 2.5's in-run mass-posting
consolidation is cheap and stays.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 20:53:45 +02:00
Ayobami Adegoke a5de938492 feat(scrape): portal health check to catch silent scraper rot (#193)
Adds Step 4.75 to /scrape: detects the failure mode where a portal changes markup and its parser exits 0 with zero or garbled results (invisible to the Step 1c fallback, which only fires on non-zero exit). Free pass over this run's results (degraded scan) plus seen_jobs.json yield history; bounded escalation on suspicion only (the portal's own SKILL.md test query, one broader retry, rate-limit never treated as evidence); health: lines in the Step 5 summary with a confirmation-gated enabled:false quarantine offer; healthy portals stay silent. Adds a /scrape health [portal] probe-only mode. Persists a portal field in seen_jobs.json additively (per the /rank precedent), with read-time URL-domain attribution for pre-field entries so no migration is needed.

Folded into /scrape rather than a standalone /doctor command - the detection lives where the evidence (run results + yield history) already is, and a routine command catches rot a user would otherwise notice weeks late. By @ayobamiseun.
2026-07-20 21:53:51 +02:00
Erik Pastor Rios 37595187d6 feat(job-scraper): per-portal enabled toggle honored by /scrape (#160)
Implements the mechanism approved in discussion #93: enabled: true|false frontmatter on portal skills (missing key = enabled), honored during /scrape portal discovery, with skipped portals reported visibly in the run summary.
2026-07-15 20:20:37 +02:00
Rasika d1e707ef1f feat(job-scraper): referral-contact LinkedIn search links for high/medium-fit jobs (#149)
* added contact skill

* refactor(job-scraper): fold referral-contacts into Step 4.5, drop contacts cache
2026-07-15 07:52:04 +02:00
Yash Rajeshbhai DarjiandCursor 0a8fc194e5 docs: sync onboarding with linkedin-search + freehire-search reality (#150)
/setup still told non-DK users that built-in CLIs are Denmark-specific.
Align setup, /add-portal, and search-queries.md with shipped country-agnostic
CLIs and /scrape auto-discovery (post #85 / #95 / #102).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 14:30:49 +02:00
Erik Pastor RiosandClaude Opus 4.8 7e8df35819 docs(job-scraper): make search queries market-neutral (#95)
Rebased onto current master. The SKILL.md description and "How It Works"
copy were already generalized by #102, so this drops those now-redundant
edits and keeps only the unique remaining value: replacing the hardcoded
Danish job boards (jobindex.dk, karriere.dk, jobfinder.dk, akademikernes.dk)
and Danish location terms in search-queries.md with configurable
placeholders and an /add-portal pointer, so the query templates apply to
any market.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 08:06:54 +02:00
Sai Sridhar Tarra fea59fd8df docs: document complete seen_jobs.json schema including rank fields (#107) (#111) 2026-07-09 21:32:43 +02:00
Yash Rajeshbhai DarjiandCursor 3bb4688e5d Use CLI detail in scraper Step 2; broaden skill description (#102)
Follow-up to #65 feedback after #52 merged CLI-first search. Step 2 now
uses each portal's detail command for CLI-sourced jobs (WebFetch only for
WebSearch fallbacks). Skill description reflects market-agnostic portal
CLIs instead of Danish-only wording.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 21:12:15 +02:00
Mads LorentzenandClaude Fable 5 a558c8593b fix: rename job-scraper skill to 'scrape' so /scrape resolves natively (#74)
The docs instruct users to run /scrape, but the skill's name was
job-scraper, so /scrape never resolved as a command - it only worked
via fuzzy trigger matching on the description. Renaming the skill's
name field makes /scrape a real, autocompleted command, consistent
with /upskill (whose skill is named upskill). Folder path unchanged.

Fixes #68. No wrapper command per the single-source-of-truth
precedent (#52).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 18:51:18 +02:00
Mads LorentzenandClaude Fable 5 a5ffcc39ff chore: untrack tracker CSV, scope scraper Bash permission, fix portal SKILL.md paths (#71)
- Untrack job_search_tracker.csv: it was both tracked and listed in
  .gitignore (same inconsistency class as the settings.local.json fix
  in #27). Users' personal rows risked merge conflicts on every pull;
  commands already create the file with the standard header when it
  is missing.
- Scope job-scraper's allowed-tools Bash entry (from #52) to
  'bun --version' and the portal-CLI invocation pattern, adopting the
  tighter form proposed in #65.
- Fix all five portal SKILL.mds documenting 'bun run skills/...'
  paths that do not resolve from the repo root ('.agents/skills/...'
  is correct) - now load-bearing since #52 wired /scrape to read
  these docs for CLI invocations. Surfaced in #66.
- Teach tools/lint_skills.py to glob-expand allowed-tools bun run
  targets so scoped wildcard permissions lint correctly.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 17:14:39 +02:00
PRATHAM KUMARandrajpratham1 1ac677dd7d This PR fixes two genuine gaps in the framework — a documented command with no backing file, and CLI tools that were installed but never called. (#52)
* feat: add /upskill command file to wire the upskill skill into Claude Code

The upskill skill and its full SKILL.md workflow already existed in
.claude/skills/upskill/SKILL.md, but there was no corresponding command
file in .claude/commands/. Without it, running /upskill in Claude Code
had zero structured behaviour — Claude would improvise with no defined
steps, mode detection, or output format.

This commit adds .claude/commands/upskill.md as the thin orchestration
layer that was missing:

- Step 0: Parses \ to determine aggregate mode (no args,
  analyses all jobs in job_search_tracker.csv) vs. targeted mode
  (a URL is passed, analyses that single posting). Unrecognised input
  triggers a clarifying prompt rather than silently misbehaving.

- Step 1: In aggregate mode, reads the tracker and exits early with a
  helpful message if it is empty, so the user is never dropped into a
  broken analysis with no data.

- Step 2: Delegates all analysis work to the existing upskill SKILL.md
  (hard skill diff, LLM synthesis, heatmap, web-searched resources,
  study order, report save). No analysis logic is duplicated here.

- Step 3: Presents a concise post-run summary — critical/high gaps,
  total estimated study time, and next-step suggestions (/scrape,
  /apply, review the saved report).

Design principle: the command is intentionally a thin driver. All
substantive logic lives in SKILL.md so it remains in one place and
is easy to update independently of the command shell.

* fix: wire CLI tools into /scrape as primary search mechanism

The repo ships five Bun CLI search tools under .agents/skills/:
  - jobindex-search   (Jobindex.dk — largest Danish board)
  - jobbank-search    (Akademikernes Jobbank — academic/professional)
  - jobdanmark-search (Jobdanmark.dk — broad coverage)
  - jobnet-search     (Jobnet.dk — government portal)
  - linkedin-search   (LinkedIn public jobs-guest API — country-agnostic)

Before this fix, none of them were ever called during /scrape. The
job-scraper SKILL.md told Claude to run WebSearch for everything,
meaning the CLIs were installed and documented but sat in dead-code
limbo with no callers.

Changes to .claude/skills/job-scraper/SKILL.md:

1. Added Bash to allowed-tools so the bun CLI commands are permitted
   by Claude Code's tool-permission system. Without this, any attempt
   to shell out would be blocked regardless of the instruction text.

2. Replaced the single WebSearch-only Step 1 with a three-part search
   strategy:

   Step 1a — bun availability check
   Runs \un --version\ first. If bun is not installed the skill
   gracefully degrades to WebSearch for all portals (Step 1c) and
   notes the fallback in the results output, rather than crashing.

   Step 1b — CLI tools as primary mechanism
   For each query term extracted from search-queries.md, runs all five
   CLIs with \--jobage 14 --limit 20 --format json\. Flags are
   consistent with each tool's documented contract so output is
   predictable. Each CLI call is independent: a non-zero exit or empty
   result on one portal does not abort searches on the others. Results
   are collected and merged before deduplication.

   Step 1c — WebSearch fallback
   Used for portals without a CLI skill (karriere.dk, jobfinder.dk,
   company career pages via site: searches) and as the universal
   fallback when bun is unavailable. This preserves backwards
   compatibility for users who have not installed bun yet.

The net effect: /scrape now actually uses the CLI infrastructure the
repo was built around. WebSearch remains available for portals outside
the shipped skill set and for users on environments without bun.

* fix: rework /scrape CLI wiring + drop /upskill command file

Two changes addressing maintainer feedback on PR #52.

--- /scrape: use portal SKILL.md as source of truth ---

The previous approach hardcoded per-portal bun invocations directly
in job-scraper/SKILL.md. This broke in practice:
  - jobbank requires --key (not --query); --query is not a valid flag
  - jobnet uses --search-string and region/occupation filters; passing
    --query silently returns the full unfiltered job firehose
  - --sort date and uniform --jobage 14 are not supported by all portals

The fix removes all hardcoded per-portal flag examples. Instead, Step
1b now instructs the agent to:
  1. Discover installed portal skills via .agents/skills/*/SKILL.md
  2. Read each portal's own SKILL.md for its documented CLI interface
  3. Translate search-queries.md terms into that portal's flag format
  4. Use each portal's supported recency and limit flags

This makes the scraper self-maintaining: new portals added via
/add-portal are automatically included without any changes to this
file, and the scraper can never drift from the CLIs again.

The bun availability check (Step 1a) and Bash in allowed-tools are
preserved - both are still needed. The WebSearch fallback (Step 1c)
is preserved and cleaned up to cover: portals without a CLI skill,
any portal whose CLI fails at runtime, and the bun-unavailable case.

--- /upskill: drop command file ---

.claude/commands/upskill.md is removed. /upskill is deliberately
skill-hosted: .claude/skills/upskill/SKILL.md is the backing file
and parses its own /upskill vs /upskill <URL> modes (same pattern
as /scrape, which also has no command file). The command file created
a second entry point that duplicated the skill's argument parsing,
violating the single-source-of-truth principle established in #44
and #49.

---------

Co-authored-by: rajpratham1 <your-email@example.com>
2026-07-08 17:08:12 +02:00
Ayobami Adegoke fce2cf23c0 feat: add /rank command to triage scraped jobs into a ranked shortlist (#43)
/scrape finds and dedupes postings; /apply evaluates one at a time in
depth. Nothing connects the two ends: after a scrape returns 20 jobs, the
user eyeballs a table to decide where to spend /apply effort. /rank is the
bridge: batch-score every new posting against the fit framework and return
a ranked shortlist.

How it works:

- Selects jobs with status "new" from job_scraper/seen_jobs.json (--all
  re-ranks everything unapplied; a focus argument filters), excluding
  anything already in job_search_tracker.csv
- Dispatches parallel general-purpose agents (~5 jobs each) that WebFetch
  each posting and score the five dimensions from 04-job-evaluation.md.
  The rubric (skill match areas, career goals, deal-breakers) is passed
  inline per the same token-efficiency rules /apply uses; agents score
  only from actually fetched content and mark dead postings expired,
  never guessing from a title
- Triage depth by design: posting text vs. profile only - no company
  research, no salary lookups. /apply's Step 1 evaluation stays
  authoritative and always re-runs on handoff
- Aggregates with the framework's 30/25/15/30 weighting and verdict
  bands; location deal-breakers veto regardless of score; deadlines
  within 7 days get urgency flags and win ties
- Updates seen_jobs.json additively (status "ranked"/"expired" plus
  rank_score/rank_verdict/rank_date) so /scrape dedup keeps working;
  the tracker is read-only. Re-running is idempotent

Integration: job-scraper SKILL.md documents the new status values and
suggests /rank after large scrape batches; README (commands list, file
tree, quick-start step 4).
2026-07-07 17:31:31 +02:00
Deepak Dhungel 37a0eed6e7 fix(skills): replace bold markdown with proper YAML Frontmatter (#16) 2026-06-15 21:41:10 +02:00
AI Job SearchandClaude Opus 4.6 c66d599d75 Initial release: AI-powered job application framework
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 08:34:06 +01:00