Commit Graph
7 Commits
Author SHA1 Message Date
Jakob Stender Guldberg 41b5fd857f fix(apply): record the drafted application in the tracker (#269) (#291)
/apply wrote a CV and a cover letter to disk and then wrote nothing to
job_search_tracker.csv, so a drafted and submitted application was
invisible to /gmail-sync, /html-report, /notion-sync, /interview,
/upskill aggregate mode, and to /rank's dedup exclusion. The safety net
that would have caught it - /gmail-sync - refuses to create missing
rows, so the failure it exists to catch is the one that disables it.
Nothing detected the loss afterwards.

Step 6b appends a drafted row carrying the two document paths, the fit
rating and the posting URL, reusing /outcome's exact header so the two
commands cannot diverge. It runs immediately after "Files Created" and
before the optional application-form offer, which ends the turn on a
question - anything placed after that offer would be skipped whenever
the user never answers, reproducing the bug. Re-running /apply updates
the row rather than duplicating it, and never moves a row that already
reached applied or beyond back to drafted. The step is mirrored into
job-application-assistant, which defers to it rather than restating it,
because /scrape Step 5 routes straight into the skill; /scrape Step 6
now defers to the same step instead of adding a row of its own.

seen_jobs.json is deliberately left alone: drafting is not applying, and
that file's vocabulary has no value for either. /rank builds its
exclusion set from company+role in the tracker regardless of status.

drafted is introduced into the status vocabulary, and every reader that
meant "submitted" is updated to say so. These readers define their open
set by exclusion from the final statuses, so a new non-final value would
otherwise have joined all of them silently: /outcome's follow-up branch
would have drafted a chase email to an employer who never received an
application, /gmail-sync would have searched for mail about it and then
flagged it as stale, /notion-sync would have published an "Applied on"
date for it, and /html-report would have counted it in the headline
application total. /outcome Step 4 also overwrites the draft date with
the submission date when a row leaves drafted, so the date column keeps
meaning "applied on". The wider vocabulary reconciliation - underscore
versus space, the separate archive enum - stays a separate concern.
2026-08-06 17:16:07 +02:00
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>
2026-08-04 20:36:55 +02:00
Ayobami Adegoke a0e81204da feat(outcome): add follow-up branch to chase quiet applications (#198)
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.
2026-07-20 20:37:22 +02:00
Jaewon Chung 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.
2026-07-17 21:26:04 +02:00
Mads LorentzenandClaude Fable 5 855b6725f3 brand: tagline line + the hired-moment coffee ask (#136)
* 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>
2026-07-12 11:17:48 +02:00
Ayobami Adegoke 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.
2026-07-08 17:08:32 +02:00
Ayobami Adegoke 4488290274 feat: add /outcome command to record application results and close the calibration loop (#54)
/setup Path A already mines documents/applications/<company>_<role>/
(job_posting.md, submitted drafts, outcome.md) to calibrate
04-job-evaluation.md and surface STAR candidates - but nothing in the
workflow systematically writes those folders, so the calibration machinery
only runs for users who hand-maintain the archive. /outcome closes the
loop: it writes the data /setup reads.

How it works:

- Identifies the application from job_search_tracker.csv (by argument, or
  by listing open applications); applications made outside the workflow
  get a new tracker row
- Records progress updates (interview stages, offers) and resolutions
  using the exact status enum documents/README.md documents, plus one
  additive value: in_progress, for open applications between updates.
  /setup's calibration only draws conclusions from final statuses
- Archives the submitted cv_draft.tex / cover_letter.tex (copy, never
  move; existing archived files are never overwritten - the archive is
  what was actually submitted) and fetches job_posting.md from the
  tracker's source URL while it is still alive; a dead URL gets a
  user-pasted copy or an explicit unavailable stub, never a
  reconstruction
- Updates the tracker row's status and notes; never restructures the CSV
- After 3+ resolved outcomes (or a repeating pattern), points the user
  back to /setup Path A - /outcome writes data, /setup interprets it,
  and this command never edits framework or profile files itself
- Idempotent: re-running appends stages and dated notes, never
  duplicates folders, rows, or history

Also aligns the outcome.md status enum across docs: setup.md Step A3
listed hired/rejected/no_response/interview_only while documents/README.md
already had offer_declined; both now carry the full enum including
in_progress. documents/applications/** and the tracker are already
gitignored, so all recorded data stays personal.

Docs: README (commands list, file tree), documents/README.md (/outcome
cross-reference and in_progress semantics), one-line handoff at the end
of /apply Step 6.
2026-07-07 19:40:12 +02:00