mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 08:36:25 +00:00
6f0178a8a1a9b720196a9d209f6c084570ae4cb7
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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> |
||
|
|
0e1a895c4e |
fix(apply): archive the job posting while /apply still holds it (#306) (#307)
/apply drafted two documents and a tracker row from the full posting, then let the text die with the session. /outcome Step 3.2 tried to recover it by re-fetching a `source` URL the spec itself expects to be dead, and a posting pasted from an email or a PDF had no `source` to re-fetch at all. Step 6b gains item 7: write the posting verbatim to documents/applications/<company>_<role>/job_posting.md, never a re-fetch or a reconstruction from memory. The folder is derived by citing /outcome Step 1.4 rather than restating the rule, so the two cannot drift. An existing file is left alone and named in the report. Step 0 and the /scrape path (job-application-assistant SKILL.md Step 1) now retain the full posting text rather than a summary, so item 7 has something verbatim to write. Pinned by tests/test_apply_records_application.py. |
||
|
|
e09d3eb37b |
fix(workflow): define tracker status enum once in /outcome, normalise readers (#299)
* fix(workflow): define tracker status enum once in /outcome, normalise readers (#298) The tracker CSV status column had no single authoritative definition. Six command files restated it with inconsistent spellings, producing two concrete bugs: - /outcome Step 4 wrote o response and offer declined (spaces). /html-report normalised only the underscore forms, so those rows matched no bucket and were silently dropped from the rejection-rate denominator. - /gmail-sync Step 2 hardcoded the final-status set with space spellings, so a row written with underscores was never recognised as final and the sync kept chasing closed applications. - /html-report included interview_only in its tracker bucket map; that value belongs to the archive outcome.md Status: field, not the CSV status column. Fix: add a '## Tracker status vocabulary' block in /outcome (the only CSV writer) defining the canonical underscore spellings once. Every reader now references that block or explicitly lists both spelling forms as read-tolerance for existing trackers. /outcome Step 4 writes no_response and offer_declined. /html-report loses interview_only and gains offer declined as a read-tolerance variant. /notion-sync Step 3 Status select options are aligned to the canonical spellings. Pinned by tests/test_tracker_status_vocab.py (9 new cases following the DraftedMeansDraftedToEveryReader CASES-table pattern). All 205 tests pass. framework_version: 1.3.0 -> 1.3.1 * fix(workflow): address review findings on the tracker status enum (#298) Follow-up to ca40df2, incorporating the maintainer and issue-author reviews. Blockers fixed: - CHANGELOG: the #298 entry had replaced the opening line of the #286 robots entry, leaving its body dangling under the new fork heads-up. Restored the deleted line and made the #298 entry self-contained above it (MadsLorentzen). - /notion-sync Step 4 now normalises legacy space spellings to the canonical underscore forms before setting the Status property. A raw push would auto-create a separate Notion select option per unique string, splitting closed applications across two filter buckets in an existing database (MadsLorentzen). Issue-author findings: - The vocabulary block now states that the space spellings are the same values as the underscore forms, not separate statuses, equally Final. Previously a reader applying the Open/Final lists literally landed on "not Final, not Open, undefined" for `offer declined`, and /apply Step 6b would refresh a closed application's row instead of appending (jakob1379). - The block moved below Step 1's closing --- as its own section: it was splitting Step 1's numbered list and silently truncating section-scoped reads of Step 1 to item 1 (jakob1379). - Open is derived by exclusion from the one explicit Final list, so a new status needs updating in a single place (jakob1379). - /html-report's bucket map gains a case-insensitive catch-all that maps unrecognised values to Rejected/Closed and names them once in the status breakdown - the #298 failure mode with a different input (jakob1379). - /apply Step 6b and /interview Step 0 anchor their final/open decisions to the vocabulary block (jakob1379). - /gmail-sync and /html-report drop their local restatements of the read-tolerance rule (jakob1379). Tests: html-report bucket assertions scoped to the Step 1 section; new pins for the equivalence clause, open-by-exclusion, block placement, the Notion normalisation, and the apply/interview anchors. |
||
|
|
fcefb8150f |
fix(web-research): stop treating a WebFetch 403 as a dead posting (#277)
* fix(web-research): stop treating a WebFetch 403 as a dead posting WebFetch sends a bot user agent, and many bank and corporate sites answer with HTTP 403 while serving the same page to a browser normally. Every command treated that as "page unavailable" and degraded silently rather than failing loudly: - /rank marked live postings `expired` - /apply fell back to search snippets, or to vague cover-letter prose - /scrape stored listing-page `#fragment` URLs, which fetch fine and return unrelated jobs, so every later /rank and /apply run on that entry failed Adds 09-web-research.md as 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-result snippet is a lead rather than a source. Wires it into /apply, /rank, /interview, /outcome, /notion-sync, the job-scraper skill, and writing-style rule 5. Bumps 03-writing-style.md to 1.2.0; 09-web-research.md starts at 1.0.0. Aggregator examples are given generically (LinkedIn, Indeed, national job boards) so the guidance holds in any market. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web-research): gate the browser-header retry on robots.txt Addresses review feedback on #277. WebFetch identifies itself as Claude-User and honors robots.txt, so a 403 has two very different causes and they must not be treated the same: a WAF default on a site whose published policy allows access, or a site that has actually declined. Retrying with browser headers in the second case circumvents the very opt-out mechanism site owners are told they can rely on, and the core framework cannot hold a looser standard than it asks of community forks. The escalation now runs tools/robots_check.py before the retry. A disallow for "*" or for "Claude-User" skips the retry entirely and goes to step 3 (find the employer's own posting). The rule is stated plainly in 09-web-research.md 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 from testing the gate against live sites, both pinned by tests/test_robots_check.py (15 offline cases): - The WAF usually blocks robots.txt too. privatebank.barclays.com returns 403 on the policy file to Claude-User and 200 to a browser, so a naive gate would block the retry on exactly the sites the retry is for. The checker reads the policy as a browser when the honest request is refused, then obeys it strictly - a policy you are prevented from reading cannot be honored, and robots.txt is not the protected resource. - urllib.robotparser cannot be used. It ends a record at a blank line and matches rules in file order, so Barclays' real file (blank lines between "User-agent: *" and its rules, "Allow: /" before "Disallow: /cs/") reads as everything-allowed. That fails open, in the one direction that matters. The checker implements RFC 9309 longest-match instead, with ties resolved to Disallow rather than Allow. Verified live: barclays /careers/ allowed and /cs/ blocked, ubs.com allowed, jobup.ch /api/ blocked while /en/jobs/ stays allowed. 09-web-research.md 1.0.0 to 1.1.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: kgb <kevingblackman@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1969d0ea70 |
feat(apply,interview): write confirmed facts back to the profile in the same turn (#211)
* feat(apply,interview): write confirmed facts back to the profile in the same turn The grounding audit added in #185 removes any claim the three sources do not support. That is right, and it has a consequence the framework did not close: the audit cannot tell a fabrication from a real fact the user stated out loud in an earlier session. Both look identical to it - absent from the sources - and both get stripped. So a fact that surfaces in conversation and is never written down is lost silently. A real metric the user confirmed disappears from every subsequent CV, and nothing reports that it happened. Adds a standing rule to /apply: when the user confirms, corrects or supplies a fact not already in 01-candidate-profile.md, write it there in the same turn and bump framework_version. 01 is one of the audit's three sources, so the fact is grounded on the next run. Adds the same exception to /interview rule 5, which previously forbade touching profile files outright. Interview prep is where new facts surface most often - a recalled metric, a corrected scope, a filled-in STAR stub - and prep files are not a substitute for the profile. Notes the source-consistency case explicitly: a fact added to 01 that CLAUDE.md and the master CV do not mention is an absence, not a contradiction, so it does not trip the audit's profile-consistency warning. If the new fact corrects either of those, fix it there too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fixup(apply,interview): drop framework_version bump from write-back rule Bumping 01's framework_version on a personal fact write-back corrupts check_upstream_updates.py's upstream_version > local_version signal, and diverges from /setup and /expand precedent where version tracks the file's structure, not personal-data edits to its content. Per MadsLorentzen review on PR #211. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ac6a734e16 |
fix(apply): name CVs main_<company>_<role> to avoid overwrites (#171)
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. |
||
|
|
75fbfc56e4 |
feat: add /interview command for stage-specific interview prep from the application archive (#58)
07-interview-prep.md holds STAR examples, tough-question patterns, questions to ask, and a roleplay protocol - and the job-application- assistant skill names interview preparation as its Step 4 - but no command operationalizes any of it. /apply ends at 'files ready', and the moment an interview is scheduled, nothing picks the thread back up. /interview does, drawing on the archive /outcome now maintains. /apply optimizes what the company reads; /interview optimizes what the company hears. The bridge is consistency: the interviewer read the submitted CV and cover letter, so prep must match what they claim. How it works: - Loads the application context from documents/applications/ <company>_<role>/: the exact posting, the submitted drafts, and outcome.md's stage + recorded feedback (stage-N feedback is the highest-priority input for stage-N+1 prep). Graceful fallbacks for applications that predate /outcome, stating plainly what is missing - Runs the Company Research Checklist that 04-job-evaluation.md defines but nothing executes, plus interviewer-angle notes from public professional info only. Every company claim is independently verified before it enters the pack - same rule as cover-letter claims - Builds a stage-specific prep pack: likely questions (earlier-stage feedback first, then fit-evaluation gaps with honest bridge answers per 07's you-don't-have-X pattern, then posting requirements, then stage type), STAR mapping via 07's Use-for tags with new drafts grounded strictly in profile facts, a consistency brief of the submitted documents' probeable claims, customized tough questions, 4-6 questions to ask, logistics tips - Saves the pack to documents/applications/<company>_<role>/ interview_prep_<stage>.md - one per stage, kept as history, gitignored with the rest of the archive - Offers a mock interview following 07's Roleplay Guidelines verbatim, with feedback calibrated against the behavioral profile (same voice-consistency rule the /apply reviewer applies) - Never edits framework or profile files, except appending user-approved STAR examples to 07 on explicit request Integration: /outcome suggests /interview when an interview stage is recorded; documents/README.md documents the interview_prep_<stage>.md files (and that /setup ignores them); README commands list and file tree. |