v1.6.0

ai-job-search-dev/ai-job-searchv1.6.0Aug 19, 2026by MadsLorentzen

Upgrading?

AI Summary

Major release focusing on contract standardization and text preservation. All CLIs now reject unknown flags, CV text survives PDF generation, and portal data contracts are unified.

Key Highlights

  • All six portal CLIs now reject unknown flags (breaking change).
  • CV text (bullets, special characters) now survives the PDF text layer.
  • All portals now emit the same contract (title, company, etc.) and ISO dates.
  • Jobindex detail command works again after site redesign.
  • Onboarding warns about public forks to prevent accidental privacy leaks.

Breaking Changes

  • All portal CLIs exit 1 with UNKNOWN_FLAG on unknown flags.
  • Jobbank deadlines are now YYYY-MM-DD (old DD.MM.YYYY is no longer valid).
  • CV text loss modes (bullets, % signs) are fixed, changing how bullets appear in PDFs.

New Features

  • seen_jobs.json records source (CLI vs WebSearch).
  • Job matching reframed around function, not title.
  • /gmail-sync searches beyond the Inbox.
  • /rank persists location_verdict.

Full Release Notes

Two themes this release: **things that used to fail silently now fail loudly**, and **the text you wrote actually survives into the documents you send**. Most of it comes from a deep empirical review of the whole template ([#347](https://github.com/MadsLorentzen/ai-job-search/pull/347) - 25 fixes, each carrying the test that would have caught it), alongside a portal-contract series and an onboarding privacy fix from the community.

## Highlights

### All six portal CLIs reject unknown flags (**breaking**, #347)
A silently discarded flag is worse than an error: on one portal, a plausible-but-wrong flag name returned the **entire database** - 13,862 postings, none matching - with exit 0 and empty stderr, indistinguishable from a real result set. Every CLI now exits 1 with `{"error", "code": "UNKNOWN_FLAG"}` on a flag it doesn't know, which is the bar `/add-portal` always set for contributed portals. If a script of yours passes a stray flag, it will now hear about it - that's the point.

### Your CV's text now survives into the PDF (#347)
Three silent text-loss modes, all with clean compiles and green CI: bullets written as `\item [text]` were parsed as LaTeX optional labels and rendered clipped off the page edge (the shipped example CV lost every experience bullet this way); an unescaped `%` in a quantified achievement ("cut latency by 40% and saved DKK 2M") silently deleted the rest of the line; and the documented ATS extraction command mis-decoded every non-ASCII character on Xpdf-based `pdftotext` builds, failing correct CVs. All fixed, a "LaTeX Special Characters" guide added, and CI now asserts body text survives into the PDF text layer.

### Every portal speaks the same contract now (#339, #340, #342, #344, #347)
The `/scrape` search contract (`title`, `company`, `location`, `date`, `url`) is now emitted by all six portals (thanks @oscarbol09 for the jobnet/jobdanmark/jobbank series), and deadlines are `YYYY-MM-DD` or `null` everywhere: jobbank's `DD.MM.YYYY` converts (**breaking** if you parsed the old form), jobindex's `"ASAP"` maps to `null` (it means "no stated deadline", which is what `null` is for), jobnet's `1900-01-01` sentinel no longer leaks from `detail`, and jobdanmark's HTML fallback stops emitting raw page text and the literal `"Løbende"` as dates. A repo-level contract pin (@oscarbol09, #344) derives the field list from the spec sentence itself and fails CI if any portal drifts - and `/rank` now treats any non-ISO deadline already stored in your data as absent rather than guessing.

### The jobindex `detail` command works again (#347)
Jobindex redesigned its pages; every selector the parser used was gone, so `detail` returned CSS-comment text as the deadline, an external ATS URL as its own id, and null everything - exit 0, on 4 of 5 live postings. Rewritten against both current page shapes; 5/5 live postings now return full descriptions and correct fields.

### Onboarding warns about public forks before it's too late (#345, #348)
The quick start walked a new user into `gh repo fork` - forks of public repos are **always public** - and two steps later `/setup` wrote their name, contact details, and salary expectations into tracked files, with the only complete warning buried in the updates section. Reported by @basilevs with a full reproduction after hitting it himself. The warning now sits next to both fork commands, and `/setup` checks your origin's visibility **before** writing anything, not after.

## Also in this release

- **`seen_jobs.json` records whether each job came from a CLI or the WebSearch fallback** (#338) - ghost-job reports are now diagnosable from stored state. Thanks @yshraj, with an empirical community review by @oscarbol09.
- **Job matching reframed around function, not title** - `/setup` and the evaluation framework now build priority categories around the kind of problem a role solves, with title variants as query inputs rather than the bet itself.
- **A `/` in a company or role name no longer nests the application archive one level too deep** (#329) - the canonical single-path-component rule now reaches all six derivation sites. Thanks @ayobamiseun, building on @jakob1379's independent spec.
- **`convert_salary_excel.py` no longer corrupts US-formatted numbers 1000×** - locale is decided by which separator appears last, and Danish compound headers pair correctly.
- **`/html-report`'s funnel counts stages from history** - an application that interviewed and was later rejected now counts as having interviewed, and declined offers stop inflating the rejection rate.
- **`/gmail-sync` searches beyond the Inbox** - `-in:sent -in:drafts` replaces `in:inbox`, so archived and label-filtered mail stays in scope.
- **`/rank` persists its location verdict as `location_verdict`** - the bare `location` key meant both a place and a PASS/FAIL/FLAG verdict in the same store; legacy entries are read compatibly.
- **`/reset documents` now clears `documents/postings/`**, `/upskill` handles rows with no fit rating, `/scrape` gets a client-side recency fallback for portals without a date filter, and `freehire-search` gains `--no-description` for cheap discovery passes (~83% smaller payloads).
- **The guards are finally guarded** - `check_framework_version.py` (the fork-safety CI gate) had zero tests and could be disabled by one line; it, `lint_skills.py`'s real checks, `robots_check.py`'s tie-break and browser-UA fallback, and CI's placeholder sentinels (which watched a comment, not the data) are all now covered by discriminating, mutation-verified tests.
- **CONTRIBUTING: invited PRs are reserved for the invitee.**

## Updating a personalized fork

Versioned files touched since v1.5.0:

| File | v1.5.0 | v1.6.0 |
|---|---|---|
| `job-application-assistant/SKILL.md` | 1.3.2 | 1.3.4 |
| `04-job-evaluation.md` | 1.2.2 | 1.2.4 |
| `05-cv-templates.md` | 1.4.0 | 1.4.2 |
| `06-cover-letter-templates.md` | 1.0.1 | 1.0.2 |

Run `python3 tools/check_upstream_updates.py` for per-file diffs, or `python3 tools/upstream_triage.py` for the commit-level view.

Five things to check after pulling:

1. **Scripts passing stray flags to portal CLIs now get exit 1** (`UNKNOWN_FLAG`) instead of silently unfiltered results. Fix the flag name; the CLI's `--help` and SKILL.md list the real ones.
2. **jobbank deadlines are now `YYYY-MM-DD`.** Anything parsing the old `DD.MM.YYYY` search output must update.
3. **Expect a small merge conflict in `cv/main_example.tex`** if you personalized it: upstream braced the placeholder bullet lines you replaced. Resolution: keep your own bullets - real prose was never affected by the bug.
4. **Stored `seen_jobs.json` data needs no migration.** Old `"ASAP"`/`DD.MM.YYYY` deadlines are treated as absent by `/rank`'s sweep rather than compared; a legacy PASS/FAIL/FLAG value in `location` is read as the verdict until re-ranking moves it to `location_verdict`.
5. **If your fork is public and personalized, read #345.** The new onboarding warnings are for the next user; if your own profile data already reached a public fork, GitHub's sensitive-data-removal process is the reliable path - force-pushing doesn't purge fork networks.

Related Repositories