README.md (7492B)
1 # Resume and Cover Letter Build System 2 3 Resumes and cover letters are written in Markdown and rendered to PDF using pandoc and weasyprint. 4 5 ## Required Packages 6 7 ```bash 8 sudo pacman -S pandoc python-weasyprint 9 ``` 10 11 ## Directory Structure 12 13 ``` 14 job_search/ 15 ├── job_tracker.py # Application tracker CLI 16 ├── job_search.db # SQLite database 17 ├── job_postings/ # Raw job posting text files 18 └── resume/ 19 ├── Chris_Roberts_Resume.md # Master resume — never edit this 20 ├── resume.css # Stylesheet for resume PDFs 21 ├── cover_letters/ 22 │ └── cover_letter.css # Stylesheet for cover letter PDFs 23 ├── interview_prep/ # Interview prep documents 24 └── <role_dir>/ # One directory per application, named for the role 25 ├── Chris_Roberts_Resume.md 26 ├── Chris_Roberts_Resume.pdf 27 ├── Chris_Roberts_Cover_Letter.md 28 └── Chris_Roberts_Cover_Letter.pdf 29 ``` 30 31 Every role directory (e.g. `cei_help_desk/`, `springdale_ditt/`) holds its resume and cover letter under the same generic filenames — the directory name is what identifies the role, not the filename. This keeps paths predictable and lets scripts/globs find "the resume" or "the cover letter" for any role without knowing its name in advance. 32 33 ## Generating a Resume PDF 34 35 Run from inside the role directory: 36 37 ```bash 38 pandoc Chris_Roberts_Resume.md -o Chris_Roberts_Resume.pdf --pdf-engine=weasyprint --css=../resume.css 39 ``` 40 41 ## Generating a Cover Letter PDF 42 43 Run from inside the role directory: 44 45 ```bash 46 pandoc Chris_Roberts_Cover_Letter.md -o Chris_Roberts_Cover_Letter.pdf --pdf-engine=weasyprint --css=../cover_letters/cover_letter.css 47 ``` 48 49 ## Workflow 50 51 1. Create a new directory under `resume/` named for the role (e.g. `resume/company_role_name/`) 52 2. Copy `Chris_Roberts_Resume.md` into it and tailor the copy — never modify the master 53 3. Write `Chris_Roberts_Cover_Letter.md` in the same directory 54 4. Generate both PDFs using the commands above (from inside the role directory) 55 5. Confirm the cover letter PDF used `cover_letter.css`, not `resume.css` — check with `pdffonts`: it should embed Noto-Serif/Georgia, not Liberation-Sans/Arial 56 57 ## Job Tracker 58 59 Applications are tracked in a SQLite database at `job_search/job_search.db`, managed with `job_tracker.py`. 60 61 Add an alias to `~/.zshrc` for convenience: 62 63 ```bash 64 alias jt="python /home/cjr/claude/job_search/job_tracker.py" 65 ``` 66 67 Common commands (run from anywhere once the alias is set): 68 69 ```bash 70 jt list # all jobs, sorted by status 71 jt show 2 # full detail for job id 2 72 jt add "Company Name" "Job Title" # add a new job (interactive prompts) 73 jt add "Company Name" "Job Title" --location "Remote" --remote y \ 74 --posting_file "job_postings/x.txt" --resume_file "resume/x/Chris_Roberts_Resume.pdf" \ 75 --cover_letter_file "resume/x/Chris_Roberts_Cover_Letter.pdf" --status tracking \ 76 --fit_rating moderate --fit_notes "..." --notes "..." 77 # add a new job non-interactively (any --field 78 # from ADD_FLAGS in job_tracker.py; add 79 # `--field value` at all and prompts are skipped) 80 jt update 2 status interviewing # update status 81 jt add-interview 2 2026-07-01 in_person "Jane Smith, HR" "Went well, discussed..." 82 jt update-interview 3 outcome advanced # update an interview outcome 83 jt add-note 2 "Sent thank-you email" # append a timestamped note 84 jt add-contact 2 "Jane Smith" "HR Manager" # record someone you met 85 ``` 86 87 Valid statuses: `tracking`, `applied`, `phone_screen`, `interviewing`, `offer`, `rejected`, `withdrawn`, `hired` 88 89 ## Job Posting Inbox (automatic intake) 90 91 A systemd user service watches `job_postings/inbox/` and automatically screens anything dropped 92 there: `~/.config/systemd/user/job-inbox.path` fires `job-inbox.service` 93 (`job_search/scripts/process_inbox.py`) whenever the directory goes from empty to non-empty. 94 95 **Drop convention:** write the new posting file somewhere else first, then `mv` it into 96 `job_postings/inbox/` — a same-filesystem `mv` is atomic, so the watcher never sees a half-written 97 file mid-download or mid-save. Don't save directly into `inbox/`. 98 99 What the daemon does, automatically, per file: 100 1. Sanity checks (non-empty, reasonable size, valid UTF-8 text) 101 2. A cheap classification pass (small model, no tool access, tight budget cap) confirming it's 102 actually a job posting and pulling out company/title/location/salary/etc. 103 3. A dedup check against `job_search.db` (same company + title already tracked) 104 105 What it does NOT do: generate a resume/cover letter or write a `job_search.db` row. That step 106 writes files and runs shell commands, so it's deliberately left for a normal interactive Claude 107 Code session — no unattended permission bypass. On success, the daemon moves the posting into 108 `job_postings/ready/<file>` with a `<file>.meta.json` sidecar of the extracted fields, and fires a 109 desktop notification (`notify-send`) telling you to ask Claude Code to process it. From there it's 110 the same manual flow as every other posting: fit assessment, tailored resume/cover letter, PDFs, 111 `jt add`. 112 113 Everything else lands in a folder next to `inbox/` for you to look at, never silently: 114 `job_postings/rejected/` (not a job posting, or a duplicate — reason logged alongside), 115 `job_postings/failed/` (a real posting, but a transient API error — retry it), 116 `job_postings/throttled/` (more than 5 files landed at once — reviewed manually before being 117 re-dropped). These are deliberately siblings of `inbox/`, not children of it — a subdirectory 118 inside `inbox/` would make it permanently "not empty" and the daemon would never stop retriggering. 119 120 Enable/check status: 121 ```bash 122 systemctl --user enable --now job-inbox.path # turn the watcher on (persists across reboots 123 # while logged in; no linger, so it does NOT 124 # run while logged out) 125 systemctl --user disable --now job-inbox.path # turn it off 126 systemctl --user status job-inbox.path # confirm it's active 127 journalctl --user -u job-inbox.service -f # watch it process a drop live 128 tail -f job_search/logs/inbox.log # flat log, easier to grep than the journal 129 ``` 130 131 Run it by hand (e.g. to reprocess something without waiting for a trigger): 132 ```bash 133 python3 scripts/process_inbox.py 134 ``` 135 136 To retry something that landed in `failed/` (transient API error) or release files from 137 `throttled/` (burst overflow) back into the pipeline, just `mv` them back into `inbox/`: 138 ```bash 139 mv job_postings/failed/<file> job_postings/inbox/ 140 mv job_postings/throttled/<file> job_postings/inbox/ 141 ``` 142 143 ## Notes 144 145 - The `--pdf-engine=weasyprint` flag is required; pandoc's default LaTeX engine does not use CSS 146 - weasyprint will emit warnings about unsupported CSS properties — these are harmless 147 - Use two trailing spaces at the end of a line to force a line break within a paragraph (standard markdown hard break) 148 - Cover letter CSS is set to 10.5pt Georgia, 1in margins to fit content on one page