Commit Graph
93 Commits
Author SHA1 Message Date
Jakob Stender Guldberg 4c38f7ce4c fix(security): move the interview protection note to the rule that provides it (#337)
The two-line comment above `documents/interview/**` says interview prep and
experience records live there. Nothing has ever written to that directory:
/interview saves its pack to
documents/applications/<company>_<role>/interview_prep_<stage>.md, covered by
the documents/applications/** rule. `git grep documents/interview` returns
only the two declarations of the rule itself (.gitignore and
REQUIRED_IGNORE_RULES), `git log --all -- 'documents/interview*'` is empty,
and documents/README.md documents the applications path outright.

Nothing leaks - the comment is the defect, and it is the misleading kind. It
is the one dedicated, well-argued line about interview material in the
personal-data block, so an auditor checking that the framework's most
sensitive artifact is covered reads it and stops, at the only path in the
block with no writer.

The comment's description of what needs protecting was always right; only its
location was wrong. It now sits above documents/applications/**, the rule that
actually provides that protection, so a reader auditing the block finds the
reasoning attached to the rule doing the work. documents/interview/** stays -
REQUIRED_IGNORE_RULES pins it, so dropping it from .gitignore alone turns CI
red, and it is harmless defence in depth - relabelled in both files as
belt-and-braces rather than the primary guard.

The new check-ignore case in GitignorePatternBehaviorTests derives the
prep-pack path from /interview's own spec instead of hardcoding it. That
distinction is the whole value of the test: a hardcoded path pins only that
documents/applications/** still matches that shape, which security_guards.py
already catches first, and stays green if /interview moves its output -
leaving the corrected comment stale exactly the way this issue found it.
Since #329 the spec states the location in two pieces - Step 1 derives the
archive folder, Step 3 names interview_prep_<stage>.md - so the test pins both
fragments separately and composes the concrete path from them. Mutation-
verified on each half: repointing the folder at documents/prep_packs/, and
renaming the file, both fail this test while `python3 tools/security_guards.py`
still reports OK.

The class's temp-repo setup moved to setUp for the second case.

ayobamiseun reviewed the pre-rebase branch and called all three rebase hazards
in advance: the split literal, the released CHANGELOG context, and the setUp
re-merge. Reached independently here during the rebase; the review was posted
first.
2026-08-31 17:49:22 +02:00
Mads LorentzenandClaude Fable 5 42ba4b475a docs(changelog): record the bun-run permission narrowing (#396)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 20:27:48 +02:00
Sandun Wijerathne ea2f25b39c fix(scrape): persist each posting's publication date in seen_jobs.json (#390) (#391)
* fix(scrape): persist each posting's publication date in seen_jobs.json (#390)

Step 2's contract guarantees a `date` on every portal CLI's search output and
CI enforces it in test_scrape_contract.py; Step 3 uses that date to scope a run
to the last 14 days. Step 4's storage schema then dropped it, so a posting's age
was unrecoverable the moment the run ended - `first_seen` records when the
scraper saw an entry, not when the employer posted it. /rank reads the stored
entry rather than the run, so it had no age signal to weigh.

A freehire-search posting dated 2024-05-13 was scraped 27 months later and
ranked Strong Fit at position 1 of 133. The scoring note observed the listing
"may be long stale" in prose nothing reads, and an /apply run drafted a tailored
CV and cover letter against it.

The schema gains `posted_date` (null when the portal returned no date, never
inferred or backfilled), documented alongside `deadline` with the same
never-backfill rule. Three new cases, each verified to fail on the unfixed spec.

Closes #390

* fix(scrape): correct the 14-day scoping cross-reference, restore EOF newline

Review follow-up on #391.

The 14-day scoping is Step 1b's list item 3, not Step 3 - Step 3 is Quick Fit
Assessment and never touches dates. The "3." list item had been promoted to a
step number. Corrected in the new SKILL.md paragraph (both occurrences), the
CHANGELOG entry, and the test class docstring; a wrong pointer in a file agents
execute as instructions actively misleads.

Also restores the trailing newline on tests/test_scrape_contract.py (the nit
left for a future touch in #344) and adds the (#390) ref to the CHANGELOG entry
to match its siblings.
2026-08-30 20:26:37 +02:00
Mads LorentzenandClaude Fable 5 93fb0e6c47 chore(release): cut v1.7.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 11:20:02 +02:00
Ayobami AdegokeandNavakanth Reddy Dumpa 3d296448bd feat(linkedin-search): report closed postings via isActive, wire into /scrape (adopts #280) (#383)
* feat(linkedin-search): add active status verification for job postings

* fix(linkedin-search): scope closed-posting detection to the top card, pin with tests (#280)

The first version matched five markers against the whole document, so
recruiter boilerplate quoting 'no longer accepting applications' in a
description flagged a live job CLOSED. Detection now stops where the
description markup begins and matches only the two markers real closed
pages carry (closed-job__flavor and the banner text, verified against
live guest pages); the three speculative phrases are dropped. Four new
fixture tests pin both directions plus the two description false-positive
cases - the false-positive pair fails on the unscoped version.

* feat(scrape): mark closed-at-source LinkedIn postings expired, never drop (#280)

/scrape Step 2 now consumes linkedin-search detail's isActive: a job whose
posting page renders the closed banner is written to seen_jobs.json with
status expired rather than silently dropped, per the /rank marking pattern -
the fix for the ghost-jobs class in #331. isActive: true is documented as
absence of the banner, not proof the posting is open.

---------

Co-authored-by: Navakanth Reddy Dumpa <navkanthr@gmail.com>
2026-08-29 11:12:26 +02:00
Ayobami Adegoke 730dcfb079 fix(setup): stop fork clones from filing issues on the upstream repo by default (#389) (#392)
gh repo fork --clone - SETUP.md's own fork command - sets the upstream
repo as gh's default repository, which gh uses for creating issues and
PRs. A user's own automation running gh issue create from a fork clone
therefore published personal job-search data on the upstream public
tracker. SETUP.md section 2 now includes gh repo set-default in the fork
commands with a point-of-decision warning, and .github/ISSUE_TEMPLATE/
carries the same heads-up the PR template already had for the web path.
Blank issues stay enabled.
2026-08-29 11:10:57 +02:00
Ayobami Adegoke 79cd383e58 fix(freehire-search): reject fractional numeric flags instead of silently truncating (#373) (#374)
parseIntFlag used bare parseInt, so --jobage 0.5 truncated to 0, failed
the jobage > 0 guard in search.ts, and posted_within_days was silently
omitted from the outbound request while the CLI exited 0. Numeric flags
now accept whole numbers >= 1 only, mirroring the Danish CLIs'
z.coerce.number().int().min(1) contract, and reject everything else with
the stderr-JSON BAD_ARG error. Five new validation cases, each verified
to fail on the unfixed code.
2026-08-27 19:04:59 +02:00
sdrarunvarshan dea8140db2 feat(ats): extract PDF text with pypdf before Poppler (#369)
* feat(ats): extract PDF text with pypdf before Poppler

Lead the ATS text-layer check with pypdf (BSD, optional pip install). Fall back to pdftotext -layout -enc UTF-8. No cache directory, no installer, no AGPL pymupdf. Windows users without Poppler still get a mechanical parseability check; visual review remains the last resort.

* Update verify_pdf.py

* Update apply.md

* Update verify_pdf.py

* Update verify_pdf.py
2026-08-26 20:07:03 +02:00
Mads LorentzenandClaude Fable 5 d1504d2388 docs(changelog): record the Python 3.10-3.14 CI matrix (#370)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 16:33:30 +02:00
Prince chukwuemekaandCursor d82df2fe51 fix(reset): clear the two personalized skill files /reset profile missed (#364) (#365)
/setup Step 3 populates six skill files; /reset profile cleared four.
04-job-evaluation.md was listed by name under "files NOT touched (they
contain framework rules, not candidate data)" while Step 3.4 writes the
user's match areas, career goals, energizing/draining tasks, financial
situation and schedule constraints into it - and ci.yml's
placeholder-integrity job already guards that file under "personal data
may have been committed". job-scraper/search-queries.md, which Step 3.8
fills with their job boards, role titles, domain keywords, city and
commute tiers, appeared nowhere in reset.md at all.

Both are tracked and unignored, so Step 1 asked the user to confirm a
wipe list that omitted them and Step 4 then reported a blank profile
while /rank kept scoring against the old skills and career goals and
/scrape kept running the old city and queries. Re-running /setup does
not necessarily clean them either: Path A skips files whose content is
"no longer placeholder text", and Step 3.8 is phrased as token
replacement, with no tokens left to replace.

Both files are now previewed and cleared, restoring their /setup
placeholders while preserving the scoring framework and the query
structure. 04-job-evaluation.md leaves the preserved list, which keeps
03-writing-style.md and 06-cover-letter-templates.md - the latter
correctly, since its [YOUR_NAME] tokens are LaTeX scaffolding Step 3
never writes to. CLAUDE.md and cv/main_example.tex stay outside the
profile scope, which reset.md:13 defines as skill files only; the
preview and Step 4 now say they still hold personal data instead of
implying a full wipe.

tests/test_reset_command.py gains a profile-scope guard beside its
documents-scope one, deriving the file list from /setup Step 3's own
headings rather than hardcoding it, so a future /setup target that
/reset forgets fails in CI. Against master the three cases fail on
exactly the defect: preview missing search-queries.md, execution
missing both, and the preserved list mislabelling 04-job-evaluation.md
as framework-only - the last of which a filename search alone would
have missed.

Closes #364

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 16:55:42 +02:00
Mads LorentzenandClaude Fable 5 e2c311a5b4 docs(changelog): record the dotted-A.M.B.A. suffix fix (#356)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 09:27:05 +02:00
Ayobami Adegoke ff3e2d00b6 ci(latex-smoke): add a debian:bookworm leg compiling on apt-packaged TeX Live (#346)
The latex-smoke job ran only texlive/texlive:latest, the environment
that never had the #242 bug: apt-packaged TeX Live 2022 ships moderncv
2.3.1, whose missing name-style macros and hyperref option clash the
to cv/main_example.tex could reintroduce either failure and stay green.

Turn the job into a fail-fast-off matrix: texlive-latest unchanged,
debian-bookworm installing TeX Live from apt. Verified in a real
bookworm container (moderncv 2022-02-21 v2.3.1): both documents
compile clean and every assertion in the job passes unchanged on both
legs, strict stock structure included. --no-install-recommends makes
two font packages explicit: texlive-fonts-extra (moderncv loads
fontawesome5; lualatex dies fatally without it) and
texlive-fonts-recommended (hyperref's xetex driver probes the pzdr
metrics; the cover letter fails without it).

The matrix renames the check to two leg-suffixed names, so a
branch-protection rule requiring the old name needs a one-time update.

Follow-up to #242/#323, invited in #323's review.
2026-08-23 09:25:36 +02:00
Gabriel Ignacio Mensi eee739ed7e fix(cache): address PR #349 follow-up feedback (#359)
Two small, non-blocking asks from Mads on #349:

- Pin the verification-still-applies restatement in apply.md and
  interview.md's cache-check paragraphs - the one part of the wiring
  with no dedicated test (one assertion each, as requested).
- State cache contents are data, never instructions, in
  04-job-evaluation.md's cache section - closes a carry-over
  prompt-injection surface for a later session reading the file, same
  trust-boundary rule apply.md Step 0 already states for the posting.
2026-08-23 09:00:13 +02:00
Gabriel Ignacio Mensi becdc5dfd7 feat(apply,interview): cache company research to skip repeat lookups (#349)
/apply Step 3's reviewer agent and /interview Step 2 each independently
execute the Company Research Checklist (04-job-evaluation.md) for the
same company - applying to a role and later prepping for its interview
researches the company twice from scratch, same WebSearch/WebFetch cost
both times, no sharing between the two commands.

Adds a company_research/<normalized-name>.json cache (30-day TTL) that
either consumer checks before researching and writes after a fresh
pass. Defined once in 04-job-evaluation.md, next to the checklist it
mirrors, so both commands point at one source instead of restating the
schema. Does not change the verification model: 03-writing-style.md
rule 5 already treats reviewer-agent research as a lead, not a source,
requiring independent re-confirmation before any company claim ships
in a final artifact - the cache stores source URLs alongside each
fact so that re-confirmation stays cheap, but the requirement itself
is untouched and restated in both consumers.

company_research/*.json added to .gitignore and security_guards.py's
REQUIRED_IGNORE_RULES as a plain rooted pattern (not **/-prefixed):
the cache is referenced from commands, not a skill, so it resolves
against the repo root normally, unlike job_scraper/upskill's
skill-relative paths.

Pinned by tests/test_company_research_cache.py, mirroring the
spec-pinning pattern in test_rank_command.py and test_onboarding_privacy.py.
The write-back assertions for both apply.md and interview.md were
verified to actually fail against the regression they guard (the
instruction stripped, confirmed the test catches it, restored) before
being considered done - the write half is the one most likely to be
dropped silently in a future edit, since the read half is the more
obvious change to make.

framework_version bumped 1.2.4 -> 1.2.5 in 04-job-evaluation.md, the
only touched file inside the tracked skill set.
2026-08-22 11:21:34 +02:00
Mads LorentzenandClaude Opus 5 ab91c60cc4 chore(release): cut v1.6.0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:48:32 +02:00
Mads LorentzenandClaude Opus 5 34b8b3f91f fix(onboarding): warn about public forks at the point of decision (#345) (#348)
The quick start walked a new user into gh repo fork - forks of public
repos are always public - and two steps later had /setup write personal
data into tracked files, with the only complete warning in SETUP.md
section 8, a section about pulling updates that a first-time user has no
reason to open during onboarding. A real user hit exactly this (#345).

The warning now sits adjacent to both fork commands (README step 1,
SETUP.md section 2, both pointing at section 8's private-remote recipe),
and /setup checks the origin's visibility BEFORE writing anything: a
public-fork origin gets a confirm-first warning instead of a note after
every file is on disk. A private origin, no origin, or a non-git
directory continues silently. Reported by @basilevs with a complete
reproduction and fix analysis; this implements his fixes (1) and (4).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:33:32 +02:00
Mads LorentzenandClaude Opus 5 ab5d23bad4 docs(changelog): record the cross-portal scrape-contract pin (#344)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:24:33 +02:00
Mads LorentzenandClaude Opus 5 dd02c82485 feat(freehire-search): add --no-description for cheap discovery passes
A default search hydrates full bodies - ~73% of the payload, ~20k tokens
per query fed into agent context - while /scrape's own Step 2 says to
pre-filter by title before reading bodies. The flag keeps every other
field and drops the bodies (live 10-result search: ~58k -> ~10k chars);
hydration stays the default per the documented trade-off. The API
returns bodies regardless of include_description=false (verified live),
so the lean guarantee is enforced client-side. Review opportunity O1
(2026-08-19), approved as an enhancement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:05:54 +02:00
Mads LorentzenandClaude Opus 5 c85640e30a fix(jobindex-search): rewrite detail parser against current page shapes
Every selector the old parser used (job-text, jix-info,
jix_robotjob--area, jix-toolbar-top__company) is gone from live pages:
detail returned CSS-comment text as the deadline, the external ATS URL
as its own id/url, null company/location/date, and a teaser description
- exit 0, on 4/5 live postings. The rewrite recognises both current
shapes (jobindex-native jd-* layout; external-ATS passthrough), always
keeps the jobindex id and jobannonce URL, anchors the deadline to a real
date in visible markup only (the label also lives in a CSS comment,
which is what the old regex captured), converts Danish long dates to
ISO, decodes Danish named entities, and returns company: null on
passthrough pages instead of the ATS brand. Live: 5/5 postings now
yield full descriptions and correct fields. Review finding F14
(2026-08-19).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:02:29 +02:00
Mads LorentzenandClaude Opus 5 9a69309749 fix(scrape): add a client-side recency fallback for flagless portals
Step 1b.3's "scope to 14 days using the portal's recency flag" was
unsatisfiable on jobdanmark, which has no date filter or sort - the
agent either silently skipped the scoping or invented a flag, and the
CLIs now reject invented flags loudly. Every portal emits date, so the
instruction now filters client-side after the call, and stops
presenting --order (a sort) as interchangeable with a filter. Review
finding F32 (2026-08-19).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:56:15 +02:00
Mads LorentzenandClaude Opus 5 2c3d2d8558 fix(html-report): funnel from stage history, rejection rate from true rejections
The funnel was computed from current status - a state, not a history -
so an application that interviewed and was later rejected never counted
as reaching Interview, and a hired candidate produced Interview=0. The
stage checkboxes Step 1.2 already merges from outcome.md are the
history; Step 2 and chart 4 now use them. The rejection rate also
counted offer_declined (a success) and withdrawn (candidate-initiated)
as rejections and left unresolved Interview/Offer rows in the
denominator. Review findings F10 and F11 (2026-08-19).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:54:46 +02:00
Mads LorentzenandClaude Opus 5 dab215073e fix(jobdanmark-search): normalize the detail command's HTML fallback branch
The JSON-LD and rendered-HTML branches returned structurally different
records: the fallback emitted raw DD-MM-YYYY page text as datePosted,
free text (including the literal "Loebende") as validThrough, and a
hardcoded null addressLocality. Overview dates now convert to ISO,
Loebende maps to null, and the locality derives from the workplace
address via the same exported extractCity search uses. Review finding
F25 (2026-08-19).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:53:24 +02:00
Mads LorentzenandClaude Opus 5 bcba687fbf fix(jobnet-search): map the 1900-01-01 deadline sentinel to null in detail
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>
2026-08-19 20:50:55 +02:00
Mads LorentzenandClaude Opus 5 07cec1f227 fix(ci): co-locate placeholder sentinels with the data they guard
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>
2026-08-19 20:47:51 +02:00
Mads LorentzenandClaude Opus 5 3bfd525cc4 fix(portals)!: reject unknown flags in all six CLIs
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>
2026-08-19 20:44:26 +02:00
Mads LorentzenandClaude Opus 5 d4e0c64c3c fix(rank): rename the location verdict field to location_verdict
"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>
2026-08-19 20:36:49 +02:00
Mads LorentzenandClaude Opus 5 a306912133 fix(linkedin-search): drop the never-delivered applyUrl detail field
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>
2026-08-19 20:34:35 +02:00
Mads LorentzenandClaude Opus 5 56f679e5c0 fix(jobdanmark-search): drop presentation-only keys from search output
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>
2026-08-19 20:33:16 +02:00
Mads LorentzenandClaude Opus 5 48965960d3 fix(jobbank-search)!: emit deadline as YYYY-MM-DD in search output
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>
2026-08-19 20:31:20 +02:00
Mads LorentzenandClaude Opus 5 b067928da7 fix(jobindex-search): map ASAP deadlines to null per the /scrape contract
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>
2026-08-19 20:29:40 +02:00
Mads LorentzenandClaude Opus 5 2edf8c41f1 test(portals): cover linkedin card date/location and jobindex parseSearchPage
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>
2026-08-19 20:03:00 +02:00
Mads LorentzenandClaude Opus 5 65fbe8b8a4 test(framework-version): cover the CI gate that had zero tests
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>
2026-08-19 20:00:25 +02:00
Mads LorentzenandClaude Opus 5 9a074b262d test(lint-skills): cover check_skill and check_command, not just settings
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>
2026-08-19 19:59:22 +02:00
Mads LorentzenandClaude Opus 5 c20458d768 test(robots-check): pin the tie-break clause and the browser-UA fallback
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>
2026-08-19 19:57:46 +02:00
Mads LorentzenandClaude Opus 5 4ed5fee221 fix(gmail-sync): replace in:inbox with -in:sent -in:drafts
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>
2026-08-19 19:55:48 +02:00
Mads LorentzenandClaude Opus 5 0e054f16e7 fix(upskill): give Step 3.3 a rule for blank fit_rating rows
/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>
2026-08-19 19:54:32 +02:00
Mads LorentzenandClaude Opus 5 57e82d2b59 fix(rank): treat non-ISO stored deadlines as absent in urgency and the sweep
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>
2026-08-19 19:53:29 +02:00
Mads LorentzenandClaude Opus 5 9ab697de64 fix(evaluation): update stale Language Gate preamble to reflect tracking
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>
2026-08-19 19:52:21 +02:00
Mads LorentzenandClaude Opus 5 2036b97040 fix(reset): include documents/postings/ in the documents scope
/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>
2026-08-19 19:50:11 +02:00
Mads LorentzenandClaude Opus 5 1c19f6c45f fix(salary-tools): decide number locale by last separator, pair compound headers
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>
2026-08-19 19:48:50 +02:00
Mads LorentzenandClaude Opus 5 7aba0b4a9d fix(jobdanmark-search): accept a comma after the postcode in location extraction
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>
2026-08-19 19:46:34 +02:00
Mads LorentzenandClaude Opus 5 b2545d5121 fix(latex): brace bracket-leading bullets, document escapes, pin pdftotext encoding
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>
2026-08-19 19:44:36 +02:00
Mads LorentzenandClaude Fable 5 40022dd8b9 docs(changelog): record the jobbank-search date-field fix (#342)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 08:34:22 +02:00
Oscar Madera 17bc698697 fix(jobdanmark-search): emit the /scrape contract fields in search output (#340)
Adds additive normalization so jobdanmark search output carries the cross-portal contract fields: company from companyName, location as the city after the postal code in companyAddress (null-safe - a missing or null address yields null instead of crashing the search), and date/deadline converted from DD-MM-YYYY to YYYY-MM-DD with safe passthrough on unexpected formats. Native fields unchanged.

Co-authored-by: oscarbol09 <80536682+oscarbol09@users.noreply.github.com>
2026-08-19 08:32:06 +02:00
Oscar Madera 04186b9a37 fix(jobnet-search): emit the /scrape contract fields in search output (#339)
Adds additive normalization so jobnet search output carries the cross-portal contract fields (company, location, date, deadline with the 1900-01-01 NotDisclosed sentinel mapped to null, and url). Emits the public /find-job/{jobAdId} route and corrects the skill's own stale /job/ documentation, which redirects anonymous visitors into the MitID login flow.

Co-authored-by: oscarbol09 <80536682+oscarbol09@users.noreply.github.com>
2026-08-19 08:20:23 +02:00
Yash Rajeshbhai Darji f136b534de feat(scraper): record whether each seen job came from a CLI or the WebSearch fallback (#338)
Adds an additive source field (cli/websearch) to the seen_jobs Step 4 schema, Step 1c tagging at collection time, and a 'fallback (websearch):' Step 5 summary line - so future ghost-job reports (#331) self-triage from stored state. Same additive-field contract as portal/deadline: never backfilled.

Co-authored-by: yshraj <87583119+yshraj@users.noreply.github.com>
2026-08-18 21:06:16 +02:00
Mads LorentzenandClaude Fable 5 0cf2ce0d36 style: restore two blank lines dropped by the #329 rebase (outcome.md Step 1.3, CHANGELOG 1.5.0 heading)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 21:04:53 +02:00
Ayobami Adegokeandayobamiseun 2ff1085254 fix(archive): derive <company>_<role> as a single path component (#329)
Extends the canonical Subfolder-naming rule by citation to all six archive derivation sites (apply, gmail-sync, interview, notion-sync, outcome, assistant SKILL.md), adds a fail-closed guard for an empty derived name, and pins every site with mutation-verified tests. framework_version 1.3.3 -> 1.3.4.

jakob1379 independently specified the same fix in his fork's issue #22 before this PR's rework.

Co-authored-by: ayobamiseun <66267222+ayobamiseun@users.noreply.github.com>
2026-08-18 21:03:16 +02:00
197281t-ship-it 5c423b4064 docs(evaluation): reframe onboarding and matching guidance around function, not title (#330)
Title-lookalike matching collapses a multi-hat career into whichever single
job-title box sounds closest, then searches only inside that box. /setup
Section 9 now asks about the function before collecting search titles,
search-queries.md says to organize priority categories by function with title
variants under each, and 04-job-evaluation.md's Experience dimension matches on
the function and nature of work performed (framework_version 1.2.2 -> 1.2.3).
From discussion #327's field report and calibration example.
2026-08-16 20:04:42 +02:00
Oscar MaderaandJakob Stender Guldberg 762d3218ef fix(html-report): read and render the tracker deadline column (#325)
/html-report was the one tracker consumer #319's deadline column left behind:
Step 1 now parses every canonical column and Step 3 renders Deadline after Date.
The drift guard derives CANONICAL_HEADER from apply.md itself, so a future
column added elsewhere but missing here fails with the column named; legacy
13-field rows read as empty deadline, never dropped, never inferred. Includes
rule-6 sweep refinements and deadline-reconciliation rules authored by
jakob1379.

Co-authored-by: Jakob Stender Guldberg <17257805+jakob1379@users.noreply.github.com>
2026-08-16 20:02:09 +02:00