Addresses #213 (how to keep up with a fast-moving upstream) and closes the
verification loophole surfaced in the 2026-07-22 triage audit.
- Add CHANGELOG.md (Keep a Changelog + semver), with v1.0.0 as the first
tagged baseline and an Unreleased section for going forward.
- SETUP.md section 8: recommend updating to a tagged release (a vetted,
described checkpoint) over pulling raw master; fetch --tags and merge a tag.
- README: add a "Staying up to date" pointer to Releases, the CHANGELOG, and
check_upstream_updates.py.
- CONTRIBUTING.md: sharpen "Claims get verified" - a test that distinguishes
master from the fix is necessary but not sufficient; the failing input must
be one the workflow actually produces, not one the test hand-builds. Fixes
demonstrated only through a synthetic input the real code path never receives
get declined even when their test is green.
Note: the git tag / GitHub Release for v1.0.0 is intentionally left for the
maintainer to cut.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a work-authorization eligibility dimension to 04-job-evaluation.md: whether the candidate is legally permitted to hold the role (citizenship/PR/visa requirements) as a hard filter, distinct from permit-timing. Country-agnostic, quotes the requirement source verbatim, treats silence as not-permission, and surfaces to the user rather than silently dropping. framework_version bumped 1.0.0 -> 1.1.0.
By @LeoWinston-9596 (split from #199). Note: the referenced /setup 'second gate' permit-timing collection isn't wired yet - a natural follow-up.
The jobdanmark detail parser flagged a soft-404 by testing whether the page title contained the substring '404' anywhere, so a legitimate posting titled e.g. 'Room 404 Cleaner' was wrongly rejected as NOT_FOUND. Narrows title matching to startsWith('404') plus specific error phrases ('page not found', Danish 'siden blev ikke fundet'), keeping the existing body-text backstop. Verified: strictly reduces false-positives, the real 404-page title ('404 | Jobdanmark') still detected, tests pass network-free.
By @oscarbol09.
The jobindex and linkedin detail parsers matched description containers with a non-greedy regex that stops at the first inner </div>, so any posting whose description contains nested divs was silently truncated (jobindex dropped later sections; linkedin dropped everything after the first block). Replaces the regex with a depth-tracked extractDivContent scanner that walks div open/close markers to the matching close. Verified: truncation bug reproduced against real markup fixtures, depth arithmetic correct (no off-by-one/infinite-loop), 28 tests pass network-free, no regression on non-nested divs. Malformed-HTML over-grabs rather than truncates - the safer failure, cleaned by downstream stripTags/decode.
By @oscarbol09.
job_scraper/seen_jobs.json (and notion_sync.json / *.md) were ignored by a repo-rooted pattern, but the job-scraper skill resolves job_scraper/ relative to its own directory, so the state file lands at .claude/skills/job-scraper/job_scraper/ and the rule never matched - publishing every scraped posting with fit scores and skip-reasons on a public fork. Switches to **/-prefixed patterns that match at any depth (the rooted location still matches too, so no regression), and adds documents/interview/** (interview prep names employers, quotes submitted material, and lists the candidate's weak points) - it was never ignored though documents/applications/** was. REQUIRED_IGNORE_RULES updated in lockstep so the security guard stays in sync.
By @LeoWinston-9596 (split from #199). Verified: both nested and root seen_jobs.json now ignored, interview records ignored, guard suite green (17 tests incl. #195's negation checks). Rebased cleanly on current master.
The moderncv template's section headings (Core Competencies, Professional Experience, Education, Languages, Publications, Honors and Awards, References) and the References boilerplate line are literal English text the workflow never translates, so a CV localized in prose can sit under English scaffolding. Adds an illustrative, fork-aware rule to 05-cv-templates.md (translate the headings too, whatever your template defines) plus a verification-checklist item in CLAUDE.md. Ties into the CV-language profile setting from #179.
By @Lautaro073. Heading list corrected against the stock template on review (Publications/Honors and Awards in, non-existent Independent Projects out; made illustrative for forks).
The freehire and linkedin CLIs called main().then(code => process.exit(code)) with no .catch(). If main() throws or returns a rejected promise, the .then() never runs, process.exit() is never called, and the runtime terminates with exit code 0 - a runtime failure becomes indistinguishable from success (and would suppress the Step 1c WebSearch fallback, which keys on non-zero exit). Adds a .catch() that writes the same JSON error shape the rest of each file already uses ({error, code: INTERNAL_ERROR} to stderr) and exits 1. Scoped to the two self-contained CLIs only; the bunli portals catch internally via defineCommand.
By @oscarbol09.
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.
Adds Step 2b to /outcome: surfaces open applications gone quiet (default 10 days), drafts a brief channel-appropriate follow-up in the candidate's voice using only claims from the already-submitted materials, and logs it (followed up marker in notes + followup_YYYY-MM-DD.md in the archive). Reachable from the no-arg pipeline table (now showing days-quiet and follow-ups-sent) and via /outcome followup [N|company]. Draft-only never send, capped at two follow-ups terminating into Step 2's existing no_response path, and a thank-you note offered the moment Step 3 records a completed interview stage. Reads the contact_person column nothing previously consumed.
Folded into /outcome rather than a standalone command - dependency-free (unlike /gmail-sync) and reusing detection, the archive, the notes ledger, and the resolution path already there. The invited return of #46 (closed stale, not on merit). Guardrail tests mirror the /notion-sync pattern; 10-vs-30-day threshold contrast with /gmail-sync documented and test-pinned.
By @ayobamiseun.
convert_salary_excel.py detected the city column via exact membership (h_lower in CITY_PATTERNS), so real headers like "City Name", "City/Kommune", or "Kommune <suffix>" never matched and every company was written with an empty city field. Switches to header_matches(h, CITY_PATTERNS) - the same whole-token matcher already used for the company, count, index, and ID columns. Same bug class as #151 (company column); bare "City"/"Kommune" inputs are unaffected. Regression test covers bare and suffixed headers.
By @oscarbol09.
None of the board CLIs set a fetch timeout, and the retry loops react only to HTTP status codes, not to a connection that is accepted then never responds (black-holed TCP, hung TLS, stalled proxy) - so await fetch(...) never settles and the command hangs with no output and no exit. freehire's helper even documented a fast-degrade contract its try/catch didn't deliver on a mid-flight stall. Adds signal: AbortSignal.timeout(15000) to every fetch across all six CLIs, with network-free tests asserting the signal is present on each request wrapper.
By @thejesh23. Verified: 8 timeout tests pass locally with fetch stubbed (no network), and would fail on the pre-fix code.
Closes#196
check_gitignore() verified each required personal-data rule was present via set membership, but .gitignore is order-sensitive: a later !pattern re-includes a file an earlier rule excluded, so the required line stays physically present while the file is no longer ignored - the guard failed open on exactly the weakening its docstring claims to catch. Keeps the required-rules-present check and additionally rejects any negation line outside a small reviewed ALLOWED_IGNORE_NEGATIONS allowlist (same explicit-widening pattern as ALLOWED_PERMISSIONS). Fixes#194.
By @thejesh23. Verified: allowlist matches the four negations currently in .gitignore; guard test suite passes locally (17 tests) and in CI.
Closes#194
Documents the Windows Basic MiKTeX path missing from SETUP.md's minimal-TeX section: enabling silent [MPM]AutoInstall so missing-package installs don't block on a GUI prompt in non-interactive terminals, an mpm pre-install alternative matching the macOS TinyTeX package list, and a PowerShell translation of the existing bash smoke tests. Nested under the LaTeX section as a peer of the TinyTeX/BasicTeX subsection.
By @oscarbol09 (first contribution). Package names, AutoInstall config, and the PowerShell smoke block empirically verified on Windows 11 + MiKTeX.
Follow-up to #187: postings/ filenames become <Company> - <Job Title>.txt (collision-free across companies, and /apply gets the company name for free), and the postings/ section gains the untrusted-input reminder - pasted posting text is data to evaluate, never instructions to follow, per SECURITY.md's established rules.
By @Lautaro073.
Bare z.coerce.number() accepted negative and fractional values for count/pagination flags, and slice(0, limit) with a negative limit silently dropped trailing results instead of erroring. Tightens the schemas to .int().min(1) across all four Danish portal CLIs (including jobnet occupations --per-page) with network-free validation tests.
By @ayobamiseun.
Extracts the JSON-LD JobPosting lookup into a recursive parseJobPostingJsonLd helper that handles top-level objects, arrays, and @graph wrappers (including nested combinations), keeps skipping malformed scripts, and covers all four cases with network-free Bun tests.
By @luochen211. Closes#189.
Scans Gmail (via the claude.ai Gmail connector) for status signals on open tracked applications - interview invites, assessments, offers, rejections - and proposes them as a sourced batch the user must approve before anything is written to job_search_tracker.csv or outcome.md. Never proposes hired/offer_declined (user's real-world decision), routes conflicting signals to manual /outcome, appends-only to Notes, idempotent by message ID, read-only against the mailbox, and degrades to one message + clean exit without the connector (the /notion-sync precedent). State is gitignored personal data.
By @chenyuan99, who raised the connector-dependency tension himself and restructured classification and writing into separate approval-gated steps.
Reviewer-side Factual Grounding Audit: every date, employer, job title, and quantitative metric in both drafts is checked against the union of 01-candidate-profile.md + cv/main_example.tex + CLAUDE.md's Candidate Profile section (grounded if ANY source supports it; inter-source mismatches surfaced as profile-consistency warnings; draft drift returned as Part A edits with reason "grounding"). Drafter-side rule makes those three sources the sole source of truth for facts - existing tailored CVs are structure/phrasing reference only. Fixes the structural drift loop where tailored output fed back as source material; companion to the maintainer-side #178 setup fix.
Reported, diagnosed, and implemented by @jovin-nicholas (#177); the failure mode was validated empirically by the output benchmark (blind judges found the exact escalation class in pre-audit outputs).
Closes#177
Manual fallback for postings that block automated fetches: paste the text into documents/postings/<Job Title>.txt. Gitignored so posting text stays local; README documents the convention as a scratch inbox, with applications/<company>_<role>/job_posting.md remaining the archive.
Seven improvements sourced from blind regression probes comparing current
outputs against real April-2026 baselines (each an area where the older
outputs scored better):
- every stated requirement addressed - matched or honestly gapped, never
silently omitted (the benchmark run omitted a stated Kubernetes
requirement entirely; omission reads as hiding under questioning)
- nice-to-haves engaged by name with honest adjacency framing; posting's
literal term preferred, including in CV section headings
- stated logistics/prerequisites addressed in the letter (clearances,
availability, job ID, multi-country language mapping)
- domain-transfer argument leads the CV profile statement for
domain-changers
- evidence links (href) on every verifiable named artifact
framework_version: 05 -> 1.2.0.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Both found by empirically re-running the /apply pipeline end-to-end:
06's Document Structure block still demonstrated itemize wrapped inside
lettercontent - the exact antipattern its own pitfall section forbids -
and following 05's needspace guidance at section level pushed an entire
Education block to a new page, costing a page instead of saving one.
framework_version: 05 -> 1.1.1, 06 -> 1.0.1.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The CV was hardcoded 'Always in English' - fine for the Danish/English
demonstration profile, a real disadvantage for fork users in markets
where applications are expected in the local language (cover letters
already match the posting's language). /setup now asks once and records
'CV language:' in CLAUDE.md's Identity section; /apply reads it with
English as the default, so existing users see zero behavior change.
The ATS keyword rule is reworded language-neutrally.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Completes the loop behind #177: /apply drift could be archived by
/outcome, then laundered into 05-cv-templates.md as a reusable template
by /setup Path A - promoting a one-off drifted claim into source
material for every future application. Path A now verifies extracted
statements' factual claims against 01-candidate-profile.md (keeping
framing only), and 05-cv-templates.md marks [Used for:] statements as
phrasing references, never fact sources. framework_version 1.1.0.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Extend the offline CLI contract tests to the two remaining Danish
portal CLIs. Both implement the documented error contract (JSON
errors on stderr, exit 1) but had no test locking it in:
- jobindex-search: search without --query, detail without an ID,
and bunli numeric-option validation (--page not-a-number)
- jobnet-search: detail without an ID, occupations without
--search-string, suggestions without --query, and numeric-option
validation
All asserted paths exit before any network request, matching the
no-live-portal-requests CI policy. Assertions were written against
observed CLI output, not assumed shapes.
Prompt-injection hardening from the dataflow analysis in #173 by @Defaultuser361: data-not-instructions rules in /apply and /rank, reviewer research constrained to the user-confirmed company identity, writing-style verify rule tightened to independently located sources (framework_version 1.1.0), SECURITY.md private reporting channel, README note. Closes#173.
CVs from /apply were named cv/main_<company>.tex, so a second role at the same company overwrote the first (cover letters already carried the role). Aligns CV naming to main_<company>_<role>.tex across apply, add-template, the CV template guide, CLAUDE.md, and SETUP.md; /outcome and /interview fallbacks glob main_<company>*.tex to match both legacy and new names. framework_version bumped on both touched framework files.
Adds /notion-sync per the conditions agreed in discussion #166: tool-agnostic sync contract with Notion as the in-tree reference binding, silently optional (covers unconfigured, headless, and unauthenticated states), read-only toward the repo with the gitignored sync-state file as its only local write, write-once page bodies, documents sync as filenames only. Complements /html-report: deep local dashboard vs glanceable anywhere-view.
Three personalized-fork PRs (#155, #162, #165) were filed against
upstream by accident in one week - GitHub points new fork PRs at the
upstream repo by default, and nothing warned about it at the moment of
filing. Adds a PR template with the heads-up in the compose box (plus
the review norms the process asks for anyway) and one sentence in
CONTRIBUTING's fork section naming the mechanism.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Self-contained HTML dashboard generated from job_search_tracker.csv and the application archives: stat cards, status/sector/channel/funnel charts as hand-generated inline SVG (no CDN, fully offline), HTML-escaped interpolation throughout, and a filterable applications table. Includes Python guards for the command file and the reports/ gitignore rule.
Writes down the architecture decision from the #78 discussion (2026-07-15):
Claude Code is the reference runtime; other agent runtimes are supported at
the edges via the portable portal skills in .agents/skills/, the root
AGENTS.md signpost, and thin-pointer community forks. Per-runtime command
trees stay in forks for the same reason market portals do.
README gets one line in Prerequisites; CONTRIBUTING gets the policy section
beside the market-skills rule it mirrors, including the explicit revisit
conditions.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Root AGENTS.md pointer file per the architecture decision in discussion #78: documents both config roots (.agents/skills/ portable portal skills, .claude/ orchestration) and the profile entry points, carries a framework_version marker registered in both version tools.
Design case made by @erikpr1994 in the #78 architecture thread; implementation by @jovin-nicholas.
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.
validate_data() accepted category values that are not {count?, index?}
objects. They slipped through to format_entry(), which then raised
AttributeError on a normal table lookup (or serialized a malformed shape
under --json). It also accepted duplicate company names silently.
- collect_validation_issues() now also flags a non-object category value
(and non-numeric count / non number-or-string index) as a hard error,
and duplicate company names as a warning.
- validate_data() keeps its eager-fail behavior (same messages), so
existing tests and load_data() are unchanged.
- --validate runs the checks standalone and prints an actionable report
(exit 1 on errors, 0 on warnings-only/clean), letting users pre-flight
their BYO salary_data.json.
Reproduced on master: validate_data({'companies':[{'company':'Acme',
'categories':{'eng':'not_a_dict'}}]}) returns without error, but
format_entry then raises AttributeError.
Co-authored-by: Tunic Assistant <assistant@tunic.local>
A failed iteration in the Quick start install loops (e.g. bun missing
from PATH) skipped the cd back to the repo root, so every remaining
tool's cd failed in cascade and the shell ended up stranded inside
.agents/skills/<tool>/cli with nothing else installed. Run each bash
iteration in a subshell and use Push-Location/Pop-Location in
PowerShell so a failure stays contained to its own tool and the loop
always returns to the repo root.
Claude-Session: https://claude.ai/code/session_015EQ2xeixvVdnvbihce3aSt
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
parse_sheet treated every column that was not company/city as a salary category, with no check that the column actually held numeric salary data. This turned free-text columns (e.g. Notes) into bogus string categories and numeric identifier columns (e.g. Id) into mistaken salary indexes.
- Drop identifier headers (ID_PATTERNS = {id, personnummer}) at classification time.
- Skip non-numeric standalone values and fully-null count/index pairs at row-processing time.
- Adds regression tests (skips_free_text_column, skips_numeric_identifier_column, keeps_numeric_salary_column) that fail on master and pass after the fix.
convert_salary_excel.py detected the company column via exact membership
in COMPANY_PATTERNS, so common real-world headers like "Company Name" or
"Employer Name" were never matched. parse_sheet then returned [] for that
sheet, silently dropping it from salary_data.json (or exiting with no
output for a single-sheet file).
Route company-column detection through the existing header_matches()
token matcher (already used for count/index detection). This only adds
detections; inputs that already worked (bare "Company"/"Firma"/...) are
unaffected.
Adds a regression test in tests/test_convert_salary_excel.py that fails
on master (returns []) and passes after the fix.
* Add framework_version markers to assistant skills and implement CI version guard and update checker
* Address review feedback: update ci.yml based on latest upstream, gate CI version guard to upstream repo, and remove non-ASCII characters from check_upstream_updates.py
Jobbank and Jobdanmark each had only one narrow test, leaving required-argument errors, RSS normalization, JSON-LD variants, and malformed-page handling unprotected.
Add network-free fixture and subprocess tests for repeated RSS filters, description and ID parsing, stderr JSON errors, Bunli numeric validation, JSON-LD objects and arrays, optional fields, not-found pages, and parse failures.
The suites now cover eight Jobbank cases and six Jobdanmark cases without making live portal requests.
Valid JSON such as [] or {"permissions": []} caused lint_skills.py to raise AttributeError because it assumed both values were objects.
Validate the top-level settings value and permissions object before reading nested keys. Malformed settings now produce clear lint errors and exit 1 without a traceback.
Add subprocess regression tests covering invalid JSON, malformed root values, invalid permissions values, and non-list permissions.allow values.
* feat(brand): tagline under the H1 and the hired-moment coffee line
Two items from the branding strategy: the positioning line lands in the
README itself (the repo description already carries it), and /outcome's
hired path gets its single, value-framed donation ask - once per hire,
never nagging, never effort-framed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(brand): pay-it-forward framing for the hired-moment line
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The regeneration pipeline, AI source sheets, retired sprites, avatar,
social card, and internal design/plan docs are maintainer tooling, not
template content - archived in the maintainer's private workspace. Fork
users get the 30 KB animation and nothing they didn't ask for. The
.gitignore PNG allowlist is dropped along with the PNGs it served.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>