Commit Graph
10 Commits
Author SHA1 Message Date
Ayobami Adegoke cd7c22325b ci: run the Python test suite — CONTRIBUTING.md asks for tests CI never executes (#100)
* ci: run the Python test suite - CONTRIBUTING.md asks for tests CI never executes

CONTRIBUTING.md tells contributors to put Python tool tests in tests/
and run the relevant suites, and tests/ now holds real ones
(test_salary_lookup.py, test_convert_salary_excel.py from #75) - but no
CI job executes them. A suite that never runs in CI can't gate a PR and
silently rots. New python-tests job: unittest discover over tests/,
stdlib only, no new dependencies. Future test files run without any
workflow change.

Also lands tests/test_security_guards.py, which missed #84's merge
window (pushed to the branch as #84 was being merged; the merge took
2a6cb8c, the tests were 260c37a). 13 unittest cases in the existing
tests/ style: each copies the guard script into a synthetic repo tree
and runs it as a subprocess - the same way CI invokes it - asserting
real exit codes and messages. Every forbidden state fails (Bash(*) and
Bash(curl:*) additions, each personal-data gitignore rule removed one
at a time, each forbidden lifecycle script, trustedDependencies,
invalid settings JSON, zero manifests); every non-event passes (dropped
shipped permission, extra ignore rules, benign scripts, hostile
manifest inside node_modules); and the real repo passes its own guards.

22 tests total, all passing locally via the exact command the job runs.

* test: use benign lifecycle-script values in fixtures - AV heuristics flag attack-shaped strings

Review found the curl-pipe-to-sh fixture value matches a real Defender
signature (Trojan:Script/Stealer.HAX!MTB): Windows quarantines the temp
package.json mid-test, making the suite flaky for any Windows
contributor who runs it - while proving nothing extra, since the guard
flags the script KEY and never inspects the value.

Fixture values are now 'echo test' (also in the node_modules-ignored
test, same class of string), with a comment on the key-only test
explaining why the value must stay benign so a future 'make the fixture
realistic' cleanup doesn't reintroduce the quarantine flake. Coverage
is unchanged: same keys, same assertions, 13 tests passing.
2026-07-10 08:06:00 +02:00
Ayobami Adegoke 779a33aa20 docs: make CONTRIBUTING.md PR/issue references clickable (#91)
GitHub auto-links #N only in issue/PR comments and commit messages, not
in rendered markdown files - so all 26 precedent references in
CONTRIBUTING.md ((#30), (#55, #56), ...) render as plain text, and the
reader can't jump to the precedent being cited.

Converted every bare #N to a reference-style link ([#N] in prose, URL
definitions collected at the bottom of the file). Definitions use the
/issues/N form, which GitHub redirects to /pull/N when the number is a
PR, so each link resolves correctly whether it points at an issue or a
pull request. No prose changed; existing inline links (the Discussions
URLs) untouched.
2026-07-09 21:12:30 +02:00
Ayobami Adegoke e16afac7b9 ci: add supply-chain guards — permission allowlist, gitignore rules, manifest checks, pinned actions (#84)
* ci: add supply-chain guards - permission allowlist, gitignore rules, manifest checks, pinned actions

This template's threat model is unusual: it ships pre-approved Claude Code
permissions (.claude/settings.json) and CLI code that every fork user
executes via those permissions. A plausible-looking PR could therefore
ship risk to every forker: widen a permission to Bash(*), weaken the
personal-data gitignore rules, or smuggle code execution into bun install
via a lifecycle script. Nothing checked for these mechanically.

New job security-guards runs tools/security_guards.py (stdlib only):

- settings.json: every permissions.allow entry must be in an exact,
  in-repo allowlist. The guard makes permission changes loud, not
  impossible - a PR that intentionally widens permissions must update
  the allowlist in the same diff, so the widening is explicit and
  reviewable
- .gitignore: the personal-data rules (tracker, documents/**, cv/main_*,
  salary data, seen_jobs) must all still be present - the mirror image
  of the placeholder-integrity job
- .agents/**/package.json: no lifecycle scripts (preinstall/install/
  postinstall/prepare/prepack) and no trustedDependencies, which would
  execute arbitrary code during bun install on users' machines

New job dependency-review (PRs only): actions/dependency-review-action
flags newly introduced vulnerable or malicious dependencies,
fail-on-severity high.

Workflow hardening: explicit top-level permissions: contents: read
(least-privilege token), and all actions pinned to commit SHAs resolved
from the same major tags already in use (checkout v4, setup-python v5,
setup-bun v2), with the tag recorded in a comment.

Honest limit, recorded in the workflow header: a PR can edit this
workflow itself, so these guards catch accidents and casual attempts,
not a determined author. Branch protection with required checks and
human review of workflow/settings diffs remain the real backstop.

Verified locally: positive run passes; injecting Bash(*) into
settings.json, deleting the tracker gitignore rule, and adding a
postinstall script each fail the guard with the intended message, and
reverting restores a clean pass.

* ci: scope dependency-review to upstream PRs - forks lack Dependency graph by default

Verified on a fork: the action fails with 'Dependency review is not
supported on this repository' until Dependency graph is manually enabled,
and forks don't inherit it. Guarded with the same
github.repository == upstream condition the other upstream-only jobs use.
With the graph enabled the action passes, so the config itself is sound.

* ci: probe Dependency graph before dependency-review - warn and pass when unavailable

The upstream PR run showed Dependency graph is disabled on the upstream
repo too (the action hard-fails: 'Dependency review is not supported on
this repository'), not just on forks. Only the repo owner can enable it,
so a hard red X here is friction, not signal.

The job now probes the dependency-graph SBOM endpoint with the workflow
token first: HTTP 200 runs the real review; anything else emits a
::warning:: naming the setting to flip (Settings -> Advanced Security ->
Dependency graph) and passes. Same graceful-skip pattern the workflow
uses for optional tools - the check self-activates the moment the graph
is enabled, no workflow change needed.
2026-07-09 06:03:01 +02:00
Ayobami Adegoke 6477987608 ci: add GitHub Actions workflow - LaTeX smoke compiles, skill lint, CLI typechecks, placeholder integrity (#59)
Every PR to this repo is currently verified by hand. This adds the checks
a machine can do:

- latex-smoke: compiles cv/main_example.tex (lualatex) and the new
  cover_letters/cover_example.tex (xelatex) in the texlive/texlive
  container, failing on any LaTeX error. Exact page-count assertions
  (CV=2, cover letter=1) run on the upstream repo only
- lint (tools/lint_skills.py, also runnable locally): every SKILL.md has
  parseable YAML frontmatter with name+description (frontmatter breakage
  happened before - 37a0eed), allowed-tools 'bun run <path>' targets
  exist, command files start with a '# /<name>' title, settings.json is
  valid JSON with a permissions.allow list
- cli-typecheck: bun install + tsc --noEmit for all five portal CLIs
  (matrix, fail-fast off)
- placeholder-integrity (upstream only): tracked template files still
  carry their placeholder tokens, catching accidental personal-data
  commits before they land

Fork-friendly by design: /setup personalizes CLAUDE.md, the skill files,
and main_example.tex in forks, so placeholder checks and exact page
counts are guarded with github.repository == upstream; compile success
and lint run everywhere. Live CLI smoke tests are deliberately excluded:
network-flaky, and linkedin-search is personal-use-only per its own ToS
warning - CI-automated requests would violate it. CLIs are typechecked
instead.

The cover letter previously had no tracked example (cover_*.tex is
gitignored), so cover_example.tex is new: a placeholder letter following
the documented 06 structure, demonstrating the correct itemize-outside-
lettercontent pattern. It doubles as the structural reference /apply
Step 2 looks for on fresh clones, which until now matched nothing. The
gitignore exception is ordered after Cover_*.tex because case-insensitive
filesystems match that pattern against cover_example.tex too.

Writing it surfaced a latent bug in the documented template itself:
06-cover-letter-templates.md's structure ends with \closing{Kind
regards,\} - but cover.cls appends its own \, and the doubled break
produces '! LaTeX Error: There's no line here to end.' on every compile
(nonstopmode swallows it, so it went unnoticed). Fixed in 06 and noted
in the example.
2026-07-08 17:09:33 +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