search guards the API's undisclosed-deadline sentinel and a test pins
it; detail dumped the raw response, so the same field for the same job
behaved two ways, and an undisclosed deadline stored via detail read as
126 years expired - /rank's sweep would retire the job on sight. All
three output formats now flow through prepareDetail. Review finding F33
(2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cv/main_example.tex's sentinel was [YOUR_NAME] - a header comment and
the pdftitle, neither of which /setup's documented personalization
touches, so CI reported the file clean while it carried a real name,
address, phone and email (the review proved this end to end; the file
is the one CV the gitignore deliberately allows to be committed). The
guard now checks the \name{} and \email{} data lines, and 01's sentinel
moves from the <!-- SETUP comment onto [YOUR_EMAIL]. New test simulates
the /setup edit and requires every checked CV sentinel to be destroyed
by it. Review finding F28 (2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Silently discarded flags produced silently wrong results: jobdanmark
with --query (its real flag is --text) returned all 13,862 jobs as if
they matched, exit 0, empty stderr - indistinguishable from a real
result set. The four bunli CLIs get an argv preflight built from each
command's own options object; linkedin and freehire validate parsed
flags against per-command known sets. help/version still pass, and
add-portal.md's existing bogus-flag-exits-1 contract now holds for the
reference implementations contributors copy. One linkedin pin updated:
"--jobage-minutes -5" now fails as UNKNOWN_FLAG (the stray -5 token)
rather than BAD_ARG - same loud-failure invariant, earlier gate. Review
finding F13 (2026-08-19), decision approved by Mads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"location" meant a place in scraper output and a PASS/FAIL/FLAG verdict
in /rank's persistence - one key, two meanings, in the same store, with
ranking able to overwrite the commute-filter place with "PASS". The
verdict now lives in location_verdict; legacy entries are read
compatibly and migrated on re-write. Also completes the seen_jobs schema
enumeration (F27 Part A): the do-not-drop instruction now names
location_verdict/language_gate/language_note. Review finding F27
(2026-08-19), decision approved by Mads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The regex required class= before href= within one tag; LinkedIn's real
markup puts href first, so applyUrl was null on every live posting while
SKILL.md claimed the command returns an apply link. Fixing the regex
would only yield the job-view URL - a duplicate of url - so the field is
removed rather than repaired, and a test pins the removal. Review
finding F19 (2026-08-19), decision approved by Mads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
coverImage, companyLogo, companyLogoSvgMarkup, overlayColor, and
silhouetteLogo were ~40% of a live payload - image keys, focal points
and overlay colours an agent can never act on, paid into context on
every /scrape query. A live 30-result response drops from ~30k to ~20k
chars. The #340 compatibility duplicates and slug (the detail command's
input) are kept deliberately. Review finding F3 (2026-08-19), decision
approved by Mads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The feed's DD.MM.YYYY parenthetical passed through raw - documented, but
contradicting the /scrape contract, every other portal, and this CLI's
own detail command for the same job, and ambiguous to a date parser
(01.09.2026: 1 Sep or 9 Jan). The known shape converts to ISO; løbende
still maps to null; unrecognized shapes pass through for /rank's
defensive handling. Breaking for anything parsing the old format - the
README's own search example already showed ISO. Review finding F5
(2026-08-19), decision approved by Mads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
apply_deadline_asap was emitted as the string "ASAP" on ~half of live
results - undocumented, contradicting the CLI's own README, and breaking
every consumer that does date arithmetic on the field (rank's sweep,
outcome's deadline check, notion-sync's typed date column). ASAP means
"no stated deadline", which the schema already defines null to mean.
The flag wins over any date field that happens to be present. Review
finding F12 (2026-08-19), decision approved by Mads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The linkedin fixture was purpose-built for entity decoding and had no
<time> or location element, so removing the date extraction - a /scrape
contract field on a default-ON portal - survived the suite. jobindex's
parseSearchPage (the Stash parser behind every search) had zero tests,
so meta.total silently dropping hitcount survived too. Both mutations
now fail exactly the new tests. The ASAP deadline branch is deliberately
left to the F12 fix, which changes its behaviour to null. Review finding
F35 (2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
check_framework_version.py guards fork-rebase safety (Gate E) and could
be neutralised by a one-line change that reads as a refactor, with
nothing in the repo noticing - a broken guard is silent by construction.
Four new tests run the real script inside an isolated git repo: clean
tree passes, unbumped edit fails, bumped edit passes, missing marker
fails. Mutation-verified against the exact return-False disable the
review demonstrated. Review finding F22 (2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The linter's main job - frontmatter keys, allowed-tools targets, the
command title rule - had zero assertions; deleting the missing-
allowed-tools error left the suite green. The fixture's yaml stub now
parses the flat frontmatter the fixtures write instead of returning a
canned mapping, and four new cases pin both check functions.
Mutation-verified against the real linter. Review finding F23
(2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tie-break test listed Disallow first - the one ordering where
deleting the clause changes nothing - and gate()'s read-the-policy-as-a-
browser recovery (the Barclays-class case 09-web-research.md documents
as covered) had no test. Both gaps are guard code whose breakage is
silent by construction. Mutation-verified: the tie-break deletion and
the UA-loop reduction each now fail exactly the new tests. Review
findings F21 and F30 (2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
in:inbox matches only messages currently in the Inbox, so it silently
excluded archived mail and everything routed past the inbox by a
label-and-archive filter - exactly the mail matched by the job-search
label Step 3.1 hunts for. The stated intent ("skip sent/drafts") is what
the negative operators express. Failure mode was silent under-detection
that read as "no updates" and left the tracker stale. Review finding F18
(2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/outcome-created tracker rows (applications made outside the workflow)
never got a fit evaluation, so fit_rating is blank - and Step 3.3's
weight formula divides by it with no stated rule. Blank read as 0 means
weight 1.0, the maximum: the job the framework knows least about would
dominate the heatmap and the learning plan. Blank now falls back to a
matched ranked entry's rank_score, else skip+count+report once - the
same pattern the skill already applies to missing gaps. Review finding
F29 (2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rule 6's expiry sweep mutates status automatically from stored deadline
values, yet had no rule for the non-ISO shapes portals have shipped into
seen_jobs.json ("ASAP", DD.MM.YYYY, free text) - "ASAP" is incomparable
and "01.09.2026" is ambiguous between 1 Sep and 9 Jan. /outcome, which
merely displays dates, already carried the defensive-parse rule. A
non-YYYY-MM-DD stored value is now handled like an absent one (left
alone, never compared, never guessed at) and reported once with its
portal. Includes the F24-style coupling test. Review finding F17
(2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
04-job-evaluation.md still said the gate result "is not a field /scrape
or /rank track" - true when the gate was introduced, false since /rank
began persisting language_gate/language_note as shortlist veto fields
and /scrape began surfacing the flag. The authoritative framework file
taught agents the opposite of rank.md's own persistence rule. New
coupling test pins that the section names the tracked fields and never
reverts to the untracked claim. framework_version 1.2.3 -> 1.2.4.
Review finding F24 (2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/reset's preview, delete block, and scope description all skipped
documents/postings/ - the drop folder for hand-pasted posting text,
documented in documents/README.md and protected as personal data by
security_guards.py - and then asserted "The documents/ folder is now
empty." The new test derives the folder list from the git tree, so any
future drop folder fails it until /reset covers it. Review finding F26
(2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review findings F7 and F8 (2026-08-19):
- parse_numeric_cell's both-separators branch always assumed European
locale, silently turning a US "1,234.56" into 1.23456 - a 1000x
corruption written to salary_data.json with no warning. The separator
that appears last is now treated as the decimal separator, which also
makes multi-group values ("1,234,567.89") parse instead of raising a
raw float error. Single-separator ambiguity guards are unchanged.
- strip_type_patterns stripped only whole tokens, so the compound header
"Lønindeks alle" kept its type word and could never pair with "Antal
alle" - failing exactly for the compound-word locale COMPOUND_PATTERNS
exists to support. It now also strips compound patterns as substrings,
mirroring header_matches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The location regex required whitespace after the 4-digit postcode, but
live companyAddress values frequently read "2670, Greve" - those results
emitted location: null (7/30 in the review's live sample; 1/30 after this
fix), leaving /scrape's geography filter nothing to act on. Extraction is
now a helper with a comma fallback that requires a non-digit city start,
so a 4-digit street number never wins over the real postcode, and the
captured city is trimmed. Review finding F2 (2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three findings from the 2026-08-19 review (F9, F31, F34):
- F9: every placeholder bullet written as \item [text] let LaTeX parse
the bracketed text as the item's optional label, rendering it clipped
off the left page edge and absent from the PDF text layer ("Achievement"
appeared 9 times in cv/main_example.tex and 0 times in the extraction,
with a clean compile and green CI). Bullets are now braced as
\item {[text]} in the example CV and in the template
06-cover-letter-templates.md teaches, and CI's stock PDF assertions
additionally require "Achievement" to survive pdftotext.
- F31: 05-cv-templates.md gains a "LaTeX Special Characters" section and
06's is completed beyond \_ and \&. The load-bearing case is an
unescaped % in a quantified achievement bullet: it starts a LaTeX
comment and silently deletes the rest of the line from the PDF.
- F34: the documented ATS extraction commands (apply.md,
05-cv-templates.md, CLAUDE.md) now carry -enc UTF-8. Xpdf-based
pdftotext builds default to Latin-1 output, so a correct non-ASCII CV
failed the replacement-character parseability check.
framework_version: 05-cv-templates.md 1.4.1 -> 1.4.2,
06-cover-letter-templates.md 1.0.1 -> 1.0.2. All three pinned by the new
tests/test_latex_guidance.py (9 tests; suite now 261).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The #323 entry described the renewcommand as 'a true no-op on moderncv 2.4+';
head iii's \firstnamestyle/\lastnamestyle route through \namefont, so the
override is what produces the 34pt name on 2.4+ as well. Verified by macro
expansion at review time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An explicit maintainer invitation to a named contributor reserves the
implementation for them (default seven days, longer on request); duplicates
filed inside the window close in the invitee's favor. Prospective policy,
prompted by the second race on invited work in two weeks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The contract's Fetching line still said "browser User-Agent", predating the
honest-UA posture (#283/#277). It now names the shipped convention and routes
browser-header escalation through the 09-web-research robots gate.
Also moves #304's CHANGELOG entry out of the released 1.4.0 section - the
branch predated the release cut, so the three-way merge landed it there -
into [Unreleased]/Changed where it belongs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The last portal CLI still sending a full Chrome spoof after #283 and
4551346. Live-verified: search and detail endpoints serve identical
responses to Mozilla/5.0 (compatible; linkedin-search-cli/1.0).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A non-Danish user's /scrape ran all four Danish boards by default,
spending tokens on irrelevant listings. The portals stay in-tree as the
maintainer's demonstration instance, one flag away.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jobbank sent a full Chrome browser string and jobdanmark's detail
command a bare Mozilla/5.0. Both now use the (compatible; <portal>-cli/1.0)
token per the identification posture settled in #277. Verified live:
both portals serve identical responses to the honest token.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The file shipped in #277 but was never added to the manifest, so forks
got no signal when it changed. Surfaced during #282 review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backfills the missing entries for #272 (localized numeric parsing), #273
(case-insensitive upstream slug compare), and #274 (SETUP.md remote-add
line), then cuts the 1.3.0 section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stock template's contact and award lines carry two decorative separator
glyphs that also extract as U+FFFD, so 'grep -c' returns 2, not 0, on a CV
that is fully fixed. Point the check at date lines and name the benign hits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Forks of public GitHub repositories are always public, so SETUP.md
section 8's 'your fork is private working space, so commit them' invited
exactly the personal-data exposure it seemed to rule out - the observed
failure mode behind several real forks that pushed filled-in profiles to
public master. Section 8 now states the fork-is-public fact plainly and
documents the safe alternative (private repo + template as upstream
remote), and /setup's completion summary carries a matching privacy note
at the exact moment profile data first lands in tracked files.
Also backfills the CHANGELOG entry for #265.
Prompted by rasstamann's discussion #266.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two personal-data ignore rules existed in .gitignore but not in
REQUIRED_IGNORE_RULES, so a change weakening either would have passed CI:
cover_letters/Cover_*.* (the uppercase naming variant /apply recognizes)
and cv/*.txt (ATS text extractions of tailored CVs).
Also: regression tests pinning #252's ragged-row bounds fix in
convert_salary_excel.py (mutation-verified), and removal of the vestigial
cover_letters/OpenFonts/cover.cls, which since #252's rename ambiguously
declared the same class as the real cover.cls (zero references; cover
letter re-compiled and page-verified after removal).
Guard-list gap surfaced by CodeRabbit's review on jakob1379's Nix demo
fork PR (jakob1379/ai-job-search#1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The list omitted security_guards.py and the exact unittest invocation;
the one recent contributor CI failure fitting #262's description (#238)
failed on precisely the omitted script. Reported by @jakob1379.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backfills the release span since v1.0.0 (16 commits): the Typst
personal-data gitignore fix and live dependency review under a
Security & privacy heading, plus the added features and fixes that
had no Unreleased entries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prompted by the extension-system question in discussion #249: the three
extension points existed as folklore across #78, /add-portal, and closed
PRs. Now stated plainly, with a read-the-code-first checklist for
borrowing portal skills and the rationale for why there is no installer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two stragglers in SETUP.md still recommended pdflatex even though the
README, CLAUDE.md, and 05-cv-templates.md all correctly say lualatex.
The compile snippet in section 7 used pdflatex, and the troubleshooting
section described the CV compile as "standard LaTeX." Both updated to
match the rest of the documentation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two additions to 05-cv-templates.md so the /apply workflow follows the
same patterns as cv/main_example.tex:
1. Add the three \renewcommand* lines to the documented preamble so
downstream CVs inherit the color1 overrides for firstname, lastname,
and section headings. Includes a short paragraph explaining the
reason and a note for users who prefer regular-weight firstname.
2. Add a "Spacing inside itemize lists" subsection that calls out the
inter-item \vspace anti-pattern with a wrong/right example and
explicitly preserves the two related patterns that DO render fine
(post-section spacer, inter-cventry \vspace{3pt}).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes for the moderncv banking + lualatex + MiKTeX combination that
the README documents as the recommended setup:
1. **Color overrides for names and section headings.** Default banking on
modern MiKTeX renders firstname, lastname, and \section headings in
black, which clashes with the rest of the blue accent scheme (links,
bullets, contact icons). Add three \renewcommand* lines that force
firstnamestyle, lastnamestyle, and sectionstyle to use \color{color1}.
Both names render bold in matching blue, and every \section heading
renders blue with the standard horizontal underline. No effect for
users on a setup where the defaults already render blue.
2. **Remove \vspace{1pt} between \item entries in itemize lists.** This
pattern (used 21 times in the template) intermittently produces an
oversized gap before a single item: every adjacent pair has the same
separator in the source, but LaTeX renders one of them noticeably
wider because the inter-item \vspace creates a paragraph break that
interacts with the list's internal \itemsep. Removing the inter-item
\vspace lets itemize use its native uniform spacing, which renders
consistently. \vspace{1pt} immediately after \section{...} (between
header and first item) and \vspace{3pt} between top-level \cventry
blocks are kept - those are different patterns and render fine.
Also updated the file's compile-comment from "pdflatex" to "lualatex"
to match the README.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original bullet ("Token-efficient. Reviewer receives drafts inline
rather than re-reading files. Verification runs once, at the end.") is
technically true for the reviewer-dispatch path but invites readers to
infer that the overall workflow uses fewer tokens than master. That is
not actually defensible: the new Step 5 (compile-and-inspect) spends
tokens on Read'ing both PDFs and on layout-fix iteration cycles, which
typically offsets the savings from inline draft passing and scoped
reviewer reads. End-to-end the workflow is roughly token-neutral, with
the variance dominated by how many layout fixes the CV needs.
Reframes the bullet as "Token-efficient reviewer dispatch" (which is
accurate and verifiable from the prompt structure) and adds an explicit
note that Step 5 spends some of those savings on PDF rendering. The
trade is real - tokens for fewer broken PDFs reaching the user - and
worth surfacing honestly rather than overclaiming.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings 02-behavioral-profile.md back into the reviewer's reading list (it
was dropped in the earlier token-efficiency refactor). The reviewer now
reads four files: 01 (candidate profile), 02 (behavioral profile), 03
(writing style), 04 (job evaluation). The LaTeX template files (05, 06)
remain excluded, since those govern structural/LaTeX concerns the drafter
already applied.
The behavioral profile is there to let the reviewer cross-check whether
the cover letter's voice matches the candidate's natural register -
something 03-writing-style.md does not encode (03 has the universal rules
like "no em-dashes, no cliches"; 02 has the per-person register like
"Collaborator PI should not be given a combative solo-hero tone").
Also cross-references 02 explicitly in the Part B "Tone and style issues"
category so the reviewer actually uses it during critique rather than
just skimming it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit of master vs the branch found three real coverage gaps in the
reviewer prompt and drafter's revise step. This commit restores them
without giving up the Part A (JSON edits) + Part B (narrative) structure.
1. Action-oriented reframing is restored as a Part B narrative category.
The previous refactor folded reframing into Part A edit rationales, but
structural weakness (e.g. "the whole opening paragraph is passive") does
not fit a clean find-and-replace and needs prose critique to surface.
2. Tone and style phrasing is strengthened from "anything that conflicts
with 03-writing-style.md" back to the master's proactive "check
against... flag issues with tone, formality, or voice", with explicit
examples (cliches, hedging, over-humility, inconsistent register).
3. Step 4 revise guidance is expanded from "Apply Part A... Apply Part B
using judgment" (which could be read as a free-for-all) to an
enumerated sub-list under Part B covering every category the reviewer
returns: missed keywords, company angles, action-oriented reframing,
tone/style. Keeps the architectural change (Part A/B split) but
restores the master's explicit "apply every kind of fix" discipline.
Also adds a small instruction to the reviewer: produce each Part B
category even if the finding is "no issues" - silence on a category can
be mistaken for skipping it.
One remaining judgment call left unchanged in this commit: the reviewer
no longer reads 02-behavioral-profile.md. The refactor excluded it on the
reasoning that 03-writing-style.md carries the voice/tone rules. This is
a conscious trade-off; if voice-matching starts slipping in practice,
restoring 02 to the reviewer's reading list is a one-line fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates README.md and SETUP.md to reflect the new /apply workflow
additions shipped in the previous commit:
- Bumps the /apply step list from 6 to 7, adding "Compile and inspect"
between Revise and Present
- Adds a "What makes this workflow different" subsection highlighting the
PDF verification loop, relevance-weighted CV cutting, drafter-reviewer
separation, and token-efficient dispatching
- Updates prerequisites note to call out lualatex (CV) and xelatex (cover
letter) explicitly, with the reason each engine is required
- Updates SETUP.md's LaTeX section to match (pdflatex -> lualatex for the
CV, with the MiKTeX fontawesome5 caveat)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new Step 5 to the /apply workflow that compiles the CV (lualatex)
and cover letter (xelatex) and visually inspects the resulting PDFs before
presenting them to the user. This catches page-break failures that .tex
review cannot detect: orphaned cventry titles, cover letters spilling to
a second page, and bullet fonts not matching body text.
Also introduces relevance-weighted cutting as the rule for shrinking an
overflowing CV: score each candidate line by (a) relevance to the posting,
(b) uniqueness in the document, and (c) load-bearing role in the cover
letter, then cut the lowest-total-score line first, regardless of section.
This replaces the old static "cut oldest section first" heuristic, which
was wrong whenever a relevant older-role bullet competed with an
irrelevant recent-role bullet.
Bundled with the earlier pending token-efficiency improvements to the
/apply workflow (inline draft passing to the reviewer, scoped reviewer
file reads, single end-of-workflow verification pass).
Changes:
- .claude/commands/apply.md: new Step 5 (Compile & Inspect PDFs), renumber
old Step 5 to Step 6, update token-efficiency rules at top to reference
Step 6 for the verification checklist
- .claude/skills/job-application-assistant/05-cv-templates.md: switch
recommended compile engine from pdflatex to lualatex (pdflatex fails on
modern MiKTeX with fontawesome5 font-expansion errors), add
"Compile-and-Inspect Loop" section with needspace/enlargethispage
patterns, add "Relevance-weighted cutting" section
- .claude/skills/job-application-assistant/06-cover-letter-templates.md:
add compile command, add "Compile-and-Inspect Loop", document the
"\lettercontent{} + itemize" pitfall and the required font-matching
Raleway-Medium wrapper fix
- CLAUDE.md: add "Compiled PDF verification (MANDATORY)" checklist items
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>