21 KiB
Changelog
All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Releases are vetted checkpoints of master. If you maintain a personalized fork,
prefer updating to a tagged release over pulling raw master (see
SETUP.md, section 8). The
framework_version markers on methodology files tell you which of your customized
files a release touched; python3 tools/check_upstream_updates.py lists them with
per-file diff commands.
Unreleased
Added
-
Spec-pinning tests for the Language Gate's
/rankcontract (#278) - four regression guards intests/test_rank_command.pypinning thelanguage_gate/language_notefields through Steps 2-5 of/rank, including the Step 4 persistence rule that was live-debugged during #275 (vetoes reported in console output butlanguage_gate: nullon every persisted entry). Mirrors the existinggaps/strengthspinning pattern. No behavior change. -
The jobnet and jobdanmark CLIs identify themselves on every API request (#283) - their
apiFetch/apiPostwrappers now send an explicitUser-Agent(jobnet-cli/1.0,jobdanmark-cli/1.0) instead of Bun's anonymous default token, matching the honest self-identification jobindex already uses onhtmlFetch. The newuser-agent.test.tssuites assert the header on every request wrapper. No response behavior observed to change.
Changed
- The four Danish demo portals now ship disabled (#288) -
jobindex-search,jobbank-search,jobdanmark-search, andjobnet-searchdefault toenabled: false, and/setup's job-portals question now acts on the answer: it flips them toenabled: truewhen your market is Denmark, and leaves them off otherwise. Previously a non-Danish user's/scraperan all four Danish boards by default, spending tokens fetching and filtering irrelevant listings. Fork heads-up: if you search the Danish market, setenabled: truein those fourSKILL.mdfiles after updating (or re-run/setup --section search); forks that already curated their portal set are unaffected.
Fixed
-
The robots gate did not fail closed (
tools/robots_check.py, #277). Found by an adversarial review run over the merged file, not by inspection. Both cases are pinned intests/test_robots_check.pyas FAIL-OPEN REGRESSIONs:- A soft
200granted permission. A host answering/robots.txtwith an HTML error page at status 200 produces a body that parses to zero rules, and zero rules read as "allowed" - so the browser-header retry ran on permission that was never given. A non-empty body carrying no recognised directive is now treated as unreadable. A genuinely empty file stays allow-all, per RFC 9309. Disallowpatterns were never percent-decoded while the request path was, soDisallow: /foo%20barnever matched/foo barand the rule was silently skipped - a fail-open on any site that encodes its own rules.
- A soft
-
curlargument hardening (tools/robots_check.py). The curl argv had no--terminator before the URL.gate()rebuilds the target asscheme://host/robots.txtbefore calling_fetch, so the gate path was never exposed; this is hardening for direct callers, with a test pinning the terminator, that a dash-leading argument fails closed end to end, and thatgate()never passes a caller-supplied URL through to curl.--max-redirs 5is set explicitly rather than left to curl's default. -
Negative and fractional filter flags are rejected in the Danish portal CLIs (#281) -
--jobage(jobindex),--radius(jobnet),--category/--jobtitle-id(jobdanmark), and--company(jobbank) now validate as positive integers, completing thepage/limit/per-pagetightening from #191. Some portals silently ignore invalid filter values and return unfiltered results, so a mistyped ID produced wrong results instead of an error. -
The upstream checker reports files missing from the upstream ref instead of a silent
[OK](#282) - if upstream renames or deletes a tracked framework file, a fork'scheck_upstream_updates.pynow lists it under a[WARNING]summary instead of skipping it and printing a false all-clear. -
09-web-research.mdis now tracked by the upstream checker - the file shipped in #277 but was never added toFRAMEWORK_FILES, so forks got no signal when it changed. -
jobbank and jobdanmark CLIs identify honestly - jobbank's
User-Agentwas a full Chrome browser string and jobdanmark's detail command sent a bareMozilla/5.0; both now use theMozilla/5.0 (compatible; <portal>-cli/1.0)token the other portal CLIs use, matching the identification posture settled in #277. Verified live: both portals serve identical responses to the honest token. -
A
WebFetch403 is no longer treated as a dead posting -WebFetchsends a bot user agent, and many bank and corporate sites answer it with HTTP 403 while serving the same page to a browser normally. Every command read that as "page unavailable" and degraded silently instead of failing loudly:/rankmarked live postingsexpired,/applyfell back to search-result snippets or to vague cover-letter prose, and/scrapestored listing-page#fragmentURLs that fetch fine but return unrelated jobs, breaking every later run on that entry. New09-web-research.md(framework_version1.0.0) is the single reference: the trust boundary, a curl browser-header retry with a tag-stripping extractor, a four-step escalation order, the login-wall case, why the employer's own careers posting beats an aggregator listing (the requisition ID and the grade survive there), and the rule that a search snippet is a lead rather than a source. Wired into/apply,/rank,/interview,/outcome,/notion-sync, the job-scraper skill, and writing-style rule 5 (03-writing-style.md1.1.0 to 1.2.0).The retry is gated on
robots.txt.WebFetchidentifies itself asClaude-Userand honorsrobots.txt, so a 403 means either a WAF default on a site whose published policy allows access, or a site that has actually declined. Newtools/robots_check.pytells them apart and the escalation runs it before retrying: a disallow for*orClaude-Userskips the retry entirely and goes straight to finding the employer's own posting. The rule is stated in the file so later edits do not erode it - the retry exists to get past bot-filtering firewalls on sites whose robots.txt permits access; it is never used to override a site that has said no. Two findings are pinned bytests/test_robots_check.py(15 offline cases): the WAF usually blocksrobots.txtitself, so the policy is read as a browser when the honest request is refused and then obeyed strictly; andurllib.robotparsercannot be used, because it ends a record at a blank line and matches in file order, which reads a real-world policy as "everything allowed".
[1.3.0] - 2026-08-03
Added
- Language Gate - no dimension or gate anywhere in the framework checked a posting's
language requirements against what the candidate actually speaks (not a Scoring Dimension,
not a
/scrape//rankfield, nothing for/apply's existing generic language detection to report to). Adds that check, structured like the existing Eligibility Gate, on a new structuredLanguagestable in CLAUDE.md /01-candidate-profile.md(/setupasks, or infers it from a CV/LinkedIn export): a posting requiring a language you haven't declared at all is a hard FAIL; one requiring a higher level than you declared in a language you do work in is FLAG, not an auto-reject, so borderline cases (a strict "fluent" bar vs. your own B1/B2) get your judgment instead of a silent drop; a requirement at or below your declared level is a clean PASS. Wired through/scrape,/rank, and/apply, withlanguage_gate/language_notepersisted intoseen_jobs.jsonalongside the existinglocationveto so a re-read of the file (or a future debugging session) can recover why a job did or didn't make the shortlist.
Fixed
-
CV date fields now use ASCII hyphens, so the PDF text layer extracts cleanly - the stock template wrote date ranges as
[YYYY--YYYY], and on the repo's mandatedlualatextoolchain the--en-dash ligature extracts from the PDF as U+FFFD (�). The stock template therefore failed the ATS checklist's own "no�replacement characters" item on every date field, and did so silently: the rendered page looks correct, and no existing check inspected the extracted text.cv/main_example.texnow uses[YYYY-YYYY]and[YYYY-Present], and05-cv-templates.mddocuments the failure mode and the check that catches it (framework_version1.3.0 to 1.4.0). The two-page layout budget is unaffected.Fork reconciliation note. The five changed lines in
cv/main_example.texare the\cventrydate fields - three under Professional Experience, two under Education - precisely the lines every fork personalizes. Rebasing forks should expect conflicts there, resolve them in favour of their own dates, and then apply the same--to-change by hand. To find remaining instances across your own CV variants:grep -rn '\\cventry{[^}]*--' cv/Verify afterwards by extracting the text layer and checking the date lines specifically:
pdftotext -layout <file>.pdf - | grep '�'- none of the hits may be a date field. (On the stock template two benign hits remain either way: the decorative separators on the contact and award lines, which are unrelated to dates and predate this fix.) -
tools/convert_salary_excel.pynow parses localized numeric string cells - Excel exports that store numbers as text (a Danish"108,5","1.234,5", or space-separated thousands) previously hitfloat()'sValueErrorand were silently dropped fromsalary_data.json. The ambiguous single-comma-plus-three-digits pattern ("1,234", thousands in one locale and a decimal in another) is deliberately skipped rather than guessed, preserving the old safe behaviour for the one case that cannot be disambiguated. (#272) -
tools/check_upstream_updates.pycompares the template-repo slug case-insensitively - GitHub serves repository paths case-insensitively, so a clone made from a lowercased URL was a legitimate direct clone that nonetheless triggered #265's fork-vs-self warning. (#273)
Changed
- SETUP.md section 8 now shows the first-time
git remote add upstream ...command before telling you togit fetch upstream, which previously failed on any clone of a personal fork with no explanation of the missing remote. (#274)
Security & privacy
- The gitignore guard now covers every personal-output rule -
security_guards.pyadditionally requires the ignore rules for Gmail sync state (gmail_sync/), generated dashboards (reports/), upskill reports (upskill/*.md), Notion sync state (**/job_scraper/notion_sync.json), pasted postings (documents/postings/**), scraper markdown output (**/job_scraper/*.md), and behavioral-report / LinkedIn-profile PDFs. With these, every.gitignorerule outside the guard's required list is build tooling noise, so any future weakening of the personal-data boundary fails CI. All rules were already present in.gitignore; the guard now enforces the full set. (#271)
[1.2.0] - 2026-08-01
Added
/ranknow persistsstrengthsandgapsintoseen_jobs.json- Step 2's scoring agents already produced both arrays per job; Step 4 previously kept onlyrank_score,rank_verdict, andrank_date, so the honest per-posting findings were printed once in Step 5 and then discarded. Both arrays are now stored verbatim and replaced (never accumulated) on--allre-ranks, so downstream consumers ofseen_jobs.jsoncan read real triage findings instead of re-deriving them. See discussion #258./upskillaggregate mode now ingests/rank's recorded gaps - previously it only readjob_search_tracker.csvand guessed required skills from therole/sector/notescolumns, even though/rankhad already fetched and scored postings that never made it into the tracker. Aggregate mode now also reads ranked entries (rank_score >= 45) fromjob_scraper/seen_jobs.json, dedupes them against tracker rows on case-insensitive company+role, and prefers a job's recordedgapsover an inferred skill list wherever both exist. The heatmap's Gap Source column now shows the recorded-vs-inferred split per skill, and the report header states how many jobs came from each source. Depends on #263 (/rankpersistinggaps/strengths); see discussion #258.
Security & privacy
- SETUP.md no longer calls a fork "private working space" - forks of public GitHub
repositories are always public, so that wording invited exactly the personal-data
exposure it seemed to rule out. Section 8 now states the fork-is-public fact plainly and
documents the safe alternative (a private repository with this repo as
upstream), and/setupends with a matching privacy note the moment profile data first lands in tracked files. Prompted by discussion #266. - The gitignore guard now covers two more personal-data rules -
security_guards.pyrequirescover_letters/Cover_*.*(the uppercase cover-letter naming variant/applyrecognizes) andcv/*.txt(ATS text extractions of tailored CVs) in.gitignore, so a future change weakening either rule fails CI instead of silently making personal files trackable. Both rules were already present in.gitignore; only the guard lagged.
Fixed
tools/check_upstream_updates.pyno longer reports a false "up to date with upstream" when it silently falls back to a fork's ownoriginremote - the default state of a plain fork clone, where the script compared the fork against itself and could never detect upstream updates. It now warns that the fallback remote is not the template repo, shows thegit remote add upstreamcommand to fix it, and names the ref it actually compared against. (#265)- Removed the vestigial
cover_letters/OpenFonts/cover.cls- an unreferenced remnant of the original font bundle that, since #252's class rename, ambiguously declared the samecoverclass as the realcover_letters/cover.cls. - Added regression tests pinning #252's ragged-row bounds fix in
tools/convert_salary_excel.py(dimension-less workbooks read inread_onlymode yield rows shorter than the header).
Changed
- CONTRIBUTING's "run what CI runs" list is now complete - it previously omitted
tools/security_guards.pyand the exactunittestinvocation, the precise checks a contributor PR had already failed on. Prompted by issue #262.
[1.1.0] - 2026-07-30
Security & privacy
- Personalized custom-template files are now gitignored regardless of engine - the
ignore rules broadened from
cv/main_*.textocv/main_*.*(and likewise for cover letters), so a fork using a Typst or other non-LaTeX template no longer commits personalizedmain_<company>.typfiles to a public fork. The*_example.texfiles stay tracked. If you registered a custom template before this release, checkgit statusonce after updating. (#238) - Dependency review is live, for forks too - the repo's Dependency graph is now enabled,
so the CI
dependency-reviewjob actually blocks PRs that introduce dependencies with known high-severity vulnerabilities, and the job is no longer gated to the upstream repo: forks get the same check, self-activating if the fork enables Dependency graph (it warns-and-passes otherwise). (#254)
Added
- freehire-search: full descriptions come back with the search -
searchnow calls freehire's agent search endpoint (/api/v1/agent/jobs/search), which serves each hit's complete description instead of the search index's truncated preview. A 20-role search is one request rather than 1 + 20detailcalls, and/scrape's Step 2 no longer needs a per-hit fetch for this portal.--description-format markdown|text|html(defaultmarkdown) selects the rendering;tableandplainoutput is unchanged. (#251) - Custom templates: any compile-to-PDF toolchain (Typst, ...) -
/add-templateno longer hardcodes alualatex/xelatex/pdflatexengine enum. Custom templates now declare a source extension and a full compile command, so Typst (typst compile) registers the same way a custom LaTeX template does. Stock CV/cover letter templates stay LaTeX, unchanged. (#238) - Application-form fields as an optional third
/applyartifact - when a posting's application form asks screening questions,/applycan now offer a prep sheet of grounded answers alongside the CV and cover letter. Opt-in; the default two-document output never changes. (#212) - Confirmed facts write back to the profile - when
/applyor/interviewsurfaces a fact the user confirms (a skill, a date, a project detail), it is written back to the profile files in the same turn instead of being lost with the conversation. (#211) - CV methodology: in-progress qualifications and tenure-vs-output -
05-cv-templates.mdgains explicit rules for stating in-progress certifications/degrees honestly and for checking claimed tenure against visible output (framework_version1.2.1 -> 1.3.0). (#210) - Scraper flags mass-posting and recycled-listing patterns -
/scrapemarks postings that look bulk-posted or recycled so they don't eat evaluation effort. (#207) - Retry contract pinned in CI - all six portal CLIs now carry 429/5xx retry-backoff tests covering every fetch wrapper, so a silent regression in retry behavior trips CI. (#246)
- README: the extension model, documented - new Customization subsection "Extending the framework: portals, templates, criteria - and borrowing from other forks": the three extension points, the copy-one-folder pattern for borrowing a portal skill from another fork with a read-the-code-first checklist, and why there is deliberately no installer (the manual copy is the security model). Prompted by discussion #249.
Fixed
/rankshortlist and below-threshold tables include each posting's URL. (#236)convert_salary_excel.py: count/index columns pair by category name instead of adjacency (#219), standalone count columns store as counts (#230), and ragged rows from dimension-less spreadsheets no longer crash with an IndexError (#252).cover.cls: duplicate package imports removed and the\ProvidesClassname fixed to match the filename, silencing a class-name-mismatch warning. (#252)- Portal CLI type-checking pinned to concrete
@types/bun/@bunli/*versions to stop environmental CI type-drift. (#226) freehire-searchpoints at freehire.me after the service's domain migration. (#229)verify_pdf.py's missing-poppler error now includes per-OS install hints. (#252)
1.0.0 - 2026-07-22
First tagged release. This marks the framework as stable and gives forks a described
checkpoint to update against instead of a moving master. It is a baseline of what
already exists rather than a set of new changes; subsequent releases will document
what changed since the previous tag.
At this baseline the framework provides:
- Application workflow - a drafter/reviewer
/applypipeline (CV + cover letter), plus/setup,/scrape,/rank,/interview,/outcome,/upskill,/expand,/html-report,/gmail-sync,/notion-sync,/add-portal,/add-template, and/reset. - Portal search skills - country-agnostic job-board CLIs (LinkedIn, freehire, and
the Danish boards) in the portable Agent Skills format under
.agents/skills/, discovered and orchestrated by/scrape, with anenabled:toggle for skipping portals. - Framework versioning -
framework_versionmarkers on methodology files plustools/check_framework_version.py(CI guard) andtools/check_upstream_updates.py(fork-side update preview). - Privacy and safety guards -
.gitignoreprotection for personal data, thetools/security_guards.pyallowlist for.gitignorenegations, and a CI policy of making no live portal requests. - Cross-runtime support - a root
AGENTS.mdpointer so Codex and Antigravity can discover the portable portal skills, with Claude Code as the reference runtime.