Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e550760
chore: rename package job-hunt to pupila
FranRom May 17, 2026
09b98cd
refactor(llm): rename JOB_HUNT_LLM* env vars to PUPILA_LLM*
FranRom May 17, 2026
60db657
refactor(cv): rename JOB_HUNT_CV_MAX_CHARS to PUPILA_CV_MAX_CHARS
FranRom May 17, 2026
81f723a
refactor(ai-review): rename JOB_HUNT_LLM references in setup-brief an…
FranRom May 17, 2026
95d3a64
refactor(feed): rename JOB_HUNT_FEED_* env vars to PUPILA_FEED_*
FranRom May 17, 2026
103ddbb
refactor(cli): rename JOB_HUNT_NO_BRIEF_CHECK to PUPILA_NO_BRIEF_CHECK
FranRom May 17, 2026
186a1dc
feat(cli): detect legacy JOB_HUNT_* env vars at startup with actionab…
FranRom May 17, 2026
9cd7a61
refactor(http): update User-Agent string from job-hunt-aggregator to …
FranRom May 17, 2026
9c75280
refactor(render): rename JOBS.md generator footer to pupila
FranRom May 17, 2026
50f4b97
refactor(ui): rename 17 Vite plugin names from job-hunt-* to pupila-*
FranRom May 17, 2026
4b9c577
refactor(ui): update scheduler-status to read pupila launchd/cron tags
FranRom May 17, 2026
6a01791
refactor(launchd): rename label base from job-hunt to pupila
FranRom May 17, 2026
0f1e414
refactor(cron): rename tag base from job-hunt to pupila
FranRom May 17, 2026
cd31700
fix(install-mcp): correct stale ogarciarevett URL to FranRom/pupila
FranRom May 17, 2026
165559b
feat(scripts): add one-shot uninstall-legacy-job-hunt.sh for safe mig…
FranRom May 17, 2026
69c67f4
docs: replace remaining job-hunt references with pupila
FranRom May 17, 2026
d2e8a39
docs(skills): drop job-hunt from skill scope headers and env var refs
FranRom May 17, 2026
bf1c418
fix(scripts): use grep -vF for legacy cron cleanup, matches inline ta…
FranRom May 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/skills/pupila-ai-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
name: pupila-ai-review
description: How the AI per-job review pipeline works in this repo - generating verdicts via local LLM CLI, parsing markdown-fenced JSON, the candidate brief lever, and the AI Apply tailored-package flow. Use when modifying src/ai-review.ts, src/lib/ai-apply.ts, tuning verdict prompts, debugging review output, editing the candidate brief, or wiring a new LLM provider.
metadata:
scope: pupila / job-hunt
scope: pupila
---

`pnpm run ai-review` is a **local-only** companion that augments selected jobs with an LLM review via `src/lib/llm.ts` (auto-detects `claude` / `codex` / `gemini` / `opencode`, override `JOB_HUNT_LLM`). Uses the user's local subscription (e.g. Claude Max) — NOT an API key, so no per-token charges.
`pnpm run ai-review` is a **local-only** companion that augments selected jobs with an LLM review via `src/lib/llm.ts` (auto-detects `claude` / `codex` / `gemini` / `opencode`, override `PUPILA_LLM`). Uses the user's local subscription (e.g. Claude Max) — NOT an API key, so no per-token charges.

The launchd/cron review agent runs daily at 07:15 by default. Without an LLM CLI, run `scripts/install-launchd.sh --no-review` (or cron equivalent).

Expand Down Expand Up @@ -54,7 +54,7 @@ Tests in `tests/ai-review-parse.test.ts` (9 cases) cover all the failure modes s

## The candidate brief

`config/candidate-brief.md` is the **only natural-language config in the repo**. Hand-edited, gitignored. Generated via `pnpm run setup-brief --file ~/cv.pdf` (or via the UI's Profile tab → drop a PDF/DOCX/MD CV). The CLI shells out to whichever local LLM CLI is installed (`claude` / `codex` / `gemini` / `opencode` — auto-detected; override via `JOB_HUNT_LLM=<provider>`).
`config/candidate-brief.md` is the **only natural-language config in the repo**. Hand-edited, gitignored. Generated via `pnpm run setup-brief --file ~/cv.pdf` (or via the UI's Profile tab → drop a PDF/DOCX/MD CV). The CLI shells out to whichever local LLM CLI is installed (`claude` / `codex` / `gemini` / `opencode` — auto-detected; override via `PUPILA_LLM=<provider>`).

The brief is embedded **verbatim** in the review prompt — it's the main lever for tuning match/skip behaviour. To change verdicts at scale, edit the brief; to change individual scores, see the `pupila-filters` skill.

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/pupila-fetchers/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: pupila-fetchers
description: How to add a new job-source fetcher (ATS API, RSS, scraper) or extend tier-S slug lists in this repo. Use when adding a new job board, integrating a new ATS, scraping a new careers site, registering a new company under Ashby/Greenhouse/Lever, or diagnosing a fetcher that returned zero items.
metadata:
scope: pupila / job-hunt
scope: pupila
---

The pipeline ingests from 13 public sources (3 ATS APIs + RSS, JSON boards, HN, HTML scrapers, an Aave Next.js scraper, and `ashby-private` for orgs whose public posting-API is disabled). Adding a source means: a fetcher, a normalizer, a `Source` literal, a slot in the orchestrator, and dedup/render wiring.
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/pupila-filters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: pupila-filters
description: How to tune job filter scoring, hard-drop rules, or debug why a specific job was kept/dropped via _signals. Use when adjusting weights in config/profile.json, adding a hard-exclude rule, tuning keyword lists, debugging fitScore, or interpreting the per-job _signals breakdown.
metadata:
scope: pupila / job-hunt
scope: pupila
---

All filter logic lives in `src/filters.ts`. Weights + keyword lists load from `config/profile.json` at runtime via `loadProfile()` (NOT a static import — the file is gitignored and auto-bootstrapped from `config/profile.default.json` on first run). Adjusting weights or keywords is a **non-code edit** to `profile.json`.
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Guidance for future Codex sessions working in this repo.

The repo ships a **neutral template** in `config/profile.json`. After onboarding (CV upload → brief generation), `/api/profile-generate` shells out to the local LLM CLI to fill in the personal keyword lists + weights based on the brief. Re-runnable from Settings → Scoring profile → Regenerate. `config/slugs.json` ships with the full ~50-company tier-S list (all public ATS URLs — non-personal data, edit by hand to add/remove companies).

**First-run UX**: a forker generates their `config/candidate-brief.md` by running `pnpm run setup-brief --file ~/cv.pdf` (or via the UI's Profile tab → drop a PDF/DOCX/MD CV). That CLI shells out to whichever local LLM CLI is installed (`Codex`, `codex`, `gemini`, `opencode` — auto-detected, override via `JOB_HUNT_LLM=<provider>`).
**First-run UX**: a forker generates their `config/candidate-brief.md` by running `pnpm run setup-brief --file ~/cv.pdf` (or via the UI's Profile tab → drop a PDF/DOCX/MD CV). That CLI shells out to whichever local LLM CLI is installed (`Codex`, `codex`, `gemini`, `opencode` — auto-detected, override via `PUPILA_LLM=<provider>`).

## Stack

Expand Down Expand Up @@ -279,7 +279,7 @@ The read happens **after** filter+dedup+sort but **before** `writeJson('data/job

## RSS feed

[`src/feed.ts`](./src/feed.ts) emits a hand-rolled RSS 2.0 XML to `data/feed.xml` containing the top 50 `newJobs` by `fitScore`. The XML is hand-built (not via fast-xml-parser) because we control the content shape — `escapeXml` covers the five entity classes. The feed metadata (title, description, link) is overridable via `JOB_HUNT_FEED_TITLE` / `JOB_HUNT_FEED_DESC` / `JOB_HUNT_FEED_LINK` env vars. To subscribe locally, point your RSS reader at the `file://` path of `data/feed.xml`. (No remote URL anymore — the project is local-first.)
[`src/feed.ts`](./src/feed.ts) emits a hand-rolled RSS 2.0 XML to `data/feed.xml` containing the top 50 `newJobs` by `fitScore`. The XML is hand-built (not via fast-xml-parser) because we control the content shape — `escapeXml` covers the five entity classes. The feed metadata (title, description, link) is overridable via `PUPILA_FEED_TITLE` / `PUPILA_FEED_DESC` / `PUPILA_FEED_LINK` env vars. To subscribe locally, point your RSS reader at the `file://` path of `data/feed.xml`. (No remote URL anymore — the project is local-first.)

## Salary parsing

Expand Down Expand Up @@ -345,7 +345,7 @@ The HTML has `<meta name="robots" content="noindex,nofollow">` as belt-and-suspe

## AI per-job review (`pnpm run ai-review`)

[`src/ai-review.ts`](./src/ai-review.ts) is a **local-only** companion to the daily pipeline that augments selected jobs with an LLM review. It shells out via `src/lib/llm.ts` (auto-detects `Codex` / `codex` / `gemini` / `opencode` on PATH, override with `JOB_HUNT_LLM=<provider>`). The CLI uses the user's local subscription (e.g. Codex Max) — **not** an API key, so there are no per-token charges. With the new local-first scheduling, the launchd/cron review agent runs this every day at 07:15 by default. If you don't have an LLM CLI installed, run `scripts/install-launchd.sh --no-review` (or the cron equivalent) to skip the review step.
[`src/ai-review.ts`](./src/ai-review.ts) is a **local-only** companion to the daily pipeline that augments selected jobs with an LLM review. It shells out via `src/lib/llm.ts` (auto-detects `Codex` / `codex` / `gemini` / `opencode` on PATH, override with `PUPILA_LLM=<provider>`). The CLI uses the user's local subscription (e.g. Codex Max) — **not** an API key, so there are no per-token charges. With the new local-first scheduling, the launchd/cron review agent runs this every day at 07:15 by default. If you don't have an LLM CLI installed, run `scripts/install-launchd.sh --no-review` (or the cron equivalent) to skip the review step.

**Inputs:**
- `data/jobs.json` — the slim list (committed)
Expand Down
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ Guidance for future Claude Code sessions working in this repo. **Slim by design*
Cross-cutting invariants (apply repo-wide):

- **`config/profile.json` is gitignored** — encodes personal scoring preferences. Auto-bootstraps from committed `config/profile.default.json` on first `pnpm run dev` / `pnpm run ui` via `bootstrapProfileIfMissing()` (idempotent — `COPYFILE_EXCL` no-ops on the steady state). Don't bypass; don't commit personalized weights.
- **Mandatory CV gate**: `pnpm run dev` checks for `config/candidate-brief.md` at startup and exits 1 if missing. Bypass with `JOB_HUNT_NO_BRIEF_CHECK=1` or `--no-brief-check`.
- **`config/candidate-brief.md` is the only natural-language config** (gitignored). Generated via `pnpm run setup-brief --file ~/cv.pdf` or via the UI's Profile tab (drop PDF/DOCX/MD CV). CLI shells out to `claude`/`codex`/`gemini`/`opencode` — auto-detected, override `JOB_HUNT_LLM=<provider>`.
- **Mandatory CV gate**: `pnpm run dev` checks for `config/candidate-brief.md` at startup and exits 1 if missing. Bypass with `PUPILA_NO_BRIEF_CHECK=1` or `--no-brief-check`.
- **`config/candidate-brief.md` is the only natural-language config** (gitignored). Generated via `pnpm run setup-brief --file ~/cv.pdf` or via the UI's Profile tab (drop PDF/DOCX/MD CV). CLI shells out to `claude`/`codex`/`gemini`/`opencode` — auto-detected, override `PUPILA_LLM=<provider>`.
- **Local-first scheduling**: daily aggregation runs via `scripts/install-launchd.sh` (macOS) or `scripts/install-cron.sh` (Linux), not GitHub Actions cron. CI runs only on push/PR for gates.
- **`data/applied.json` source of truth** for application tracking (UI writes via Vite middleware). Commit manually to persist across machines. **Don't filter applied jobs out of the main list** — user explicitly wants them visible.

Expand Down Expand Up @@ -79,7 +79,7 @@ pnpm run mcp # MCP server over stdio

`pnpm run dev` → `tsx src/index.ts` is the main pipeline (what launchd/cron runs). Steps:

1. **CV gate** — fail-fast if `config/candidate-brief.md` missing (bypass: `JOB_HUNT_NO_BRIEF_CHECK=1` or `--no-brief-check`).
1. **CV gate** — fail-fast if `config/candidate-brief.md` missing (bypass: `PUPILA_NO_BRIEF_CHECK=1` or `--no-brief-check`).
2. **Profile bootstrap** — `bootstrapProfileIfMissing()` copies `config/profile.default.json` → `profile.json` on first run.
3. **Fetch** — all 13 sources in parallel via `processFetcher()` + `Promise.all`. Each fetcher returns `{ items, errors }` and **never throws** (a rejection would kill the whole run).
4. **Normalize** — per-source `normalize<Source>()` → `Job[]`. Salary fields populated via `withSalary()` spread.
Expand Down Expand Up @@ -134,7 +134,7 @@ Settings tab (eight panels), Jinder (swipe-to-apply queue), AI Apply (per-job ta

## AI per-job review

`pnpm run ai-review` is a **local-only** companion that augments selected jobs with an LLM review via `src/lib/llm.ts` (auto-detects `claude`/`codex`/`gemini`/`opencode`, override `JOB_HUNT_LLM`). Uses the local subscription — **not** an API key, so no per-token charges. Output: `data/ai-reviews.json`.
`pnpm run ai-review` is a **local-only** companion that augments selected jobs with an LLM review via `src/lib/llm.ts` (auto-detects `claude`/`codex`/`gemini`/`opencode`, override `PUPILA_LLM`). Uses the local subscription — **not** an API key, so no per-token charges. Output: `data/ai-reviews.json`.

Daily workflow:

Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,16 @@ The repo ships with neutral defaults. To make it yours:
### 1. Clone or fork

```bash
gh repo fork FranRom/job-hunt --clone
cd job-hunt
gh repo fork FranRom/pupila --clone
cd pupila
pnpm install
```

Or click "Fork" on GitHub, then `git clone <your-fork>`.

### 2. Generate your candidate brief (required)

The brief at `config/candidate-brief.md` is the natural-language description of who you are, what you want, and what to avoid. **This step is mandatory** — `pnpm run dev` will refuse to start until the file exists. (Bypass with `JOB_HUNT_NO_BRIEF_CHECK=1` if you genuinely want raw aggregation with no AI review.)
The brief at `config/candidate-brief.md` is the natural-language description of who you are, what you want, and what to avoid. **This step is mandatory** — `pnpm run dev` will refuse to start until the file exists. (Bypass with `PUPILA_NO_BRIEF_CHECK=1` if you genuinely want raw aggregation with no AI review.)

**The file is gitignored** — it contains CV-derived personal information and should never be committed. setup-brief and the onboarding wizard write to the gitignored canonical path.

Expand All @@ -125,7 +125,7 @@ Or open the UI and use the Profile tab:
pnpm run ui # http://127.0.0.1:5173 → Profile tab → drop your CV
```

The auto-detected provider order is `claude` → `codex` → `gemini` → `opencode` (whichever is on `PATH` first). Override with `JOB_HUNT_LLM=codex pnpm run setup-brief ...`. No API keys; uses your existing CLI subscription.
The auto-detected provider order is `claude` → `codex` → `gemini` → `opencode` (whichever is on `PATH` first). Override with `PUPILA_LLM=codex pnpm run setup-brief ...`. No API keys; uses your existing CLI subscription.

> **The two personalization layers, briefly:**
> - `config/profile.json` (committed defaults) controls **what gets fetched + scored** (weights, keyword lists, tier-S slugs).
Expand Down Expand Up @@ -165,8 +165,8 @@ Two agents run on independent schedules so you can tune them separately:
./scripts/install-launchd.sh --review-time 09:00
./scripts/install-launchd.sh --no-review # aggregator only (no LLM CLI)
./scripts/install-launchd.sh --uninstall # remove both
launchctl list | grep job-hunt # check status
launchctl start dev.${USER}.job-hunt.aggregate # trigger now
launchctl list | grep pupila # check status
launchctl start dev.${USER}.pupila.aggregate # trigger now
```

launchd's `StartCalendarInterval` catches up missed runs after wake — if your laptop was asleep at 7am, it runs once the lid opens.
Expand Down Expand Up @@ -548,7 +548,7 @@ The Jobs filter bar adds two unified-skip / unified-queue controls:

[`src/ai-review.ts`](./src/ai-review.ts) is an **optional, local-only** companion that adds an LLM "second opinion" to selected jobs. Each job gets a structured review — summary, what they want, what they offer, red flags, and a verdict (`strong-match | match | weak-match | skip`) — so you can scan the day's matches in seconds instead of reading every posting.

It shells out through [`src/lib/llm.ts`](./src/lib/llm.ts) to whichever local LLM CLI is available (`claude`, `codex`, `gemini`, or `opencode`; override with `JOB_HUNT_LLM=<provider>`). There are no project API keys and no per-token billing from this repo, but this cannot run in CI because the workflow runner is not authenticated as your local CLI user. Run it locally after the daily pipeline.
It shells out through [`src/lib/llm.ts`](./src/lib/llm.ts) to whichever local LLM CLI is available (`claude`, `codex`, `gemini`, or `opencode`; override with `PUPILA_LLM=<provider>`). There are no project API keys and no per-token billing from this repo, but this cannot run in CI because the workflow runner is not authenticated as your local CLI user. Run it locally after the daily pipeline.

### One-time setup

Expand Down Expand Up @@ -733,7 +733,7 @@ Keyword arrays are joined with `|` and compiled into word-bounded, case-insensit
<summary>Click to expand the full tree</summary>

```
job-hunt/
pupila/
├── .github/
│ ├── workflows/
│ │ └── check.yml # PR/push: biome + typecheck + tests + build + audit
Expand Down Expand Up @@ -831,7 +831,7 @@ pnpm run clean:onboarding # wipe only the onboarding state (preferences + bri
pnpm run clean -- --all # full fresh-clone reset (see "Reset to a clean slate" below)
```

> **Heads up:** `pnpm run dev` refuses to start unless `config/candidate-brief.md` exists — set up your candidate brief first via `pnpm run setup-brief` or the UI Profile tab. Bypass with `JOB_HUNT_NO_BRIEF_CHECK=1` (or `--no-brief-check`) for raw aggregation without AI review.
> **Heads up:** `pnpm run dev` refuses to start unless `config/candidate-brief.md` exists — set up your candidate brief first via `pnpm run setup-brief` or the UI Profile tab. Bypass with `PUPILA_NO_BRIEF_CHECK=1` (or `--no-brief-check`) for raw aggregation without AI review.

The pre-commit hook runs `lint && typecheck` on every commit. To bypass it for an emergency commit: `SKIP_SIMPLE_GIT_HOOKS=1 git commit ...`.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "job-hunt",
"name": "pupila",
"version": "0.1.0",
"private": true,
"type": "module",
Expand Down
8 changes: 4 additions & 4 deletions scripts/install-cron.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TAG_AGG="# job-hunt:aggregate:${REPO_ROOT}"
TAG_REV="# job-hunt:review:${REPO_ROOT}"
TAG_AGG="# pupila:aggregate:${REPO_ROOT}"
TAG_REV="# pupila:review:${REPO_ROOT}"

AGG_TIME="07:00"
REV_TIME="07:15"
Expand Down Expand Up @@ -72,14 +72,14 @@ if [[ -z "$PNPM" ]]; then
exit 1
fi

# Read current crontab (no error if empty), strip any existing job-hunt
# Read current crontab (no error if empty), strip any existing pupila
# entries for this repo so re-running is idempotent.
CURRENT="$(crontab -l 2>/dev/null || true)"
CLEANED="$(printf '%s\n' "$CURRENT" | grep -vF "$TAG_AGG" | grep -vF "$TAG_REV" || true)"

if [[ "$UNINSTALL" == "1" ]]; then
printf '%s\n' "$CLEANED" | crontab -
echo "✓ Removed job-hunt cron entries for $REPO_ROOT"
echo "✓ Removed pupila cron entries for $REPO_ROOT"
exit 0
fi

Expand Down
8 changes: 4 additions & 4 deletions scripts/install-launchd.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env bash
# Install two launchd agents on macOS:
#
# 1. dev.${USER}.job-hunt.aggregate — runs `pnpm run dev` (fetch + filter
# 1. dev.${USER}.pupila.aggregate — runs `pnpm run dev` (fetch + filter
# + score + write data/jobs.json + JOBS.md + feed.xml). No LLM needed.
#
# 2. dev.${USER}.job-hunt.review — runs `pnpm run ai-review` (per-job
# 2. dev.${USER}.pupila.review — runs `pnpm run ai-review` (per-job
# LLM verdict via your local CLI: claude / codex / gemini / opencode).
# Skipped via --no-review for users without an LLM CLI installed.
#
Expand All @@ -20,7 +20,7 @@

set -euo pipefail

LABEL_BASE="dev.${USER}.job-hunt"
LABEL_BASE="dev.${USER}.pupila"
LAUNCH_DIR="$HOME/Library/LaunchAgents"
AGG_LABEL="${LABEL_BASE}.aggregate"
REV_LABEL="${LABEL_BASE}.review"
Expand Down Expand Up @@ -172,7 +172,7 @@ Trigger now:
launchctl start ${REV_LABEL}

Status:
launchctl list | grep job-hunt
launchctl list | grep pupila

Uninstall:
$0 --uninstall
Expand Down
6 changes: 3 additions & 3 deletions scripts/install-mcp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
# the client config(s).
#
# Usage:
# curl -sSf https://raw.githubusercontent.com/ogarciarevett/job-hunt/main/scripts/install-mcp.sh | bash
# curl -sSf https://raw.githubusercontent.com/FranRom/pupila/main/scripts/install-mcp.sh | bash
# # or, if you've cloned the repo already:
# bash scripts/install-mcp.sh
#
# Env overrides:
# PUPILA_HOME - install location (default: $HOME/.pupila)
# PUPILA_REPO - git URL (default: https://github.com/ogarciarevett/job-hunt.git)
# PUPILA_REPO - git URL (default: https://github.com/FranRom/pupila.git)
# PUPILA_REF - branch/tag/commit to checkout (default: main)
# PUPILA_DRY_RUN - if set to 1, print intended actions and exit
#
Expand All @@ -25,7 +25,7 @@
set -euo pipefail

PUPILA_HOME="${PUPILA_HOME:-$HOME/.pupila}"
PUPILA_REPO="${PUPILA_REPO:-https://github.com/ogarciarevett/job-hunt.git}"
PUPILA_REPO="${PUPILA_REPO:-https://github.com/FranRom/pupila.git}"
PUPILA_REF="${PUPILA_REF:-main}"
PUPILA_DRY_RUN="${PUPILA_DRY_RUN:-0}"

Expand Down
Loading