Commit Graph
53 Commits
Author SHA1 Message Date
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 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 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 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 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 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
Oscar MaderaandJakob Stender Guldberg c855e11d22 fix(tracker): persist the application deadline end to end (#319) (#324)
The deadline is written at every moment it is provably in hand and survives
every write that follows: seen_jobs.json base field, /rank stored-value urgency
+ expiry sweep with Step 4 persistence, tracker 14th column with header-line-only
migration for existing files, /scrape-path extraction (assistant SKILL.md 1.3.2
-> 1.3.3), preserve-unparsed-fields in /outcome and /gmail-sync, notion-sync
deadline precedence. Design, scope analysis, and the folded refinements by
jakob1379 (#319, #328).

Co-authored-by: Jakob Stender Guldberg <17257805+jakob1379@users.noreply.github.com>
2026-08-16 19:56:45 +02:00
Ayobami Adegoke 1ab6c78332 docs(scraper): drop the phantom 'evaluated' status from seen_jobs.json vocabulary (#316)
The schema documented new/skipped/evaluated/ranked/expired, but
'evaluated' has had no writer and no reader since the initial release,
and post-#269 the tracker owns all lifecycle state after drafting.
Also tightens /rank Step 1's '--all' wording, which leaned on an
'applied' status the schema deliberately lacks.

Closes #315
2026-08-11 19:03:07 +02:00
Mads LorentzenandClaude Fable 5 0dc0f562fc docs(add-portal): honest-UA fetching contract line; relocate #304 entry to Unreleased
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>
2026-08-10 21:21:53 +02:00
Muhammad HaseebandClaude Opus 5 234c5d4ff5 docs(add-portal): specify credential handling for portals needing an API token (#304)
* docs(add-portal): specify credential handling for portals needing an API token

/add-portal can already scaffold a skill for a portal that returns usable
content only through a paid fetching service, but the spec says nothing
about the credential such a skill needs: no guidance on where it lives, no
requirement to document it, no defined failure mode when it is unset, and
no point at which the user learns the portal costs money per query.

Three additions, all to the generator spec:

- Step 2 gains a reconnaissance point for whether the portal needs a
  credential at all, to be raised with the user before scaffolding. A
  portal that bills per query is a different proposition from a free one
  and the user should get to decline it.
- The portal-skill contract gains a Credentials rule: environment variable
  only, named <SERVICE>_API_TOKEN, never a CLI flag (flags leak into shell
  history and process listings), never in url-reference.md or a fixture,
  and a MISSING_CREDENTIALS exit rather than a fallthrough to an
  unauthenticated request that fails confusingly.
- SKILL.md for such a skill must carry a Setup section naming the service,
  the variable, and the per-call billing.

Spec only. All six shipped portals are free and unauthenticated, so no
existing skill changes and the zero-dependency default is untouched.

Evidence this is not speculative: running /add-portal against portals that
sit behind bot-detection produced four token-requiring skills in my own
fork (Bright Data Web Unlocker), each of which had to invent its own
convention for reading, documenting, and failing on the token, because the
spec defines none.

Verified: lint_skills, check_framework_version, security_guards all OK;
python3 -m unittest discover -s tests 196 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(add-portal): subordinate the credential check to the robots.txt decision

Review catch: as written, Step 2.5 could be read as "robots.txt or the
terms said no, but a paid unlocker gets through, so here is the convention
for that" - which would let a paid fetcher launder a robots refusal.

That is not the intent and it contradicts the posture settled in #277/#286
(09-web-research.md, tools/robots_check.py): robots.txt is decided first
and honestly, and is never overridden by better fetching machinery.

State the subordination explicitly in 2.5 rather than leaving it implied by
step order, and record it in the changelog entry so the constraint survives
later edits to the step. The credential path is for portals whose robots.txt
permits access but whose bot protection blocks ordinary fetches - paying for
access a site allows, never paying past a site's no.

Wording follows the reviewer's suggestion.

Verified: lint_skills, check_framework_version, security_guards all OK;
python3 -m unittest discover -s tests 219 passed.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 21:19:43 +02:00
Ayobami Adegoke f658bb6f9a ci: discover portal CLIs dynamically so fork-added portals get checked (#310)
The cli-checks matrix hardcoded the six shipped portals, so a CLI
scaffolded by /add-portal in a fork shipped typecheck and test scripts
that fork CI never ran - while security_guards.py already globs
.agents/**/package.json and covers new portals automatically. A
discover-clis job now emits the matrix from
.agents/skills/*/cli/package.json; on upstream it resolves to the same
six portals, and a fork-added portal joins the matrix with no workflow
edit. /add-portal's Register step now says so.
2026-08-09 20:27:44 +02:00
Jakob Stender Guldberg 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.
2026-08-09 20:26:58 +02:00
Oscar Madera 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.
2026-08-07 20:47:24 +02:00
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
Mads LorentzenandClaude Fable 5 cffacfdde0 feat(portals): ship the Danish demo portals disabled, /setup enables them for Danish-market users (#288)
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>
2026-08-06 08:02:11 +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
Gabriel Ignacio MensiandClaude Sonnet 5 6392ca1628 Add a Language Gate - no dimension currently checks language requirements (#275)
Nothing in the framework checks a posting's language requirements
against what the candidate actually speaks. It is not one of the five
Scoring Dimensions in 04-job-evaluation.md, it is not checked in
/scrape's Step 3 fit assessment, and it is not a field in /rank's JSON
output - even though /apply's Step 1 already extracts a posting's
required language generically, with nowhere to report a mismatch to.

This adds a Language Gate, structured like the existing Eligibility
Gate (read the posting, classify against profile data, hard-stop on a
real mismatch), built on a new structured Languages table in CLAUDE.md
/ 01-candidate-profile.md. /setup now asks for it directly (Path C), or
infers it from a CV/LinkedIn export (Paths A/B - LinkedIn exports
already carry a self-rated Languages section).

The gate compares a posting's stated language requirements against
that table with three outcomes:

- Requires a language not declared at all -> hard FAIL, never
  presented.
- Requires a higher level in a language that is declared (e.g. "fluent
  English" against a declared B1/B2) -> FLAG, not an auto-reject -
  scored and drafted normally, with the gap surfaced so the candidate
  judges it themselves (a "fluent" bar reads very differently from a
  strict employer vs. one that's flexible on it).
- Requires a language at or below the declared level -> clean PASS.

Wired through the three places that need it: /scrape (Step 3), /rank
(new language_gate/language_note fields alongside the existing
location veto - both are now persisted to seen_jobs.json, not just
used transiently to decide one run's shortlist), and /apply (Step 1's
language extraction now has somewhere to report to).

Out of scope, deliberately: this does not touch the free-form
Deal-breakers list or how it's used elsewhere (e.g. Scoring Dimension
4's relocation check) - that's a separate question this change takes
no position on.

Validated with two live-testing passes against real, unfetched
postings (not fabricated text) across 3 portals and 3 market languages
(Danish, German, Spanish/Argentina): 8/8 postings gated correctly in
the first pass, including ambiguous real-world wording ("you
communicate well in English") a rigid rule would have gotten wrong. A
second pass, run specifically to force a hard-FAIL case, found one
(a Danish posting requiring the ability to read Danish) and confirmed
it persists correctly and would be excluded from /rank's shortlist.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 18:46:59 +02:00
Mads LorentzenandClaude Fable 5 d4b406efff docs(privacy): stop calling a fork 'private working space'; warn at /setup time
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>
2026-08-01 22:03:22 +02:00
NotAbdelrahmanelsayed 1cdaf9497f feat(rank): persist triage gaps and strengths into seen_jobs.json (#263)
/rank's Step 2 scoring agents already return strengths and gaps per job,
but Step 4 only persisted rank_score/rank_verdict/rank_date - both arrays
were printed once in Step 5 and then discarded. Store them verbatim in
seen_jobs.json (replaced, not accumulated, on --all re-ranks) so downstream
consumers can read real triage findings instead of re-deriving them.

Discussed in #258.
2026-07-31 17:41:41 +02:00
Novica Nakov 82a60300b6 feat(add-template): make /add-template engine-agnostic (#238)
* feat(add-template): make /add-template engine-agnostic so Typst can register alongside LaTeX

/add-template hardcoded a lualatex|xelatex|pdflatex engine enum and .tex
assumptions, so custom templates could only be LaTeX. Replace the enum with a
declared source extension + compile command, so any toolchain (Typst via
`typst compile`, or others) registers the same way stock LaTeX templates did.

Stock CV/cover-letter pipeline stays LaTeX and untouched (per #181).

Also fixes a latent bug this surfaced: apply.md's compile step ignored the
ACTIVE-TEMPLATE block and always ran lualatex/xelatex on .tex regardless of
the active template, and .gitignore's cv/main_*.tex pattern would not have
ignored a non-.tex draft (personal-data leak). Both now resolve from the
declared extension/command.

* fix(add-template): satisfy security_guards on the .gitignore Typst fix

security_guards.py pins the personal-data ignore rules by exact string and
gates negations through an allowlist, so broadening cv/main_*.tex and
cover_letters/cover_*.tex to *.* (for .typ drafts) needed a matching update
to REQUIRED_IGNORE_RULES.

Also tighten the .gitignore itself per review: keep the re-include
negations at .tex instead of widening them to *.* too. The stock example
files are always LaTeX, so .tex is enough to re-include them, and a
wildcard negation would have also re-included build artifacts
(main_example.pdf/.aux) that should stay ignored. ALLOWED_IGNORE_NEGATIONS
needs no change since the negations are unchanged.

Also adds a CHANGELOG entry under Unreleased for the Typst/custom-template
support.
2026-07-26 16:21:33 +02:00
LeoWinston-9596andClaude Opus 4.8 41ddc0c73c RFC feat(08): application-form fields as a third /apply artifact (#212)
* feat(08): add application-form fields as a third /apply artifact

/apply produces a CV and a cover letter. Many applications need a third
thing: free-text typed into a portal. Graduate programs, large-employer
ATS systems and startup forms ask for self-introductions, structured
project entries, motivation questions and pitches under a hard character
limit - none of which either document covers, and all of which the
interviewer reads alongside the CV.

Governing rule: a form field selects from what is already true and
arranges it for the question asked. It never introduces a new claim.
All accuracy rules from 03 and 05 apply unchanged.

Covers three field types (self-introduction, structured project entries,
hard character limits), the output format (a plain .txt the candidate
pastes from, with counts stated and internal NOTE TO SELF blocks marked
as not-for-pasting), and a verification checklist.

Two places where form fields are stricter than a CV, because both are
easy to get wrong:

- Project entries carry a name and a role, so they read as ownership of
  the whole project in a way a terse CV bullet does not. Contributory
  work has to be scoped inside the description.
- Project dates are the dates of the project, not of the employment.
  Narrowing them is more accurate where the candidate can say when the
  project started - but never invent a boundary to improve the ratio.

Registers the file in SKILL.md (framework_version 1.0.1 -> 1.1.0) and in
the FRAMEWORK_FILES list in tools/check_upstream_updates.py, so it is
covered by the update check like every other framework file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fixup(08,apply): wire as optional /apply offer; align grounding to three-source union

- apply.md Step 6: offer the third artifact after CV/cover letter are
  produced, mirroring the /outcome house pattern for optional capabilities
  (offer, act only on yes, default output unchanged).
- 08-application-forms.md: ground claims against the framework's
  01-candidate-profile.md + master CV + CLAUDE.md union (per #185)
  instead of only 01, in both the governing rule and the checklist.

Per MadsLorentzen review on PR #212. Rebase onto merged #210 (the
tenure-check reference this file cites) still pending — #210 hasn't
landed on upstream/master yet.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 16:05:42 +02:00
LeoWinston-9596andClaude Opus 4.8 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>
2026-07-26 16:04:23 +02:00
NotAbdelrahmanelsayed c7a1e0cf89 fix(rank): include job posting URL in ranking tables (#236)
* fix(rank): include job posting URL in shortlist/below-threshold/excluded tables

/rank's output tables listed title, company, and score but dropped the
posting link, forcing the user to go dig it out of seen_jobs.json to
open a job they wanted to act on. The key in seen_jobs.json is already
the URL, so this is a formatting fix, not a new lookup.

* fix(rank): link to entry's url field, not the seen_jobs.json key

Some portals key seen_jobs.json entries by a company+title composite
rather than the URL, so [Link](<key>) could render a broken link.
Every entry carries a dedicated url field, which is always valid.
2026-07-25 19:38:02 +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
Yuan Chen 61d17d1bda feat(commands): add /gmail-sync - confirm-before-write status sync from Gmail (#170)
Scans Gmail (via the claude.ai Gmail connector) for status signals on open tracked applications - interview invites, assessments, offers, rejections - and proposes them as a sourced batch the user must approve before anything is written to job_search_tracker.csv or outcome.md. Never proposes hired/offer_declined (user's real-world decision), routes conflicting signals to manual /outcome, appends-only to Notes, idempotent by message ID, read-only against the mailbox, and degrades to one message + clean exit without the connector (the /notion-sync precedent). State is gitignored personal data.

By @chenyuan99, who raised the connector-dependency tension himself and restructured classification and writing into separate approval-gated steps.
2026-07-19 19:38:15 +02:00
Jovin Nicholas f51a766e72 feat(apply): factual grounding audit against the union fact base (#185)
Reviewer-side Factual Grounding Audit: every date, employer, job title, and quantitative metric in both drafts is checked against the union of 01-candidate-profile.md + cv/main_example.tex + CLAUDE.md's Candidate Profile section (grounded if ANY source supports it; inter-source mismatches surfaced as profile-consistency warnings; draft drift returned as Part A edits with reason "grounding"). Drafter-side rule makes those three sources the sole source of truth for facts - existing tailored CVs are structure/phrasing reference only. Fixes the structural drift loop where tailored output fed back as source material; companion to the maintainer-side #178 setup fix.

Reported, diagnosed, and implemented by @jovin-nicholas (#177); the failure mode was validated empirically by the output benchmark (blind judges found the exact escalation class in pre-audit outputs).

Closes #177
2026-07-19 19:22:46 +02:00
Mads LorentzenandClaude Fable 5 122db059cb feat(apply): requirement-coverage rules and CV targeting improvements from output benchmark (#184)
Seven improvements sourced from blind regression probes comparing current
outputs against real April-2026 baselines (each an area where the older
outputs scored better):
- every stated requirement addressed - matched or honestly gapped, never
  silently omitted (the benchmark run omitted a stated Kubernetes
  requirement entirely; omission reads as hiding under questioning)
- nice-to-haves engaged by name with honest adjacency framing; posting's
  literal term preferred, including in CV section headings
- stated logistics/prerequisites addressed in the letter (clearances,
  availability, job ID, multi-country language mapping)
- domain-transfer argument leads the CV profile statement for
  domain-changers
- evidence links (href) on every verifiable named artifact
framework_version: 05 -> 1.2.0.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 22:22:05 +02:00
Mads LorentzenandClaude Fable 5 de91a97fe8 feat(apply): make CV language a profile setting, defaulting to English (#179)
The CV was hardcoded 'Always in English' - fine for the Danish/English
demonstration profile, a real disadvantage for fork users in markets
where applications are expected in the local language (cover letters
already match the posting's language). /setup now asks once and records
'CV language:' in CLAUDE.md's Identity section; /apply reads it with
English as the default, so existing users see zero behavior change.
The ATS keyword rule is reworded language-neutrally.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:39:51 +02:00
Mads LorentzenandClaude Fable 5 3847088986 fix(setup): ground Path A profile-statement extraction against the profile (#178)
Completes the loop behind #177: /apply drift could be archived by
/outcome, then laundered into 05-cv-templates.md as a reusable template
by /setup Path A - promoting a one-off drifted claim into source
material for every future application. Path A now verifies extracted
statements' factual claims against 01-candidate-profile.md (keeping
framing only), and 05-cv-templates.md marks [Used for:] statements as
phrasing references, never fact sources. framework_version 1.1.0.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:39:17 +02:00
Mads Lorentzen fb91be7a0b security: treat job postings as untrusted input across /apply and /rank (#175)
Prompt-injection hardening from the dataflow analysis in #173 by @Defaultuser361: data-not-instructions rules in /apply and /rank, reviewer research constrained to the user-confirmed company identity, writing-style verify rule tightened to independently located sources (framework_version 1.1.0), SECURITY.md private reporting channel, README note. Closes #173.
2026-07-17 21:48:59 +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
Adri f1ed475d59 feat(notion-sync): one-way read-only pipeline view in Notion via MCP (#169)
Adds /notion-sync per the conditions agreed in discussion #166: tool-agnostic sync contract with Notion as the in-tree reference binding, silently optional (covers unconfigured, headless, and unauthenticated states), read-only toward the repo with the gitignored sync-state file as its only local write, write-once page bodies, documents sync as filenames only. Complements /html-report: deep local dashboard vs glanceable anywhere-view.
2026-07-17 21:26:02 +02:00
Yuan Chen 848eddbecc feat(html-report): add /html-report command for application tracker dashboard (#131)
Self-contained HTML dashboard generated from job_search_tracker.csv and the application archives: stat cards, status/sector/channel/funnel charts as hand-generated inline SVG (no CDN, fully offline), HTML-escaped interpolation throughout, and a filterable applications table. Includes Python guards for the command file and the reports/ gitignore rule.
2026-07-16 21:48:59 +02:00
Yash Rajeshbhai DarjiandCursor 0a8fc194e5 docs: sync onboarding with linkedin-search + freehire-search reality (#150)
/setup still told non-DK users that built-in CLIs are Denmark-specific.
Align setup, /add-portal, and search-queries.md with shipped country-agnostic
CLIs and /scrape auto-discovery (post #85 / #95 / #102).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 14:30:49 +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 8bee3ddb97 docs(add-template): clean all test compile artifacts (#130) 2026-07-11 22:06:04 +02:00
Ayobami Adegoke 379abad503 docs(add-template): clarify --use activation metadata (#129) 2026-07-11 22:05:05 +02:00
Sai Sridhar Tarra 30e8b44759 docs: mention /interview as next step after /apply (#108) (#110) 2026-07-10 08:04:23 +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
Ayobami Adegoke fce2cf23c0 feat: add /rank command to triage scraped jobs into a ranked shortlist (#43)
/scrape finds and dedupes postings; /apply evaluates one at a time in
depth. Nothing connects the two ends: after a scrape returns 20 jobs, the
user eyeballs a table to decide where to spend /apply effort. /rank is the
bridge: batch-score every new posting against the fit framework and return
a ranked shortlist.

How it works:

- Selects jobs with status "new" from job_scraper/seen_jobs.json (--all
  re-ranks everything unapplied; a focus argument filters), excluding
  anything already in job_search_tracker.csv
- Dispatches parallel general-purpose agents (~5 jobs each) that WebFetch
  each posting and score the five dimensions from 04-job-evaluation.md.
  The rubric (skill match areas, career goals, deal-breakers) is passed
  inline per the same token-efficiency rules /apply uses; agents score
  only from actually fetched content and mark dead postings expired,
  never guessing from a title
- Triage depth by design: posting text vs. profile only - no company
  research, no salary lookups. /apply's Step 1 evaluation stays
  authoritative and always re-runs on handoff
- Aggregates with the framework's 30/25/15/30 weighting and verdict
  bands; location deal-breakers veto regardless of score; deadlines
  within 7 days get urgency flags and win ties
- Updates seen_jobs.json additively (status "ranked"/"expired" plus
  rank_score/rank_verdict/rank_date) so /scrape dedup keeps working;
  the tracker is read-only. Re-running is idempotent

Integration: job-scraper SKILL.md documents the new status values and
suggests /rank after large scrape batches; README (commands list, file
tree, quick-start step 4).
2026-07-07 17:31:31 +02:00
Ayobami Adegoke 2551824ecb feat: add ATS & keyword verification pass to /apply's compile-and-inspect loop (#40)
An ATS reads the compiled PDF's embedded text layer, not the rendered page,
and LaTeX can silently produce PDFs whose text extracts as garbage: icon
glyphs where contact details should be, (cid:*) markers from fonts without
Unicode mappings, interleaved lines from multi-column layouts. This matters
more now that /add-template lets users bring arbitrary templates. The
existing Step 5 loop verifies what a human sees; this adds verification of
what a parser sees.

New Step 5d in /apply (CV only - cover letters rarely go through keyword
screening; cleanup renumbered to 5e):

- Extract the CV PDF's text layer with pdftotext -layout. pdftotext
  (poppler) is an optional dependency: if missing, the mechanical check is
  skipped with a warning and keyword coverage falls back to the visual PDF
  read - the same graceful-skip pattern as salary_lookup.py
- Parseability checks verified against a real extraction of the stock
  template: email/phone must survive as literal text (fontawesome icons
  extract as harmless glyph-name noise like MOBILE-ALT/Envelope, but a
  contact detail carried only by an icon or hyperlink is invisible to ATS),
  no (cid:*) or replacement-character garbage, reading order matching
  visual order, dates present
- Keyword coverage reuses the required/preferred list from Step 1, matched
  in the posting's language, reported as covered / synonym-only /
  missing-have-it / missing-gap. Honesty rule enforced: keywords the
  profile genuinely supports get added to experience bullets; genuine gaps
  stay visible, never stuffed

Integration: CLAUDE.md verification checklist section, ATS Parseability
guidance in 05-cv-templates.md, narrow Bash(pdftotext:*) entry in the
pre-approved permissions (keeping with the tightened scope from #27),
cv/*.txt gitignored (extraction is personal data; also deleted by the
step itself), and optional-dependency docs in README and SETUP.
2026-07-07 06:28:59 +02:00
Ayobami Adegoke 79b153764d feat: add /add-portal command for generating local job-portal search skills (#37)
The README has always invited users outside Denmark to build equivalents of
the four Danish portal CLI skills, but doing so meant reverse-engineering
.agents/skills/*/cli/ by hand. /add-portal turns that invitation into a
guided workflow:

- Interviews the user for the portal URL, skill name, market/language
  (trigger phrases in the local language, like the Danish skills), and a
  realistic test query
- Investigates the portal before writing code: search-URL pattern, result
  structure (JSON API preferred over HTML), detail-page pattern, robots.txt
  and access rules. Auth-walled portals are declined; portals with
  restrictive terms get a prominent personal-use-only warning in the
  generated SKILL.md (same as linkedin-search)
- Scaffolds from the canonical structure with linkedin-search as the
  zero-dependency reference, enforcing the shared portal-skill contract:
  search/detail commands, common flags, {meta, results} JSON shape, stderr
  JSON errors, backoff on 429/5xx, chunked parsing
- Mandatory live test-run (search + detail + test suite) before registering
- Optionally wires the portal into /scrape via search-queries.md

The generator is country-agnostic; its output is market-specific and stays
in the user's fork, matching the repo policy that upstream remains a
universal template.

Docs: README (commands list, file structure, Job search tools section) and
SETUP.md (CLI install section pointer).
2026-07-06 21:04:44 +02:00
Ayobami Adegoke 6b144dc456 feat: add /add-template command for registering custom LaTeX templates (#30)
Users could already swap the stock moderncv/cover.cls templates, but only by
hand-editing the guidance in 05-cv-templates.md and 06-cover-letter-templates.md.
/add-template automates that:

- Interviews the user for the template's instructions: compile engine, fonts
  (bundled files or system), style rules to preserve, and hard page limit
- Stores the template profile-agnostic ([PLACEHOLDER] tokens) under templates/
  with a TEMPLATE.md manifest, so templates are safe to commit and share
- Runs a mandatory test compile with dummy data before registering anything
- Activates via a single managed block in 05/06, which /apply already reads,
  so no changes to the /apply workflow are needed; --use default is a clean
  revert to the stock templates
- --list and --use <name> manage multiple registered templates

Docs: README (commands list, file structure, LaTeX templates section) and
SETUP.md (compile section pointer).
2026-07-05 20:27:33 +02:00
Mads LorentzenandClaude Opus 4.7 974b29ec3a refactor: fold /setup_docs into /setup as third onboarding path (#9)
* refactor: fold /setup_docs into /setup as third onboarding path

- Auto-detect documents/ at /setup Step 0; offer three paths (documents
  folder, single CV import, interview).
- Inline document-driven merge logic into setup.md as Path A: read-before-
  write, additive vs. conflicting bucket merge with per-conflict prompts,
  inference labeling for behavioral and style files, STAR-stub generation
  instead of fabrication.
- Step 3 substeps for skill files skip when Path A populated them; non-
  skill substeps (CLAUDE.md, cv/main_example.tex, search-queries.md)
  always run. Path C interview preserved verbatim. /setup --section <name>
  update flow preserved.
- Delete .claude/commands/setup_docs.md.
- Update /reset prompts to point only at /setup. Update documents/README.md
  to reference /setup throughout. Update README.md to drop the standalone
  /setup_docs section and add upskill/ to the skills tree.
- Remove the trailing comma left in .claude/settings.local.json after the
  PR #6 revert (was technically invalid JSON).

Single onboarding entry point. documents/ folder convention unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(README): finish readme alignment for the /setup fold

- Update onboarding-paths tip to reflect three paths (was "Either"
  before /setup_docs landed in PR #6).
- Add documents/ and upskill/ entries to the file tree, both relied
  on by the new /setup Path A and by /upskill respectively.
- Add a brief "Other commands" section briefly describing /expand and
  /upskill so users discover them without having to read individual
  command files. /reset stays in the existing "Starting over"
  subsection, now linked from the new section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:57:33 +02:00
Bach 2ba441f3fb feat: add /setup_docs, /reset, /expand commands and /upskill skill
Adds four contributions from @Michael-Bach:

- /setup_docs - document-driven profile population from a documents/ folder (CV, LinkedIn export, diplomas, references, past applications). Idempotent merge with explicit additive vs. conflicting buckets and per-conflict prompts.
- /reset - typed-RESET confirmation gate for clearing profile data and/or documents folder contents.
- /expand - additive competency enrichment from documents and public URLs already in the profile (GitHub repos, portfolio sites), with web-searched syllabus lookups for named courses and certifications.
- /upskill - skill-gap analysis vs tracked jobs (or single URL), produces a prioritized heatmap and learning plan with year-tagged WebSearch queries.

Also adds the documents/ folder convention with README and gitignore entries for personal output files.

Closes #6
2026-04-29 09:30:08 +02:00
Mads LorentzenandClaude Opus 4.7 fa9c422d26 feat(apply): restore 02-behavioral-profile.md to reviewer reading list
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>
2026-04-24 20:14:59 +02:00